热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

LG3781[SDOI2017]切树游戏

题意题目描述小Q是一个热爱学习的人,他经常去维基百科学习计算机科学。就在刚才,小Q认真地学习了一系列位运算符,其中按位异或的运算符\(\oplus\)对他影响很大。按位异或的运算符是双目运

题意

题目描述

小Q是一个热爱学习的人,他经常去维基百科学习计算机科学。

就在刚才,小Q认真地学习了一系列位运算符,其中按位异或的运算符\(\oplus\)对他影响很大。按位异或的运算符是双目运算符。按位异或具有交换律,即\(i \oplus j = j \oplus i\)

他发现,按位异或可以理解成被运算的数字的二进制位对应位如果相同,则结果的该位置为\(0\),否则为\(1\),例如:\(1(01) \oplus 2(10) = 3(11)\)

他还发现,按位异或可以理解成参与运算的数字的每个二进制位都进行了不进位的加法,例如:\(3(11) \oplus 3(11) = 0(00)\)

现在小Q有一棵\(n\)个结点的无根树\(T\),结点依次编号为\(1\)\(n\),其中结点\(i\)的权值为\(v_i\)

定义一棵树的价值为它所有点的权值的异或和,一棵树\(T\)的连通子树就是它的一个连通子图,并且这个图也是一棵树。

小Q想要在这棵树上玩切树游戏,他会不断做以下两种操作:

Change x y 将编号为\(x\)的结点的权值修改为\(y\)

Query k 询问有多少棵\(T\)的非空连通子树,满足其价值恰好为\(k\)

小Q非常喜(bu)欢(hui)数学,他希望你能快速回答他的问题,你能写个程序帮帮他吗?

输入输出格式

输入格式:

第一行包含两个正整数\(n\) , \(m\),分别表示结点的个数以及权值的上限。

第二行包含\(n\)个非负整数\(v_1, v_2,\dots , v_n\),分别表示每个结点一开始的权值。

接下来\(n-1\)行,每行包含两个正整数\(a_i , b_i\),表示有一条连接\(a_i\)\(b_i\)的无向树边。

接下来一行包含一个正整数\(q\),表示小Q操作的次数。

接下来\(q\)行每行依次表示每个操作。

输出格式:

输出若干行,每行一个整数,依次回答每个询问。因为答案可能很大,所以请对\(10007\)取模输出。

输入输出样例

输入样例#1:

4 4
2 0 1 3
1 2
1 3
1 4
12
Query 0
Query 1
Query 2
Query 3
Change 1 0
Change 2 1
Change 3 3
Change 4 1
Query 0
Query 1
Query 2
Query 3

输出样例#1:

3
3
2
3
2
4
2
3

说明

对于\(100\%\)的数据,\(1 \leq a_i,b_i,x \leq n\) , \(0 \leq v_i,y,k ,修改操作不超过\(10000\)个。

分析

理论依据首推猫锟的解题报告。

算法讨论

如果只有一次询问,非常容易想到暴力 DP。先转有根树。在全局记录答案数组 \(ans(k)\) 表示权值为 \(k\) 的子树个数。对每个点 \(i\) 记录 \(f(i, k)\) 表示子树中深度最小的点为 \(i\) 且子树权值为 \(k\) 的连通子树个数。记录 \(g(i, j, k)\) 表示子树中深度最小的点为 \(i\) 且所有其他的节点都在 \(i\) 的前 \(j\) 个子节点的子树中的连通子树个数。那么我们就有以下方程(设 \(Ch(i)\)\(i\) 的子节点列表):

  • \(g(i, 0, k) = [k=v_i]\)
  • \(g(i, j, k) = \sum_{t=0}^{127} g(i,j-1,t)\times (f(Ch(i)_j, k\oplus t) + [k\oplus t = 0])\)
  • \(f(i, k) = g(i, |Ch(i)|, k)\)
  • \(ans(k) = \sum_{i=1}^n f(i, k)\)

总时间复杂度为 \(O(nm\times 128^2)\)

接下来可以注意到第 2 个式子是一个“异或卷积”的形式,不难想到使用 FWT 可以优化到 \(O(128\log 128)\)。然后注意到 FWT 之后,加法和乘法都可以直接按位相加,因此可以在一开始就将所有数组 FWT,运算过程中全部使用** FWT 之后的数组**,最后再将 $ans( * ) $ 数组 FWT 回去即可。这样就可以去掉一个 \(\log 128\)。时间复杂度为 \(O((n + \log 128)\times 128)\)

再接下来就是优化修改复杂度了。看过我论文或做过 BZOJ 4712 的同学容易想到使用链分治维护树上动态 DP。首先将树进行轻重链剖分,然后按照重链构成的树的顺序进行 DP。如果这样以后每一条重链上的转移可以高效维护、支持修改,那么每次修改点 \(p\) 之后,我们就可以高效地更新点 \(p\) 到根的 \(O(\log n)\) 条重链的信息即可。

首先 \(ans(k)\) 是全局变量,不好维护。那么可以不记录 \(ans( * )\),而是记录 \(h(i, k)\) 表示 \(i\) 子树中的 \(f(i, k)\) 的和,那么这样整个 DP 就有子树的阶段性了。

可以发现 \(f(i, k)\) 就是先将 \(g(i, 0, * )\) 和所有子节点 \(p\)\(f(p, k ) + [k = 0]\) 全部卷积起来的值。即如果设 \(F_i(z)\) 表示 \(f(i, * )\) 这一数组的生成函数,那么可以得出 \[F_i(z) = z^{v_i}\prod_{p\in Ch(i)} (F_p(z) + z^0) \] 这里的卷积定义为异或卷积。那么对于一条重链上的每一个点 \(i\),我们只需要将 \(i\) 的所有轻儿子 \(lp\)\(F_{lp}(z) + z^0\) 全部卷积起来,这样就考虑了所有轻儿子的子树中的贡献,设这个卷积的结果为 \(LF_i(z)\)。同样对于每个点我们记录 \(LH_i(z)\) 表示这个点的每个轻儿子的 \(H_{lp}(z)\) 之和(这里 \(H_i(z)\) 的定义类似 \(F_i(z)\),只不过是对 \(h(i, * )\) 定义的)。每个点的轻边的信息和可以用线段树维护来支持高效修改。

Claris 大神说这里信息可减因此不用线段树,但我觉得这里的 \(LF_i(z)\) 的信息相减需要做除法,如果出现 10007 的倍数则没有逆元,无法相除,因此我仍然采用线段树维护。

注意到上述算法只要求我们能求出 \(F_{重链顶}(z)\)\(H_{重链顶}(z)\),就可以维护父亲重链的信息或答案了。因此现在只需要考虑所有过当前重链的子树。在这里我们有如下两种截然不同的思路。

基于维护序列信息的算法

论文中提到的方法是转化为序列上的问题,然后使用线段树维护。由于连通子树和重链的交一定也是一段连续的链,那么我们显然就可以像最大子段和问题那样,记录 \(D_{a,b}(z)\) 表示 \(a=[\)左端点在连通子树中\(]\)\(b=[\)右端点在连通子树中\(]\) 的方案数。这个算法将修改复杂度优化为 \(O(128\log^2 n + 128\log 128)\),已经可以通过本题了。

但是这个算法有一定的问题:首先它具有较大的常数因子,运行时间较慢。其次,这个算法仍然利用了具体题目的性质——连通子树和重链的交还是链。而并非所有的题都有这样的性质。最后,由于要不重不漏地计数,代码细节繁多,十分难写。

基于变换合并的算法

对于一条重链,设重链上的点按深度从小到大排序后为 \(p_1,p_2,...,p_c\),那么我们可以得出以下方程:

  • \(F_{p_c}(z) = H_{p_c}(z) = z^{v_{p_c}}\) (因为 \(p_c\) 没有子节点)
  • \(F_{p_i}(z) = LP_{p_i}(z) \times (F_{p_{i+1}}(z) + z^0) \times {z^{v_{p_i}}}\)
  • \(H_{p_i}(z) = H_{p_{i+1}}(z) + F_{p_{i}}(z)\)

而我们所需要求的只有 \(F_{p_1}(z)\)\(H_{p_1}(z)\)

可以观察到上面这个式子中,向量 \(\left(F_{p_{i+1}}(z), H_{p_{i+1}}(z), z^0\right)\) 是通过一个线性变换得到向量 \(\left(F_{p_i}(z), H_{p_i}(z), z^0\right)\),具体地来说是右乘上这样一个矩阵:

\[M_i=\begin{pmatrix} LF_{p_i}{z}\times {z^{v_{p_i}}} & LF_{p_i}{z}\times {z^{v_{p_i}}} & 0 \\ 0 & 1 & 0 \\ LF_{p_i}{z}\times {z^{v_{p_i}}} & LH_ {p_i} + LF_{p_i}{z}\times {z^{v_{p_i}}} & 1 \end{pmatrix}\]

而矩阵乘法是满足结合律的,也就是说,我们只需要用线段树支持单点修改某个矩阵 \(M_i\)、维护矩阵的积,我们就可以高效地求出我们所需要的向量 \((F_{p_1}(z), H_{p_1}(z), 1)\)。而这是容易做到的,因此这个算法是完全可行的。这样,这个算法也将修改复杂度优化为了 \(O(128\log^2 n + 128\log 128)\),可以通过本题。

简单优化这个算法的常数。注意到形如 \[\begin{pmatrix} \underline{a} & \underline{b} & 0 \\ 0 & 1 & 0 \\ \underline{c} & \underline{d} & 1 \end{pmatrix}\] 的矩阵乘法对这个形式封闭,因为 \[\begin{pmatrix} \underline{a_1} & \underline{b_1} & 0 \\ 0 & 1 & 0 \\ \underline{c_1} & \underline{d_1} & 1 \end{pmatrix} \times \begin{pmatrix} \underline{a_2} & \underline{b_2} & 0 \\ 0 & 1 & 0 \\ \underline{c_2} & \underline{d_2} & 1 \end{pmatrix} = \begin{pmatrix} \underline{a_1 a_2} & \underline{b_1 + a_1 b_2} & 0 \\ 0 & 1 & 0 \\ \underline{a_2 c_1 + c_2} & \underline{b_2 c_1 + d_1 + d_2} & 1 \end{pmatrix}\]

因此我们只需要对每个矩阵维护 \(a,b,c,d\) 四个变量即可。同时可以直接用等号右边的形式来计算矩阵乘法,这样就只需要做 \(4\) 次而不是 \(27\) 次生成函数乘法了,常数大大减小了。

比较与扩展

这两个算法的时间复杂度相同,并且都可以扩展到询问某一个有根树子树的信息——只需要对那一条重链询问一下信息和/变换和即可。

我们来审视一下后一个算法。首先,这个算法基于的是直接对这个 DP 本身进行优化这样一种思想,而不是通过分析具体题目的性质进行处理,因此这种算法具有更高的通用性。其次,由于这个算法是直接对这个 DP 本身进行优化,因此正确性显然,细节也要少于论文中介绍的在区间中维护 \(D_{a,b}(z)\) 信息的方法(维护 \(D_{a,b}(z)\) 这个方法必须严格分类,因此细节繁多,常数也较大)。因此这个算法比前一个的算法更加优秀。

然而,事实上这个算法同样利用了题目的一些性质——这题是计数类问题,而且转移是线性变换,因此可以用矩阵来维护,而矩阵恰恰是一种可以合并的变换。那么对于其他的题目,是否也能用这种基于变换合并的算法呢? (答案是可以的,下文略)

再分析

猫锟的写法不利于实现,参照Achen的题解。

设为\(f(e)\)为当前子树中包含根节点的每种权值联通块数目的生成函数,\(g(e)\)为子树中所有的每种权值联通块数目的生成函数(g就是答案的生成函数啦)。y是x的儿子。
\[f_x(e)=e^{val_x}\prod f_y(e)+e^0\\g_x(e)=f_x(e)-e^0+\sum g_y(e)\]
每个\(f\)后面加了一个\(e^0\)这是为了处理乘起来的时候最后要加一个\(e^0\),我们直接把\(f\)定义中加一个\(e^0\),这样最后用的时候减去一个\(e^0\)就行了。

这个dp是可以用FWT优化的,FWT后可以直接乘除和加减,且可以最后再在根上IFWT回去得到需要的\(g_{root}(e)\),就非常方便了。
带修改我们仍然树剖,下面y为x的轻儿子。
\[f_x(e)=(e^{val_x}\prod f_y(e))*f_{mson}(e)+e^0 \\g_x(e)=(e^{val_x}\prod f_y(e))*f_{mson}(e)+g_{mson}(e)+\sum g_y(e)\]
写成矩阵
\[\begin{bmatrix}f_x \\g_x\\1\end{bmatrix}=\begin{bmatrix}e^{val_x}\prod f_y(e) & 0 &e^0\\e^{val_x}\prod f_y(e) & 1 & \sum g_y \\0 & 0 &1\end{bmatrix}\begin{bmatrix}f_{mson} \\g_{mson}\\1\end{bmatrix}\]
这样直接套上面那个模板,矩阵里面套数组,就可以\[O(nm\log n*3^3)\]了,应该是可以过的,如果写树剖上线段树再带一个log就不知道能不能过了。

这个矩阵同样具有封闭运算性质:
\[\begin{bmatrix}a_1 & 0 &b_1\\c_1 & 1 &d_1\\0 & 0 & 1\end{bmatrix}\begin{bmatrix}a_2 & 0 &b_2\\c_2 & 1 &d_2\\0 & 0 & 1\end{bmatrix}=\begin{bmatrix}a_1a_2 & 0 &a_1b_2+b_1\\c_1a_2+c_2 & 1 &c_1b_2+d_2+d_1\\0 & 0 & 1\end{bmatrix}\]
只需要对每个矩阵维护a,b,c,d就可以了。且这就是一个子段和的形式。不太清楚有没有什么直接得到子段和的方式。

既然舍掉了\(9 \times 9\)的矩阵,那么叶子节点如何初始化?方法是把叶子节点也写成4元素矩阵的形式,最后把系数矩阵乘以一个\(\begin{bmatrix}e^0 \\ 0\\ 1\end{bmatrix}\),相当于从一个虚拟节点转移过来就行了。

这题因为取模又是除法,10007的倍数没有逆元,所以要记录模意义下0的个数来达到除0的目的。

代码
#include
#define rg register
#define il inline
#define co const
templateil T read(){
    rg T data=0,w=1;
    rg char ch=getchar();
    while(!isdigit(ch)){
        if(ch=='-') w=-1;
        ch=getchar();
    }
    while(isdigit(ch))
        data=data*10+ch-'0',ch=getchar();
    return data*w;
}
templateil T read(rg T&x){
    return x=read();
}
typedef long long ll;

co int N=3e4+7,mod=10007,inv2=5004;
int n,m,val[N],UP,K,inv[mod+7];
char op[10];

void FWT(int a[],int f){
    for(int i=1;isz[mson[x]]) mson[x]=to[i];
    }
    hvson[x]=hvson[x]>1?1:0;
    nsz[x]=sz[x]-sz[mson[x]];
}

int p[N],ch[N][2];
#define lc ch[x][0]
#define rc ch[x][1]
bool isroot(int x) {return ch[p[x]][0]!=x&&ch[p[x]][1]!=x;}
void upd(int x){
    if(lc) sum[x]=sum[lc]*dt[x];else sum[x]=dt[x];
    if(rc) sum[x]=sum[x]*sum[rc];
}
int sta[N],top;
int build(int l,int r){
    int tot=0,ntot=0;
    for(int i=l;i<=r;++i) tot+=nsz[sta[i]];
    for(int i=l;i<=r;++i){
        ntot+=nsz[sta[i]];
        if(ntot*2>=tot){
            int x=sta[i];
            lc=build(l,i-1);if(lc) p[lc]=x;
            rc=build(i+1,r);if(rc) p[rc]=x;
            upd(x);return x;
        }
    }return 0;
}
int RT,tpf[N];
num tpff[N];
void getac(int x){
    get_f(dt[x].a,val[x]);
    if(hvson[x]){
        get(fy[x],tpf);
        for(int l=0;l(),read());
    dfs1(1,0);
    RT=dfs2(1);
    int Q=read();
    for(int cs=1;cs<=Q;++cs){
        int x,y;
        scanf("%s",op);
        if(op[0]=='C'){
            read(x),read(y);
            change(x,y);
        }
        else{
            read(K);
            for(int i=0;i

无关话题

既然是或卷积,那么矩阵中0和1的意思是什么呢?

乘以1我们想要得到原来的元素,所以1代表的应该是多项式\(e^0\)

乘以0我们想要得到0,所以0的意义就是多项式0。

综上,这个矩阵中的0和1是有意义的。


推荐阅读
  • 本文介绍了一个题目的解法,通过二分答案来解决问题,但困难在于如何进行检查。文章提供了一种逃逸方式,通过移动最慢的宿管来锁门时跑到更居中的位置,从而使所有合格的寝室都居中。文章还提到可以分开判断两边的情况,并使用前缀和的方式来求出在任意时刻能够到达宿管即将锁门的寝室的人数。最后,文章提到可以改成O(n)的直接枚举来解决问题。 ... [详细]
  • Linux环境变量函数getenv、putenv、setenv和unsetenv详解
    本文详细解释了Linux中的环境变量函数getenv、putenv、setenv和unsetenv的用法和功能。通过使用这些函数,可以获取、设置和删除环境变量的值。同时给出了相应的函数原型、参数说明和返回值。通过示例代码演示了如何使用getenv函数获取环境变量的值,并打印出来。 ... [详细]
  • 本文介绍了设计师伊振华受邀参与沈阳市智慧城市运行管理中心项目的整体设计,并以数字赋能和创新驱动高质量发展的理念,建设了集成、智慧、高效的一体化城市综合管理平台,促进了城市的数字化转型。该中心被称为当代城市的智能心脏,为沈阳市的智慧城市建设做出了重要贡献。 ... [详细]
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
  • 本文讨论了使用差分约束系统求解House Man跳跃问题的思路与方法。给定一组不同高度,要求从最低点跳跃到最高点,每次跳跃的距离不超过D,并且不能改变给定的顺序。通过建立差分约束系统,将问题转化为图的建立和查询距离的问题。文章详细介绍了建立约束条件的方法,并使用SPFA算法判环并输出结果。同时还讨论了建边方向和跳跃顺序的关系。 ... [详细]
  • c语言\n不换行,c语言printf不换行
    本文目录一览:1、C语言不换行输入2、c语言的 ... [详细]
  • 本文介绍了为什么要使用多进程处理TCP服务端,多进程的好处包括可靠性高和处理大量数据时速度快。然而,多进程不能共享进程空间,因此有一些变量不能共享。文章还提供了使用多进程实现TCP服务端的代码,并对代码进行了详细注释。 ... [详细]
  • 本文介绍了解决二叉树层序创建问题的方法。通过使用队列结构体和二叉树结构体,实现了入队和出队操作,并提供了判断队列是否为空的函数。详细介绍了解决该问题的步骤和流程。 ... [详细]
  • 本文介绍了PE文件结构中的导出表的解析方法,包括获取区段头表、遍历查找所在的区段等步骤。通过该方法可以准确地解析PE文件中的导出表信息。 ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • 本文介绍了一种划分和计数油田地块的方法。根据给定的条件,通过遍历和DFS算法,将符合条件的地块标记为不符合条件的地块,并进行计数。同时,还介绍了如何判断点是否在给定范围内的方法。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 本文介绍了P1651题目的描述和要求,以及计算能搭建的塔的最大高度的方法。通过动态规划和状压技术,将问题转化为求解差值的问题,并定义了相应的状态。最终得出了计算最大高度的解法。 ... [详细]
  • 本文介绍了C函数ispunct()的用法及示例代码。ispunct()函数用于检查传递的字符是否是标点符号,如果是标点符号则返回非零值,否则返回零。示例代码演示了如何使用ispunct()函数来判断字符是否为标点符号。 ... [详细]
  • 动态规划算法的基本步骤及最长递增子序列问题详解
    本文详细介绍了动态规划算法的基本步骤,包括划分阶段、选择状态、决策和状态转移方程,并以最长递增子序列问题为例进行了详细解析。动态规划算法的有效性依赖于问题本身所具有的最优子结构性质和子问题重叠性质。通过将子问题的解保存在一个表中,在以后尽可能多地利用这些子问题的解,从而提高算法的效率。 ... [详细]
author-avatar
CHERRYMJM
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有