1 条题解

  • 0
    @ 2025-12-11 7:01:00

    题解:动态维护树上异或值计数

    问题转化

    给定一棵 nn 个节点的树,每个节点有权值 viv_i0vi<m0 \le v_i < mmm22 的幂且 m128m \le 128)。
    定义一棵连通子树的价值为子树中所有节点权值的异或和。
    支持两种操作:

    1. 修改单个节点的权值
    2. 查询价值为 kk 的非空连通子树个数

    答案对 1000710007 取模。

    关键性质

    mm22 的幂,m128m \le 128,所以权值范围很小,可以用异或卷积处理。

    树形 DP

    fu[x]f_u[x] 表示以 uu 为根的子树中,包含 uu 且价值为 xx 的连通子树个数。
    gu[x]g_u[x] 表示以 uu 为根的子树中,不要求包含 uu(即可以包含 uu 的子树的某个连通块)且价值为 xx 的连通子树个数。

    初始化:fu[vu]=1f_u[v_u] = 1(只有 uu 自己),gug_u 初始全 00

    转移:考虑 uu 的一个子节点 vv,已经合并了前若干个子树的 fu,guf_u, g_u,现在加入子树 vv

    新的 fuf_u' 由两部分组成:

    1. 不含 vv 子树:fuf_u
    2. 包含 vv 子树:fuf_ufvf_v 的异或卷积(因为 uu 必须选,vv 子树也必须包含 vv

    具体来说: 令 h=fufvh = f_u \otimes f_v(异或卷积),则 $f_u'[x] = f_u[x] + \sum_{a \oplus b = x} f_u[a] \times f_v[b]$。

    但注意:fuf_ufvf_v 卷积表示 uu 所在的连通块和 vv 所在的连通块合并(通过边 (u,v)(u,v) 连接),形成新的连通块,价值为两者异或。

    因此:

    fu=fu+(fufv)f_u' = f_u + (f_u \otimes f_v)

    这里 \otimes 是异或卷积。

    对于 gug_u

    1. 不含 vv 子树:gug_u
    2. 包含 vv 子树:gvg_v
    3. fuf_ufvf_v 卷积后得到的新连通块也贡献给 gug_u(因为这些连通块在 uu 的子树内) 但 gug_u 实际上表示整个已合并子树中的答案,所以:
    gu=gu+gv+(fufv)g_u' = g_u + g_v + (f_u \otimes f_v)

    最终对于整棵树(任选根,比如 11),所有价值为 kk 的连通子树个数就是 groot[k]g_{root}[k]

    异或卷积优化

    由于 m128m \le 128,可以直接 O(m2)O(m^2) 卷积,但 n30000n \le 30000,总复杂度 O(nm2)O(n m^2) 过大。

    利用 Fast Walsh-Hadamard Transform (FWHT) 优化异或卷积到 O(mlogm)O(m \log m)

    对于两个数组 A,BA,B 长度 mmmm22 的幂),FWHT 可以在 O(mlogm)O(m \log m) 时间内计算 C=ABC = A \otimes B

    步骤:

    1. A,BA,B 做 FWHT 变换得 A^,B^\hat{A}, \hat{B}
    2. C^[i]=A^[i]×B^[i]\hat{C}[i] = \hat{A}[i] \times \hat{B}[i]
    3. C^\hat{C} 做逆 FWHT 得 CC

    由于模数 1000710007 很小,直接整数运算即可。

    动态 DP

    问题在于有修改操作,需要动态维护 fu,guf_u, g_u

    使用 动态 DP 技巧,将树剖分成重链,对每条重链用线段树维护矩阵乘积。

    定义状态向量 Fu=(fu,gu,1)F_u = (f_u, g_u, 1),其中 fu,guf_u, g_u 是长度为 mm 的数组,11 是常数 11

    转移可以写成矩阵形式: 设 uu 有轻儿子 v1,,vtv_1,\dots,v_t,重儿子 sonson。 令 LFu=fuLF_u = f_u 仅考虑轻儿子的贡献(即 fuf_u 初始为 [vu][v_u],然后与所有轻儿子的 fvf_v 卷积)。 类似定义 LGu=guLG_u = g_u 仅考虑轻儿子的贡献。

    FuF_u 可以由 FsonF_{son} 通过一个转移矩阵得到:

    $$\begin{bmatrix} f_u \\ g_u \\ 1 \end{bmatrix} = \begin{bmatrix} LF_u \otimes f_{son} & 0 & LF_u \\ LG_u + g_{son} + (LF_u \otimes f_{son}) & 1 & LG_u \\ 0 & 0 & 1 \end{bmatrix} \times \begin{bmatrix} f_{son} \\ g_{son} \\ 1 \end{bmatrix} $$

    实际上更精确的: fu=LFu+(LFufson)f_u = LF_u + (LF_u \otimes f_{son})(注意这里 LFuLF_u 是已经包含 uu 和轻儿子的基础数组) gu=LGu+gson+(LFufson)g_u = LG_u + g_{son} + (LF_u \otimes f_{son})

    因此转移矩阵 MuM_u 作用在 (fson,gson,1)(f_{son}, g_{son}, 1) 上得到 (fu,gu,1)(f_u, g_u, 1)

    由于 fu,guf_u, g_u 是数组,矩阵的每个元素是一个线性变换(即对数组的卷积或加法)。

    简化:标量化

    由于 m128m \le 128,我们可以将数组视为向量,转移矩阵就是 2m+12m+1 维的矩阵,但矩阵乘法复杂度 O(m3)O(m^3) 不可接受。

    更好的方法:直接在线段树节点维护 ffgg 数组,合并时用 FWHT 卷积。

    重链剖分 + 线段树

    对每个节点 uu,预处理:

    • lfulf_uuu 与所有轻儿子卷积后的 ff 数组(初始为 [vu][v_u]
    • lgulg_uuu 的所有轻儿子的 gg 之和

    对于重链,从叶子到根用线段树维护合并: 线段树节点 [l,r][l,r] 存储:

    • ff:该区间节点组成的连通块(包含 top)的 ff 数组
    • gg:该区间内的总答案

    合并两个相邻区间 [l,mid][l,mid][mid+1,r][mid+1,r]: 设左区间对应节点 uu(深度较小),右区间对应其重儿子链。 合并方式与 DP 转移相同: fnew=fu+(fufson)f_{new} = f_u + (f_u \otimes f_{son}) gnew=gu+gson+(fufson)g_{new} = g_u + g_{son} + (f_u \otimes f_{son})

    这样,线段树根节点存储的就是整条重链的答案。

    对于整棵树,从根开始,依次合并每条重链。

    修改操作

    修改节点 xx 的权值:

    1. 更新 xxlfxlf_x(因为 vxv_x 变了)
    2. 更新 xx 所在重链的线段树
    3. 由于 xxlflf 改变,会影响其父节点的 lflf(如果 xx 是父节点的轻儿子)
    4. 不断向上跳重链,更新受影响链的线段树

    每次更新复杂度 O(logn×mlogm)O(\log n \times m \log m)

    查询操作

    查询价值为 kk 的连通子树个数: 整棵树的答案存储在根所在重链的线段树根节点的 gg 数组中,直接输出 groot[k]mod10007g_{root}[k] \bmod 10007

    初始化

    先 DFS 预处理每个节点的 lf,lglf, lg,然后对每条重链建线段树。

    算法步骤

    1. 读入 n,m,v[],n, m, v[], 边,建树。
    2. 树链剖分,得到重儿子、top 等。
    3. DFS 计算每个节点的 lf,lglf, lg
      • lfulf_u 初始为只有 vuv_u 的数组(即 lfu[vu]=1lf_u[v_u]=1
      • lgulg_u 初始全 00
      • 对每个轻儿子 vvlfu=lfu+(lfufv)lf_u = lf_u + (lf_u \otimes f_v)(这里 fvf_vvv 为根的子树答案,需要递归计算) lgu=lgu+gvlg_u = lg_u + g_v
      • 计算 fu,guf_u, g_u(暂时用于初始化)
    4. 对每条重链建线段树,每个叶子节点对应一个节点 uu,存储 (lfu,lgu)(lf_u, lg_u)。 线段树合并时使用 FWHT 卷积。
    5. 处理操作:
      • Change x y: a. 更新 vx=yv_x = y b. 重新计算 xxlfxlf_x(初始化为 [y][y],然后与所有轻儿子的 ff 卷积) c. 更新 xx 所在重链的线段树 d. 设 ppxx 的父节点,若 xxpp 的轻儿子,则更新 pplfplf_p(需要除去旧的 fxf_x 贡献,加入新的 fxf_x 贡献) 由于模数小,无法直接除法,可以用“减-加”方式:$lf_p = lf_p - (lf_p \otimes old\_f_x) + (lf_p \otimes new\_f_x)$ 但注意这是卷积操作,不能简单加减。实际需要重新计算 pp 的所有轻儿子的贡献。 简单做法:记录 pp 的所有轻儿子列表,重新卷积。 e. 重复向上更新直到根。
      • Query k: 输出 groot[k]mod10007g_{root}[k] \bmod 10007
    6. 所有操作结果对 1000710007 取模。

    复杂度

    • 预处理:O(nmlogm)O(n m \log m)
    • 修改:O(logn×mlogm×O(\log n \times m \log m \times 轻儿子数)),轻儿子数均摊 O(logn)O(\log n)
    • 查询:O(1)O(1)

    由于 m128m \le 128,可过。

    • 1

    信息

    ID
    6079
    时间
    3000ms
    内存
    512MiB
    难度
    10
    标签
    递交数
    1
    已通过
    1
    上传者