动态DP(居然是毒瘤DP,那就不学了(bushi))
1.引子
让我们从一个常见的问题入手吧:
给定一棵 \(n\) 个点的树,点带点权,\(u\)点的点权为\(au\),求出这棵树的最大权独立集的权值大小。
Tip
树上最大独立集问题是指在给定的树中,找到一个具有最大节点数量的独立集。独立集是指在树中选择一组节点,使得这些节点两两之间没有边相连。换句话说,任何两个选择的节点之间都不存在直接连接。
解法显然,设dp\[u\]\[0/1\]分别表示以u为根节点的子树,不选/选择u节点的最大独立集权值。由定义可推出状态转移方程:
\\\begin{align} dp\[u0&=\sum_{v\in child(u)}^{} max(dpv0,dpv1)\\ dpu1&=\sum_{v\in child(u)}^{} dpv0 \end{align} \]
边界条件为:
\\\begin{align} dp\[u0&=0\\ dpu1&=au \end{align} \]
所以这道题就做完了......吗?
2.序
让我们再来看看丧心病狂的出题人干的好事:有 m 次操作,每次操作给定 \(x\),\(y\),表示修改点 \(x\) 的权值为 \(y\)。
\(1\le n \le 10^5, 1\le m \le 10^5\)(加强版甚至为 \(1\le n \le 10^6,1\le m \le 10^6\),朴素树剖都过不去!!!)
\(OMG!!!\) 出生出题人为了恶心\(OIers\)竟然丧心病狂到把静态数据变为动态数据了!!!
如果我们每次修改权值后都暴力再跑一次\(DP\),那么时间复杂度就会来到惊人的\(O(nm)\),显然是过不了这道题的。
那么我们只能另寻他法了。
注意到 ,时间复杂度之所以如此之高,是因为每次修改后,我们只会傻乎乎地全局跑一次\(DP\),但事实上,每次修改只会影响被修改的点到根节点的这一条链 ,所以不难想到使用树链剖分对其进行处理。(当然全局平衡二叉树、Toptree、LCT这个常数太大还是算了吧等也可以进行处理,甚至时间复杂度为单\(log(n)\),但是谁又能拒绝好想又好写的树剖呢?)
3.正题
终于讲到这里了qwq
将\(u\)节点的儿子分为重儿子\(wsonu\)和轻儿子,重儿子贡献直接累积到\(u\)节点,轻儿子的单独新开一个数组\(gu0/1\),分别表示
所有\(u\)轻儿子对 \(dpu0\) 的贡献以及所有轻儿子对 \(dpu1\) 的贡献再加上\(u\)自己的点权(之所以加上是为了方便转移),则:
\\\begin{align} g\[u0&=\sum_{v\in child(u),v\neq wsonu}^{}=max(dpv0,dpv1)\\ gu1&=\sum_{v\in child(u),v\neq wsonu}^{}=au+dpv0 \end{align} \]
则:
\\\begin{align} dp\[u0&=gu0+max(dpwson\[u]0,dpwson\[u]1)\\ dpu1&=gu1+dpwson\[u]0 \end{align} \]
于是,我们就可以愉快地进行矩阵优化了!定义\(max-plus\)为广义上的矩阵乘法(只要满足结合律即可)(\(max\)相当于\(+\),\(+\)相当于\(*\))
则:
\\\begin{bmatrix} dp\[u0\\ dpu1 \end{bmatrix} = \begin{bmatrix} gu0&gu0\\ gu1&-\infty \end{bmatrix} \begin{bmatrix} dpwson\[u]0\\ dpwson\[u]1 \end{bmatrix} \]
因此,对于每条链,若定义\(Topu,Endu\)为\(u\)所在链的顶端和链顶端所在链的结束位置,则对于 \(u\) 所在的一条重链,只有链顶 \(Topu\) 是轻节点 。那么修改节点 \(u\) 时,链内有且仅有 \(Topu\) 的父节点 \(fatherTop\[u]\) 的转移矩阵会被修改。考虑会修改成什么样。
首先,可以通过线段树维护重链上的转移矩阵,并同时维护区间乘积,但是需要注意运算顺序。此处因为进行了树链剖分,则有 \(dfnu=dfnwson\[u]-1\),因此线段树维护时"左乘右"即可。
修改 \(gi1\) 时,只需要在线段树上单点修改转移矩阵即可。
之后便是算出 \(\begin{bmatrix}ftop\[u]0\\ftop\[u]1\end{bmatrix}\),从而利用 \(fTop\[u]0,fTop\[u]1\) 更新 \(fatherTop\[u]\) 的转移矩阵。
4.C++代码
动态DP模板(数据加强版)
点击查看代码
//注意,这是加强版的,会有常数优化
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5,INF=0x3f3f3f3f;
int n,m;
int a[N];
int head[N],to[N<<1],nxt[N<<1],tot;
int fa[N],siz[N],wson[N],dfn[N],Top[N],End[N],rev[N],idx,root[N];
//父亲、子树大小、重儿子、dfs序、链顶、链尾、dfs序的反映射、编号、链顶在线段树上的编号
int f[N][2];
struct matrix{
int mat[2][2];
friend matrix operator *(matrix A,matrix B){
matrix C;
//手动展开,优化常数
C.mat[0][0]=max(A.mat[0][0]+B.mat[0][0],A.mat[0][1]+B.mat[1][0]);
C.mat[0][1]=max(A.mat[0][0]+B.mat[0][1],A.mat[0][1]+B.mat[1][1]);
C.mat[1][0]=max(A.mat[1][0]+B.mat[0][0],A.mat[1][1]+B.mat[1][0]);
C.mat[1][1]=max(A.mat[1][0]+B.mat[0][1],A.mat[1][1]+B.mat[1][1]);
return C;
}
};
matrix val[N],tr[N<<2];
int ls[N<<2],rs[N<<2],cnt;//动态开点,优化常数
void addedge(int u,int v){
to[++tot]=v;
nxt[tot]=head[u];
head[u]=tot;
}
inline void pushup(int u){
tr[u]=tr[ls[u]]*tr[rs[u]];
//注意,在推导矩阵形式的状态转移方程时,我们的转移矩阵是从左乘到右的,所以更新也必须是这个顺序(否则必须从右乘到左)
}
void build(int &u,int l,int r){
if(!u) u=++cnt;
if(l==r){
tr[u]=val[rev[l]];
return ;
}
int mid=(l+r)>>1;
build(ls[u],l,mid);
build(rs[u],mid+1,r);
pushup(u);
}
void update(int u,int l,int r,int pos){
//单点修改
if(l==r){
tr[u]=val[rev[pos]];
return ;
}
int mid=(l+r)>>1;
if(pos<=mid) update(ls[u],l,mid,pos);
else update(rs[u],mid+1,r,pos);
pushup(u);
}
void dfs1(int u){
siz[u]=1;
for(int i=head[u];i;i=nxt[i]){
int v=to[i];
if(v==fa[u]) continue;
fa[v]=u;
dfs1(v);
siz[u]+=siz[v];
if(siz[v]>siz[wson[u]]) wson[u]=v;
}
}
void dfs2(int u,int topchain){
dfn[u]=++idx;
rev[dfn[u]]=u;
Top[u]=topchain;
End[topchain]=max(End[topchain],dfn[u]);
//初始化
f[u][0]=0;
f[u][1]=a[u];
//初始化
val[u].mat[0][0]=val[u].mat[0][1]=0;
val[u].mat[1][0]=a[u];
val[u].mat[1][1]=-INF;
if(wson[u]){
dfs2(wson[u],topchain);
f[u][0]+=max(f[wson[u]][0],f[wson[u]][1]);
f[u][1]+=f[wson[u]][0];
}
for(int i=head[u];i;i=nxt[i]){
int v=to[i];
if(v==fa[u] || v==wson[u]) continue;
dfs2(v,v);
//根据定义进行状态转移
f[u][0]+=max(f[v][0],f[v][1]);
f[u][1]+=f[v][0];
val[u].mat[0][0]+=max(f[v][0],f[v][1]);
val[u].mat[0][1]=val[u].mat[0][0];
val[u].mat[1][0]+=f[v][0];
}
}
void updatepath(int u,int nval){
val[u].mat[1][0]+=nval-a[u];
a[u]=nval;
while(u){
matrix bef=tr[root[Top[u]]];
update(root[Top[u]],dfn[Top[u]],End[Top[u]],dfn[u]);
matrix aft=tr[root[Top[u]]];
//这就是为什么要记录链顶在线段树上的编号的原因。若不记录,则需额外调用两次查询函数求出链顶的值,这会增大常数。实际上,我们只需要链顶的值,并不需要复杂的查询操作
//所以,这样即可优化常数
u=fa[Top[u]];
if(!u) break;
val[u].mat[0][0]+=max(aft.mat[0][0],aft.mat[1][0])-max(bef.mat[0][0],bef.mat[1][0]);
val[u].mat[0][1]=val[u].mat[0][0];
val[u].mat[1][0]+=aft.mat[0][0]-bef.mat[0][0];
//根据定义进行更新
}
}
int main(){
cin.tie(nullptr)->sync_with_stdio(false);
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<n;i++){
int u,v;
cin>>u>>v;
addedge(u,v);
addedge(v,u);
}
dfs1(1);
dfs2(1,1);
for(int i=1;i<=n;i++)
if(Top[i]==i) build(root[i],dfn[i],End[i]);//对于每条重链单独建树
int lst=0;
while(m--){
int x,y;
cin>>x>>y;
updatepath(x^lst,y);
matrix ans=tr[root[1]];
lst=max(ans.mat[0][0],ans.mat[1][0]);
cout<<lst<<'\n';
}
return 0;
}
5.后记
呼呼,终于写完了这篇博客。
推荐大家去阅读一下TH911大佬的博客,我有很多疑问都是被TH911大佬的博客解决的(才不是因为别人的界面比我好看呢,哼😕!)