3841. Palindromic Path Queries in a Tree
- Array
- String
- Divide and Conquer
- Bit Manipulation
- Tree
- Depth-First Search
- Binary Indexed Tree
- Segment Tree
Intuition
A multiset of letters can be rearranged into a palindrome exactly when at most
one letter appears an odd number of times. Only parity matters, so a path can
be summarised by a 26-bit mask where bit c is the parity of letter c, and
the path is good when the mask has at most one bit set.
Parity masks combine with XOR. Root the tree at 0 and let P[x] be the XOR of
the letter bits of every node from the root down to x, inclusive. For a path
u .. v with lowest common ancestor l, P[u] ^ P[v] cancels everything from
the root down to l, including l itself. Adding l back gives
mask(u, v) = P[u] ^ P[v] ^ bit(s[l])
Updates are the hard part. Changing the letter at x from a to b XORs
bit(a) ^ bit(b) into P[y] for every y in the subtree of x, and nothing
else. In Euler-tour order a subtree is one contiguous range, so each update
is a range XOR and each P[y] lookup is a point query. A Fenwick tree over a
difference array handles both in O(log n): XOR the delta in at the range start
and again just past the range end, and a point value is the prefix XOR.
Approach
- Build the adjacency list and run an iterative DFS from node 0, recording
preorder positions
tin, parents, depths and the initial root-to-node masksbase. Subtree sizes (computed in reverse preorder) givetout = tin + size - 1. - Build binary-lifting tables for LCA.
- Keep a Fenwick tree of XOR deltas, all zero at the start. The current mask
of
xisbase[x] ^ prefixXor(tin[x]). update x c:d = bit(old) ^ bit(c). Ifd != 0, XORdattin[x]and attout[x] + 1, and store the new letter.query u v: findl = lca(u, v), computemask = P[u] ^ P[v] ^ bit(s[l]), and answermask & (mask - 1) == 0.
Complexity
- Time: O((n + q) log n) for the DFS, lifting tables, and log-time Fenwick and LCA work per query
- Space: O(n log n) for the lifting table
Edge Cases
u == v→ the path is one node, its mask has one bit, alwaystruen = 1→ every query isu == v- An update that sets a node to its current letter has delta 0 and changes nothing
- A path graph of 5 * 10^4 nodes is 5 * 10^4 deep, so the DFS must be iterative
- An update on the root changes every
P, which is the range[0, n - 1]; the end marker atnfalls outside the tree and is skipped - The LCA's letter is added back after the XOR; leaving it out gives the wrong
answer whenever
lis on the path, which is always
Code
class Solution:
def palindromePath(self, n: int, edges: list[list[int]], s: str, queries: list[str]) -> list[bool]:
"""
3841. Palindromic Path Queries in a Tree
Time: O((n + q) log n)
Space: O(n log n)
"""
adj = [[] for _ in range(n)]
for u, v in edges:
adj[u].append(v)
adj[v].append(u)
ch = [1 << (ord(c) - 97) for c in s]
# Iterative preorder DFS from node 0. base[x] = parity mask root..x.
parent = [0] * n
depth = [0] * n
base = [0] * n
tin = [0] * n
order = []
seen = [False] * n
seen[0] = True
base[0] = ch[0]
stack = [0]
while stack:
u = stack.pop()
tin[u] = len(order)
order.append(u)
for w in adj[u]:
if not seen[w]:
seen[w] = True
parent[w] = u
depth[w] = depth[u] + 1
base[w] = base[u] ^ ch[w]
stack.append(w)
size = [1] * n
for u in reversed(order):
if u != 0:
size[parent[u]] += size[u]
tout = [tin[u] + size[u] - 1 for u in range(n)]
# Binary lifting: up[k][x] is the 2^k-th ancestor of x (root maps to itself).
log = max(1, (n - 1).bit_length())
up = [parent]
for _ in range(1, log):
prev = up[-1]
up.append([prev[prev[x]] for x in range(n)])
def lca(u: int, v: int) -> int:
if depth[u] < depth[v]:
u, v = v, u
diff = depth[u] - depth[v]
k = 0
while diff:
if diff & 1:
u = up[k][u]
diff >>= 1
k += 1
if u == v:
return u
for k in range(log - 1, -1, -1):
if up[k][u] != up[k][v]:
u = up[k][u]
v = up[k][v]
return parent[u]
# Fenwick tree of XOR deltas over preorder positions (1-indexed).
tree = [0] * (n + 1)
def add(i: int, d: int) -> None:
i += 1
while i <= n:
tree[i] ^= d
i += i & -i
def mask_of(x: int) -> int:
m = base[x]
i = tin[x] + 1
while i:
m ^= tree[i]
i -= i & -i
return m
answer = []
for q in queries:
parts = q.split()
if parts[0] == "update":
x = int(parts[1])
new = 1 << (ord(parts[2]) - 97)
d = ch[x] ^ new
if d:
ch[x] = new
add(tin[x], d)
if tout[x] + 1 < n:
add(tout[x] + 1, d)
else:
u, v = int(parts[1]), int(parts[2])
mask = mask_of(u) ^ mask_of(v) ^ ch[lca(u, v)]
answer.append(mask & (mask - 1) == 0)
return answerimport java.util.*;
/**
* 3841. Palindromic Path Queries in a Tree
* Time: O((n + q) log n)
* Space: O(n log n)
*/
class Solution {
private int n, log;
private int[] depth, tin, tout, base, ch, tree;
private int[][] up;
public List<Boolean> palindromePath(int n, int[][] edges, String s, String[] queries) {
this.n = n;
// Adjacency list in CSR form
int[] deg = new int[n + 1];
for (int[] e : edges) {
deg[e[0] + 1]++;
deg[e[1] + 1]++;
}
for (int i = 0; i < n; i++) {
deg[i + 1] += deg[i];
}
int[] adj = new int[2 * (n - 1)];
int[] fill = Arrays.copyOf(deg, n);
for (int[] e : edges) {
adj[fill[e[0]]++] = e[1];
adj[fill[e[1]]++] = e[0];
}
ch = new int[n];
for (int i = 0; i < n; i++) {
ch[i] = 1 << (s.charAt(i) - 'a');
}
// Iterative preorder DFS from node 0. base[x] = parity mask root..x.
int[] parent = new int[n];
depth = new int[n];
base = new int[n];
tin = new int[n];
int[] order = new int[n];
boolean[] seen = new boolean[n];
int[] stack = new int[n];
int top = 0, idx = 0;
seen[0] = true;
base[0] = ch[0];
stack[top++] = 0;
while (top > 0) {
int u = stack[--top];
tin[u] = idx;
order[idx++] = u;
for (int j = deg[u]; j < deg[u + 1]; j++) {
int w = adj[j];
if (!seen[w]) {
seen[w] = true;
parent[w] = u;
depth[w] = depth[u] + 1;
base[w] = base[u] ^ ch[w];
stack[top++] = w;
}
}
}
int[] size = new int[n];
Arrays.fill(size, 1);
for (int i = n - 1; i > 0; i--) {
int u = order[i];
size[parent[u]] += size[u];
}
tout = new int[n];
for (int u = 0; u < n; u++) {
tout[u] = tin[u] + size[u] - 1;
}
// Binary lifting: up[k][x] is the 2^k-th ancestor of x (root maps to itself).
log = Math.max(1, 32 - Integer.numberOfLeadingZeros(n - 1));
up = new int[log][];
up[0] = parent;
for (int k = 1; k < log; k++) {
up[k] = new int[n];
for (int x = 0; x < n; x++) {
up[k][x] = up[k - 1][up[k - 1][x]];
}
}
// Fenwick tree of XOR deltas over preorder positions (1-indexed).
tree = new int[n + 1];
List<Boolean> answer = new ArrayList<>();
for (String q : queries) {
String[] parts = q.split(" ");
if (parts[0].equals("update")) {
int x = Integer.parseInt(parts[1]);
int next = 1 << (parts[2].charAt(0) - 'a');
int d = ch[x] ^ next;
if (d != 0) {
ch[x] = next;
add(tin[x], d);
if (tout[x] + 1 < n) {
add(tout[x] + 1, d);
}
}
} else {
int u = Integer.parseInt(parts[1]);
int v = Integer.parseInt(parts[2]);
int mask = maskOf(u) ^ maskOf(v) ^ ch[lca(u, v)];
answer.add((mask & (mask - 1)) == 0);
}
}
return answer;
}
private void add(int i, int d) {
for (i++; i <= n; i += i & -i) {
tree[i] ^= d;
}
}
private int maskOf(int x) {
int m = base[x];
for (int i = tin[x] + 1; i > 0; i -= i & -i) {
m ^= tree[i];
}
return m;
}
private int lca(int u, int v) {
if (depth[u] < depth[v]) {
int t = u;
u = v;
v = t;
}
int diff = depth[u] - depth[v];
for (int k = 0; diff > 0; k++, diff >>= 1) {
if ((diff & 1) != 0) {
u = up[k][u];
}
}
if (u == v) {
return u;
}
for (int k = log - 1; k >= 0; k--) {
if (up[k][u] != up[k][v]) {
u = up[k][u];
v = up[k][v];
}
}
return up[0][u];
}
}