
2026-06-13:查询树上回环旅途。用go谈话,给定一棵包含 n 个节点的无向树(节点编号为 0 到 n-1),树用数组 edges 暗示:edges 的长度为 n-1,每个元素为 [u, v],暗示节点 u 与 v 之间有一条无向边。
另外给定一个字符串 s(长度为 n,只包含小写英翰墨母),其中 s[i] 是分派给节点 i 的字符。
再给定多少操作 queries,每条操作有两种重要之一:
• update u c:把节点 u 的字符更新为 c,即令 s[u] = c。
• query u v:研讨从节点 u 到节点 v 的唯沿阶梯(包含 u 和 v 这两个端点)。把这条旅途上
• 统共节点对应的字符规律取出,组成一个字符串。判断这个字符串的字符能否通过重排得到一个回环串(回环串指正着读和反着读疏导)。
需要为每条 query 闭幕复返一个布尔值:能重排成回环串则为 true,不然为 false。复返闭幕按 queries 中出现的 query 限定组成数组。
1
edges.length == n - 1。
edges[i] = [ui, vi]。
0
s 由小写英翰墨母组成。
输入生成的 edges 暗示一棵有用的树。
1
queries[i] = "update ui c" 或
queries[i] = "query ui vi"。
0
c 是一个小写英翰墨母。
输入: n = 3, edges = [[0,1],[1,2]], s = "aac", queries = ["query 0 2","update 1 b","query 0 2"]。
输出: [true,false]。
讲明注解:
"query 0 2":旅途 0 → 1 → 2 得到的字符串是 "aac",不错重新陈列造成 "aca",这是一个回环串。因此,answer[0] = true。
"update 1 b":将节点 1 更新为 'b',咫尺 s = "abc"。
"query 0 2":旅途上的字符为 "abc",无法重新陈列造成回环串。因此,answer[1] = false。
因此,answer = [true, false]。
题目来独力扣3841。
一、举座解题中枢旨趣(前置铺垫)
1. 字符串可重排成回环的判定例则
一个字符串打乱后能组成回环,充要条目:最多唯有1种字符出现奇数次,其余全部偶数次。
用二进制掩码简化统计:26个小写字母对应26位二进制数,第k位为1代表字母'a'+k出现次数为奇数,0代表偶数;异或运算恰好能达成“次数奇偶翻转”。
• 旅途字符总奇偶掩码:二进制中1的数目 ≤ 1 → 输出true,不然false;
• 判断掩码1的个数:mask & (mask-1) == 0,仅0个/1个1时等式建立。
2. 树上两点旅途异或掩码数学公式
树上u到v的旅途 = 根→u + 根→v - 2×根→LCA(u,v) + LCA自身
异或性质:归拢个数异或两次会对消(x^x=0),因此旅途原始静态掩码公式:
xor(u,v) = rootXor[u] ^ rootXor[v] ^ rootXor[lca] ^ rootXor[lca] ^ val(lca)
化简:xor(u,v) = rootXor[u] ^ rootXor[v] ^ val(lca)
3. 单点字符修改的难点与树状数组差分决策
庸俗静态前缀掩码只可搞定不变字符,本题有update单点修改:修改节点x字符,等价于x整棵子树内统共点的根前缀掩码全部异或一个修碰巧val。
原因:纵情节点y在x子树内,根到y的旅途一定会经过x;x字符改变,统共子树节点的根旅途奇偶掩码全部翻转。
DFS本领戳将子树映射为通顺区间[in[x], out[x]],区间异或更新用差分树状数组(异或版Fenwick) 达成:
• 区间[l, r]异或val:update(l, val)、update(r+1, val);
• 查询单点in[x]的全局修正总掩码:树状数组前缀异或pre(in[x]);
• 最终实在根前缀掩码 = 原始静态rootXor[x] ^ 全局修正掩码pre(in[x])。
4. LCA求解姿首:二进制倍增
预搞定每个节点2^k级先人,O(logn)求两点最近全球先人,适配5e4范围数据。
二、代码完满分步引申历程(不含代码,纯逻辑拆解)
阶段1:输入数据运弯曲与无向树建说合表
1. 读取节点总额n、边数组edges、运转字符串s、操作数组queries;
2. 构建大小为n的说合表g:遍历每条边[u,v],双向添加(无向树),g[u]追加v、g[v]追加u;
3. 预分派全局存储数组:
• dep[]:每个节点深度;
• timeIn[]/timeOut[]:DFS入、出本领戳,象征子树区间;
• pa[][]:倍增先人表,pa[x][k]代表节点x进取跳2^k步的先人;
• pathXorFromRoot[]:静态根前缀异或掩码,未修改字符时的原始奇偶掩码;
• t[]:字节数组,及时保存每个节点现时字符,反馈update修改;
• clock:本领戳计数器,运转0。
阶段2:DFS遍历,完成本领戳、深度、静态掩码、一级先人预搞定
根固定为节点0,递归DFS(x, 父节点p):
1. 纪录x的2^0级先人pa[x][0] = p;
2. 本领戳clock自增,赋值timeIn[x] = clock;
3. 打算静态根掩码pathXorFromRoot[x]:父节点静态掩码 异或 现时节点字符对应二进制位;根点0单独运弯曲掩码为自身字符;
4. 遍历x统共说合子节点y,跳过父节点p:
• 子节点深度dep[y] = dep[x]+1;
• 递归引申DFS(y, x);
5. 统共子节点遍历完成后,赋值timeOut[x] = clock;此时x的整棵子树对应本领戳通顺区间[timeIn[x], timeOut[x]]。
阶段3:预搞定二进制倍增先人表(完满2^k级先人)
1. 打算最大倍增层数mx:取n二进制位数,保证2^mx隐讳树最大深度;
2. 逐层递推填充pa数组:从k=0到mx-2,遍历统共节点x:
• 若x的2^k级先人p≠-1,则x的2^(k+1)级先人 = p的2^k级先人;
• 若p=-1(无表层先人),则pa[x][k+1] = -1;
完成后可通过倍增快速进取跳纵情步数。
阶段4:封装两个器用函数
器用1:uptoDep(x, d)
将节点x进取跳,直到深度就是d:
滚球app2026世界杯中国官网下载1. 打算需要进取跳的步数差值k = dep[x]-d;
2. 轮回取出k最低位2^t,kaiyun(中国)2026世界杯官方网站x跳至pa[x][t],解除最低位,直到k=0;复返调治深度后的节点。
器用2:getLCA(x, y),求两点最近全球先人
1. 保证y深度更深,若dep[x]>dep[y]则交换x、y;
2. 调用uptoDep把y上跳到与x归拢深度;
3. 若x==y,径直复返x为LCA;
4. 从最大倍增层向下遍历到0:若x、y现时2^k先人不疏导,同步将x、y跳至各自先人;
5. 轮回收尾后x、y父节点即为LCA,复返pa[x][0]。
阶段5:运弯曲异或型树状数组Fenwick
1. 树状数组长度n+1(本领戳从1入手,下标1~n);
2. 树状数组重载异或更新、前缀异或查询逻辑,米兰milan(中国)体育官方网站替代庸俗加法;
3. 作用:真贵统共update操作带来的区间异或修正,单点查询得到该节点累计修正掩码。
阶段6:逐行搞定每一条查询操作,分两类分支
分支A:操作是 update u c(修改节点u字符为c)
1. 索要目的节点编号u、新字符c,读取节点u现时旧字符t[u];
2. 打算修正掩码val:旧字符二进制位 异或 新字符二进制位;val代表u子树统共节点掩码需要翻转的数值;
3. 更新全局字符数组t[u] = c,保存最新字符;
4. 对联树区间[timeIn[u], timeOut[u]]引申区间异或更新:
• 树状数组更新 timeIn[u] 位置,异或val;
• 树状数组更新 timeOut[u]+1 位置,异或val;
差分旨趣:区间内前缀查询会近似val,区间外对消为0。
分支B:操作是 query x y(查询x到y旅途能否重排回环)
1. 索要两个端点x、y;调用getLCA得到两点全球先人lca;
2. 辨认查询x、y本领戳对应的累计修正掩码:fx = f.pre(timeIn[x]),fy = f.pre(timeIn[y]);
3. 打算两点及时实在根前缀掩码:
realX = pathXorFromRoot[x] ^ fx
realY = pathXorFromRoot[y] ^ fy
4. 套旅途掩码公式打算整条旅途总奇偶掩码:
totalMask = realX ^ realY ^ (1
推导讲明:realX^realY对消根到LCA两段旅途,枯竭LCA自身字符,需单独补上;
5. 判断回环条目:totalMask二进制中1的数目≤1,等价于totalMask & (totalMask-1) == 0;
6. 将布尔闭幕存入谜底数组ans。
阶段7:统共操作搞定完结,复返ans数组当作最终输出
三、样例输入完满推演(缓助意会历程)
输入:n=3,边0-1、1-2,运转s=aac,查询3条
1. DFS后本领戳:in[0]=1,in[1]=2,in[2]=3;out[0]=3,out[1]=3,out[2]=3;
静态掩码:pathXor[0]=001(a)、pathXor[1]=001^010(a^a)=000、pathXor[2]=000^100(c)=100;
2. 第一条query 0 2:
LCA=1;无update修正fx=fy=0;
totalMask=001 ^ 100 ^ 010(lca字符a)=111;二进制两个1,高傲≤1 → true;
3. update 1 b:旧字符a(001),新b(010),val=011;区间[2,3]异或011;
4. 第二条query 0 2:
fx=pre(1)=0,fy=pre(3)=011;
realX=001^0=001,realY=100^011=111;
totalMask=001^111 ^ 010(lca咫尺是b)=100;二进制3个1 → false;
输出 [true,false],与样例匹配。
四、总本领复杂度分析
设节点数目n,查询操作数目q(n、q上限均5e4),倍增层数log₂n≈16。
1. 预搞定阶段复杂度
1. 建说合表:O(n)
2. DFS遍历整棵树:O(n)
3. 倍增先人表填充:O(n·logn)
总预搞定:O(n logn)
2. 每条操作复杂度
• update操作:两次树状数组更新,单次Fenwick O(logn) → O(logn)
• query操作:1次LCA(O(logn))+ 两次Fenwick前缀查询(各O(logn))+ 陋劣元运算 → O(logn)
全部q次操作总复杂度:O(q logn)
举座总本领复杂度
O((n + q) · logn)
n、q均5e4,logn≈16,总运算量可控,高傲数据范围要求。
五、总突出空间复杂度分析
1. 说合表g:存储n-1条双向边 → O(n)
2. dep、timeIn、timeOut、pathXorFromRoot、t数组:均长度n → O(n)
3. 倍增先人表pa:n × logn(logn固定16) → O(n logn)
4. 异或树状数组fenwick:长度n+1 → O(n)
5. 谜底数组ans:最多q个布尔值 → O(q)
总突出空间复杂度
O(n logn + q)
主导项为倍增表n logn,q与n同量级,实际可简化形容为O(n logn)。
Go完满代码如下:
package main
import (
"fmt"
"math/bits"
"strconv"
"strings"
)
type fenwick []int
func newFenwickTree(n int) fenwick {
returnmake(fenwick, n+1) // 使用下标 1 到 n
}
// a[i] ^= val
// 1
// 本领复杂度 O(log n)
func (f fenwick) update(i int, val int) {
for ; i
f[i] ^= val
}
}
// 打算前缀异或和 a[1] ^ ... ^ a[i]
// 1
// 本领复杂度 O(log n)
func (f fenwick) pre(i int) (res int) {
for ; i > 0; i &= i - 1 {
res ^= f[i]
}
return
}
func palindromePath(n int, edges [][]int, s string, queries []string) (ans []bool) {
g := make([][]int, n)
for _, e := range edges {
x, y := e[0], e[1]
g[x] = append(g[x], y)
g[y] = append(g[y], x)
}
mx := bits.Len(uint(n))
pa := make([][16]int, n)
dep := make([]int, n)
timeIn := make([]int, n) // DFS 本领戳
timeOut := make([]int, n)
clock := 0
pathXorFromRoot := make([]int, n) // 从根入手的旅途中的字母奇偶性的王人集
pathXorFromRoot[0] = 1
var dfs func(int, int)
dfs = func(x, p int) {
pa[x][0] = p
clock++
timeIn[x] = clock
for _, y := range g[x] {
if y != p {
dep[y] = dep[x] + 1
pathXorFromRoot[y] = pathXorFromRoot[x] ^ 1
dfs(y, x)
}
}
timeOut[x] = clock
}
dfs(0, -1)
for i := range mx - 1 {
for x := range pa {
p := pa[x][i]
if p != -1 {
pa[x][i+1] = pa[p][i]
} else {
pa[x][i+1] = -1
}
}
}
uptoDep := func(x, d int)int {
for k := uint32(dep[x] - d); k > 0; k &= k - 1 {
x = pa[x][bits.TrailingZeros32(k)]
}
return x
}
// 复返 x 和 y 的最近全球先人
getLCA := func(x, y int)int {
if dep[x] > dep[y] {
x, y = y, x
}
y = uptoDep(y, dep[x]) // 使 y 和 x 在归拢深度
if y == x {
return x
}
for i := mx - 1; i >= 0; i-- {
px, py := pa[x][i], pa[y][i]
if px != py {
x, y = px, py // 同期往上跳 2^i 步
}
}
return pa[x][0]
}
// 上头全是模板,底下入手本题逻辑
t := []byte(s)
f := newFenwickTree(n) // 阻扰树状数组是异或运算
for _, q := range queries {
if q[0] == 'u' {
x, _ := strconv.Atoi(q[7 : len(q)-2])
c := q[len(q)-1]
val := 1
t[x] = c
// 子树 x 全部异或 val,弯曲成对区间 [timeIn[x], timeOut[x]] 的差分更新
f.update(timeIn[x], val)
f.update(timeOut[x]+1, val)
} else {
q = q[6:]
i := strings.IndexByte(q, ' ')
x, _ := strconv.Atoi(q[:i])
y, _ := strconv.Atoi(q[i+1:])
lca := getLCA(x, y)
res := pathXorFromRoot[x] ^ pathXorFromRoot[y] ^ f.pre(timeIn[x]) ^ f.pre(timeIn[y]) ^ 1
ans = append(ans, res&(res-1) == 0) // 至多一个字母的出现次数是奇数
}
}
return
}
func main {
n := 3
edges := [][]int{{0, 1}, {1, 2}}
s := "aac"
queries := []string{"query 0 2", "update 1 b", "query 0 2"}
result := palindromePath(n, edges, s, queries)
fmt.Println(result)
}

Python完满代码如下:
# -*-coding:utf-8-*-
import sys
from typing import List
def palindromePath(n: int, edges: List[List[int]], s: str, queries: List[str]) -> List[bool]:
# 建图
g = [[] for _ in range(n)]
for x, y in edges:
g[x].append(y)
g[y].append(x)
# 二进制提高的层数
mx = n.bit_length
pa = [[-1] * mx for _ in range(n)]
dep = [0] * n
time_in = [0] * n
time_out = [0] * n
path_xor = [0] * n # 从根到现时节点的旅途异或值
path_xor[0] = 1
clock = 0
# 递归可能会很深,提高递归深度收尾
sys.setrecursionlimit(300000)
def dfs(x: int, p: int):
nonlocal clock
pa[x][0] = p
clock += 1
time_in[x] = clock
for y in g[x]:
if y != p:
dep[y] = dep[x] + 1
path_xor[y] = path_xor[x] ^ (1
dfs(y, x)
time_out[x] = clock
dfs(0, -1)
# 栽培倍增表
for i in range(mx - 1):
for x in range(n):
p = pa[x][i]
if p != -1:
pa[x][i + 1] = pa[p][i]
else:
pa[x][i + 1] = -1
def upto_dep(x: int, d: int) -> int:
# 将节点 x 提高到深度 d
diff = dep[x] - d
while diff > 0:
lsb = diff & -diff
step = (lsb.bit_length - 1) # 最低位 1 的位置
x = pa[x][step]
diff ^= lsb # diff -= lsb 也不错
return x
def get_lca(x: int, y: int) -> int:
if dep[x] > dep[y]:
x, y = y, x
y = upto_dep(y, dep[x]) # 提高到归拢深度
if y == x:
return x
for i in range(mx - 1, -1, -1):
px, py = pa[x][i], pa[y][i]
if px != py:
x, y = px, py
return pa[x][0]
# 树状数组(异或版块)
class Fenwick:
def __init__(self, size: int):
self.tree = [0] * (size + 1) # 下标 1..size
def update(self, i: int, val: int):
while i
self.tree[i] ^= val
i += i & -i
def pre(self, i: int) -> int:
res = 0
while i > 0:
res ^= self.tree[i]
i &= i - 1
return res
t = list(s) # 可变字符串
bit = Fenwick(n) # 最多用到 time_out[x]+1,time_out 最大为 n,因此 n 弥漫(实际上需要 n+1 大小,用 n+2 更安全)
# 为安全起见,不错扩大少量
bit = Fenwick(n + 5)
ans = []
for q in queries:
if q.startswith('u'): # update
parts = q.split
x = int(parts[1])
c = parts[2]
old = t[x]
val = (1
t[x] = c
# 子树区间差分更新
bit.update(time_in[x], val)
bit.update(time_out[x] + 1, val)
else: # query
parts = q.split
x = int(parts[1])
y = int(parts[2])
lca = get_lca(x, y)
res = (path_xor[x] ^ path_xor[y] ^
bit.pre(time_in[x]) ^ bit.pre(time_in[y]) ^
(1
ans.append(res & (res - 1) == 0) # 至多一个字母出现奇数次
return ans
if __name__ == "__main__":
n = 3
edges = [[0, 1], [1, 2]]
s = "aac"
queries = ["query 0 2", "update 1 b", "query 0 2"]
result = palindromePath(n, edges, s, queries)
print(result)

C++完满代码如下:
#include
#include
#include
#include
#include
using namespace std;
// 树状数组(异或版块)
class Fenwick {
vector tree;
int n;
public:
Fenwick(int size) : n(size), tree(size + 1) {} // 下标 1..n
// a[i] ^= val, 1
void update(int i, int val) {
while (i
tree[i] ^= val;
i += i & -i;
}
}
// 前缀异或和 a[1]^...^a[i], 1
int pre(int i) {
int res = 0;
while (i > 0) {
res ^= tree[i];
i &= i - 1;
}
return res;
}
};
vector palindromePath(int n, vector>& edges, string s, vector& queries) {
// 建图
vector> g(n);
for (auto& e : edges) {
int x = e[0], y = e[1];
g[x].push_back(y);
g[y].push_back(x);
}
// 倍增层数
int mx = 0;
while ((1
if (mx == 0) mx = 1; // 至少保证 1 层
vector> pa(n, vector(mx, -1));
vector dep(n, 0);
vector timeIn(n), timeOut(n);
int clock = 0;
vector pathXorFromRoot(n, 0);
pathXorFromRoot[0] = 1
// DFS 递归函数
function dfs = [&](int x, int p) {
pa[x][0] = p;
++clock;
timeIn[x] = clock;
for (int y : g[x]) {
if (y != p) {
dep[y] = dep[x] + 1;
pathXorFromRoot[y] = pathXorFromRoot[x] ^ (1
dfs(y, x);
}
}
timeOut[x] = clock;
};
dfs(0, -1);
// 栽培倍增表
for (int i = 0; i
for (int x = 0; x
int p = pa[x][i];
if (p != -1)
pa[x][i + 1] = pa[p][i];
else
pa[x][i + 1] = -1;
}
}
// 提高到指定深度
auto uptoDep = [&](int x, int d) {
int diff = dep[x] - d;
while (diff > 0) {
int lsb = diff & -diff;
int step = __builtin_ctz(lsb);
x = pa[x][step];
diff &= diff - 1; // diff -= lsb
}
return x;
};
// 最近全球先人
auto getLCA = [&](int x, int y) {
if (dep[x] > dep[y]) swap(x, y);
y = uptoDep(y, dep[x]);
if (y == x) return x;
for (int i = mx - 1; i >= 0; --i) {
int px = pa[x][i], py = pa[y][i];
if (px != py) {
x = px;
y = py;
}
}
return pa[x][0];
};
string t = s; // 可变字符数组
Fenwick bit(n); // 树状数组长度 n,下标 1..n
vector ans;
for (auto& q : queries) {
if (q[0] == 'u') { // update
int x; char c;
sscanf(q.c_str, "update %d %c", &x, &c);
char old = t[x];
int val = (1
t[x] = c;
// 子树区间差分
bit.update(timeIn[x], val);
bit.update(timeOut[x] + 1, val);
} else { // query
int x, y;
sscanf(q.c_str, "query %d %d", &x, &y);
int lca = getLCA(x, y);
int res = pathXorFromRoot[x] ^ pathXorFromRoot[y]
^ bit.pre(timeIn[x]) ^ bit.pre(timeIn[y])
^ (1
ans.push_back((res & (res - 1)) == 0);
}
}
return ans;
}
int main {
int n = 3;
vector> edges = {{0, 1}, {1, 2}};
string s = "aac";
vector queries = {"query 0 2", "update 1 b", "query 0 2"};
vector result = palindromePath(n, edges, s, queries);
// 输出闭幕
cout
for (size_t i = 0; i
if (i > 0) cout
cout
}
cout
return0;
}

咱们驯顺东谈主工智能为庸俗东谈主提供了一种“增强器用”,并发奋于于共享全观念的AI常识。在这里,您不错找到最新的AI科普著述、器用评测、提高效果的心事以及行业知悉。
迎接更动“福大大架构师逐日一题”开云·kaiyun体育,发音问可赢得口试贵府,让AI助力您的改日发展。