动态动态规划 & 全局平衡二叉树 小记
估计这几天是正式学习 ddp,所以特写笔记。
DDP 简介
是这样一类技巧,利用广义的矩阵乘法实现 单点修改权值,动态查询某个点的 DP 值
其核心思想是利用矩阵乘法具有结合律(可以使用数据结构维护)的优势
序列上的 Ddp
先看一个例子:最大子段和,显然我们有 \(f_{i}=\max(f_{i-1},0)+a_i\)
现在我们要支持修改点权,还是要求 \(\max f_i\)。
那么我们可以维护这样几个量:\(f_i,g_i=\max_{j\in [1,i]}f_j,a_i\)
也就是有:
那么写成 \((\max,+)\) 运算的矩阵形式也就是有:
我们使用线段树维护矩阵乘法,每次单点修改只需要修改这个矩阵,最后求出所有矩阵的积之后就可以求出答案了。
树上的 Ddp
来看一个模板题。
给定一个带点权的树,求其最大带权独立集权值之和
\(m\) 次询问,每次修改一个点的点权,修改永久生效
容易有如下暴力 dp:
设 \(f_{i,0/1}\) 为当前点是否取的最大权独立集大小(子树内)。
则显然有:
我们能否将其转化为一个序列上的问题呢?
树链剖分,我们可以利用重剖的优秀性质:一条链至多划分为 \(\log n\) 个区间。
那么我们只需要解决:
- 维护重链的矩阵运算结果
- 维护轻儿子(链顶)向上更新的辅助信息。
考虑将树进行轻重链剖分,同时维护 \(tf\) 表示仅考虑当前点合并轻儿子信息之后的答案,我们定义 \(u\) 的重儿子是 \(son_u\),容易有:
这样做的目的是:保证每个点有唯一前驱,以转化为序列,且容易修改辅助信息的和(至多有 \(\log n\) 次)
首先我们预处理出 \(tf,f\),然后对于每个节点 \(u\) 维护转移矩阵 \(mat_u\),将 \(tf\) 与 \(f_{son}\) 纳入考虑范围,容易有:
于是就可以知道 \(mat_u\) 的值了。
这样利用线段树维护区间积就解决了第一个问题。注意转移顺序,是深度大的乘上深度小的转移矩阵,所以线段树上是右区间乘左区间,并且还需要记录链底的编号。
然后我们考虑第二个问题 \(upd(x,k)\),考虑这样解决:
-
首先更改 \(mat_x\) 的值
-
跳到链顶 \(x\leftarrow top_x\)
-
使用旧 链顶 \(dp\) 值,撤销掉对 \(fa_x\) 的 \(tf\) 影响
-
取出新的链顶的 \(dp\) 值
-
使用 新 链顶 \(dp\) 值,加入对 \(fa_x\) 的 \(tf\) 影响
-
更新 \(fa_x\) 的转移矩阵
-
-
更新 \(x=fa_x\) 回到 \(2\)
当当前链顶是树根时候,不需要进行 \(2,3\) 操作
这个题目有一个有趣的性质是叶子节点的转移矩阵第一行可以等效为 \([f_0,f_1]\)。
一般情况下不具备这个性质时需要单独处理
复杂度是 \(O(n\log^2 n)\),带八倍矩阵乘法常数。
全局平衡二叉树
这时,你在做完模板题之后发现:你遇到了模板题的加强版:P4751
在数据规模扩大到 \(10^6\) 级别后,似乎树剖有些捉襟见肘,此时我们是否存在更加适合于 Ddp 的结构呢?它就是 全局平衡二叉树
如果您认为在下拙作较为晦涩,欢迎左转 Oi-wiki
算法介绍
我们注意到,树链剖分方法里,有一个地方开销是比较大的,就是利用线段树查询一段区间的矩阵积,考虑优化掉它。
有没有什么办法可以 \(O(1)\) 实现这一步呢?
可以的,我们考虑将每一条重链单独拿出,按照深度建立一颗二叉搜索树(这保证了左右子树加树根是一段连续区间),同时树根维护整个子树的矩阵积,这样的话,我们可以 \(O(1)\) 查询一个子树的矩阵积,就只需要实现如下操作:
- 跳到二叉树的父亲(假设存在,同一条重链),
pushup
- 跳出重链,类似树剖进行撤销和更新,同时继续往上跳。这一步显然是 \(O(\log n)\) 的总共,这一步我们可以对每个二叉树根的父亲设置为链顶在原树上的父亲
发现我们只需要保证跳跃总步数是 \(O(\log n)\) 级别即可。
考虑这样一种剖分策略:定义 \(lsz_u=sz_u-sz_{son_u}\),我们拿出一条重链上的点,做 \(lsz\) 的前缀和,取出它的带权中点(也就是 \(sum\le 2s[1,x]\) 的最小 \(x\)),将它设置为根,剩下两半递归处理。
考虑你从节点 \(u\) 开始向上跳,令一个 \(k\) 为二叉树子树内 \(lsz\) 的和,每跳一个二叉树上的点,这个 \(k\) 会翻倍,与此同时,你跳到另一棵树上时,\(k\) 是不会减少的,综上你每在二叉树上跳一次,那么会使得 \(k\) 翻倍,所以跳二叉树边总次数也是 \(\log n\) 级别的。
因此我们保证了跳跃总步数是 \(\log n\) 级别
建树后是:
我们称 二叉树边为重边,非二叉树边为轻边
建树是容易的,我们递归每一个重链的链顶,提取出这一条重链,对其递归建树,最后解决父亲的关系即可。
int divide(int l,int r){
if(l>r)return 0;
int sum=0;
for(int i=l;i<=r;++i)sum+=lsz[sta[i]];
for(int i=l,t=lsz[sta[l]];i<=r;++i,t+=lsz[sta[i]])if(sum<=t*2){
lc[sta[i]]=divide(l,i-1);
rc[sta[i]]=divide(i+1,r);
if(lc[sta[i]])fa[lc[sta[i]]]=sta[i];
if(rc[sta[i]])fa[rc[sta[i]]]=sta[i];
pushup(sta[i]);
return sta[i];
}
}
int build(int u,int f){
for(int x=u,l=f;x;l=x,x=son[x])for(auto v:e[x])if(v!=l&&v!=son[x])fa[build(v,x)]=x;
top=0;for(int x=u;x;x=son[x])sta[++top]=x;
return divide(1,top);
}
维护 Ddp
我们可以类似于平衡树的方法维护,只不过这个是静态树。
每个点建立两个矩阵 \(mat1,mat2\),其中 \(mat1\) 是自身原本的转移矩阵,而 \(mat2\) 是所在二叉树的子树内 \(mat1\) 合并的结果。
注意合并顺序与矩阵乘法顺序有关系,这一步可以通过 pushup
实现。
void pushup(int x){
mat2[x]=mat1[x];
if(lc[x])mat2[x]=mat2[lc[x]]*mat2[x];
if(rc[x])mat2[x]=mat2[x]*mat2[rc[x]];
}
往上跳的一步,如果没有跳出重链,那么直接 pushup
即可,否则类似树链剖分,撤销并更新 \(tf\)
void upd(int x,int v){
mat1[x].a[1][0]+=v-w[x];w[x]=v;
for(int i=x;i;i=fa[i]){
if(lc[fa[i]]!=i&&rc[fa[i]]!=i&&fa[i]){
int t=fa[i];
mat1[t].a[0][0]=mat1[t].a[0][1]=mat1[t].a[0][0]-get2(i);
mat1[t].a[1][0]-=get(i);
pushup(i);
mat1[t].a[0][0]=mat1[t].a[0][1]=mat1[t].a[0][0]+get2(i);
mat1[t].a[1][0]+=get(i);
}
else pushup(i);
}
}
题目选练
首先模板题已经没了。
保卫王国
比较水的题目啊。
首先考虑这玩意是最小点覆盖,如果用全集减去最大独立集来做的话就是模板题了。不过为了练习我们还是选择打一遍朴素 dp。
首先这个修改可以看作是将点权修改为 \(0\) 或者 \(\infty\),就是动态问题了。
同样考虑设 \(f,tf\),有:
改写为矩阵形式是:
改改模板就能过了吧。而且这玩意也不需要特判叶子。
洪水
这个题的主要目的是说明在全局平衡二叉树上如何查找一个点的 dp 值。
显然我们可以设 \(f_u,tf_u\),显然有转移 \(f_u=\min(w_u,\sum f_v)\)
那么也有:
由于转移涉及 \(w\),可以设计转移:
这里可能需要特判叶子。
这里我们考虑全局平衡二叉树如何查找一个节点的 dp 值,整个修改过程是不变的。
譬如我们需要找 \(u\) 的 \(dp\) 值,显然重链上在 \(u\) 后面的那些矩阵其实是 \(u\) 向上跳,当它走左边的时候右边的儿子,一个个组成的,也就是若干的子树操作,这里乘算即可。
int gans(int u){
mat now=mat1[u];if(rc[u])now=mat2[rc[u]]*now;
while(f[u]&&(lc[f[u]]==u||rc[f[u]]==u)){
if(lc[f[u]]==u){
if(rc[f[u]])now=mat2[rc[f[u]]]*mat1[f[u]]*now;
else now=mat1[f[u]]*now;
}
u=f[u];
}
return now.a[0][0];
}
Min-Max搜索 ZJOI2019
注意题目是 \(\max\),且每个叶子点权不一样。
那么答案的贡献路径是一条链
那么这条链的任何一个点它断掉之后都可以导致答案变化。
由于代价是 \(\max\),我们自然想到枚举 \(k\) 并且求解 \(\le k\) 的问题答案。
这时候我们可以用一个 \(dp\),设 \(f_u\) 为:
-
如果 \(u\) 不在答案链上
\(f_u\) 为其当前值仍然符合答案不变的要求的方案数,例如 \(dep\) 是奇数就是 \(<w_{rt}\),否则是 \(>w_{rt}\)
-
如果 \(u\) 在答案链上,\(f_u\) 为当前值不变的方案数。
那么不妨设 \(cnt\) 为叶子节点个数,则显然最终的变化方案数是 \(2^{cnt}-f_1\)。
因为我们保证了所有值不变,它们并未互相影响。
考虑转移
-
非叶子节点
显然你可以容斥转移,只要所有儿子非法,那么自己就可以取到一个合法解(例如自身取 \(\min\),这时候所有的儿子是取 \(\max\),它们要求 \(<w_{rt}\),只要它们全部 \(>w_{rt}\) 自身就满足 \(>w_{rt}\) 的要求了)。
也就是
\(f_u=\prod_{v\in Son(u)}(2^{c_{v}}-f_v)\),\(c\) 是子树内叶子节点个数
-
叶子节点
根据当前节点自身是否可以让其非法且其大小关系是否合适决定是否可以选择这个点让其合法找方案数即可。注意有情况是选了一定会导致非法。这里稍微分讨就好了。
-
答案链上点
根据上述非叶子节点的分析
- 对于不是答案链上的儿子,乘上 \(2^{c_v}-f_v\)
- 对于是答案链上的儿子,乘上 \(f_v\)
这样每次暴力更改可以获得 70 高分。
void dp(int u,int fa,int lim){
if(lef[u]){
if(abs(u-wrt)<K&&(lim&1)==(u<wrt))f[u]=1;
else f[u]=((dep[u]&1)==(u<wrt))<<1;
return ;
}
f[u]=1;
for(int v:e[u])if(v!=fa){
dp(v,u,lim);
f[u]=(pw[sz[v]]+p-f[v])%p*f[u]%p;
}
}
void dfs(int u,int fa){
f[u]=1;
for(auto v:e[u])if(v!=fa){
if(wrt==val[v])dfs(v,u),f[u]=f[u]*f[v]%p;
else dp(v,u,dep[u]),f[u]=(pw[sz[v]]+p-f[v])%p*f[u]%p;
}
}
注意到答案实际上是删掉答案链后,所留下的森林里每棵树的 \(2^{c_v}-f_v\) 的积,并且每次 \(k\) 增大 \(1\) 所改变的点的点权个数是 \(O(1)\) 的,可以考虑对于每个树都使用全局平衡二叉树进行维护。
那么我们就可以设 \(M_u=2^{c_u},tf_u=\prod_{v\in Son(u),v\neq son}(M_v-f_v),C_u=\prod_{v\in Son(u),v\neq son}M_v\)。
就有:
注意到乘法可能有零而导致撤销出错,可以维护一个 \(rl_u\) 表示 \(tf_u\) 中去掉所有零之后的数字,同时维护 \(cnt0_{u}\) 表示计算过程中的零的个数来得到真实值。
切树游戏 SDOI2017
考虑异或卷积,设 \(F_u(x)\) 为子树 \(u\) 的连通子图(也就是 \(u\) 是深度最小的点)其权值为 \(k\) 的异或生成函数,定义 \(F(x)·G(x)=\sum\sum f_ig_jx^{i\oplus j}\)
注意 \(m\le 128,q\le 30000,change \le 10000\)
感觉有点像 \((n+q)m\log n\) 的味道
考虑暴力DP,可以维护 \(FWT(F_u(x))\),这样有 \(FWT(F_u(x))=FWT(x^{w_u})·\prod_{v\in Son(u)}(FWT(F_v(x))+FWT(0))\)
答案就是 \([x^k]\sum_u IFWT(FWT(F_u(x)))=[x^k]IFWT(\sum FWT(F_u(x)))\)
可以考虑再维护一个 \(FWT(G_u(x))\) 所有子树的 \(FWT(G)\) 加上 \(FWT(u)\)
这样做是 \(nm\log m\) 单次。
这有个弊端就是你每次都必须要把这个 \(IFWT\) 的整个数组弄出来。
直接维护这个向量就好了吧,这个向量对应位置 \(+,*\) 的运算也满足矩阵乘法的要求。矩阵乘法的内部元素可以是一个向量。
不妨设 \(f_u,g_u\),令 \(·\) 为全一那就有:
那么按常理,维护 \(tf_u,tg_u\) 有:
而 \(f,g,tf,tg\) 满足乘法分配律,结合律,加法结合律,交换律,所以我们可以把它当作矩阵来弄。
有:
注意到在 \(son\) 是叶子时,左侧矩阵与底部行相同,所以可以不用特判
但是修改时怎么撤销呢?预处理所有逆元即可?0 怎么办?。
事实上我们可以像上一个题一样考虑 \(tf\) 的撤销,可以将 \(tf\) 的每一项再维护一下非零值的乘积以及当前零的个数就可以方便的做撤销了,然后再提取值即可。
现在复杂度变成了 \(O(qm\log n)\),带矩阵乘法 27 倍常数,果不其然 TLE。循环展开同样无果。
不必气馁,我们探求一下这个矩阵有什么性质:
它仍然满足这种形式,可以被 \(a,b,c,d\) 表达,所以我们只需要维护 \(a,b,c,d\) 四个量即可计算矩阵乘法。
这样就降低到了 4 倍常数,可以通过。