动态DP
动态 \(DP\)
引入:
一般来说,树上 \(DP\) 问题是不做改变的,只用计算一遍就行了。
但是给你说:更改一个点的权值,再去询问你答案。
这时候再算一遍?或者是只处理这个点的父亲部分?但是这样万一成链,那不是时间复杂度又爆了?
所以,我们引入了 动态 \(DP\) 这个概念。
解决问题:
直接说怎么做不好说,还是用例子解决。
例题:
给定一棵 \(n\) 个点的树。\(i\) 号点的点权为 \(a_i\)。有 \(m\) 次操作,每次操作给定 \(u,w\) 表示修改点 \(u\) 的权值为 \(w\)。你需要在每次操作之后求出这棵树的最大权独立集的权值大小。
考虑没有修改做法:
最大权独立集可以理解成相连的点不能同时选,因此很容易转移:设 \(dp[i][0/1]\) 表示 \(i\) 点为根选择/不选择 \(i\) 时的最大值,则有转移方程:
其中若 \(i\) 为叶子节点,则 \(dp[i][0]=0,dp[i][1]=val[i]\) 。
答案就是 \(max(dp[1][0],dp[1][1])\).
带上修改
这里,我们使用了重链剖分。因为有以下性质:
- 不会形成特别长的链,时间复杂度不会炸
- 重链的链尾都是叶子节点,且只有叶子节点无重儿子
- 一条重链所在的区间在 \(dfs\) 序上是连续的一段区间,可以用数据结构维护。
我们考虑一些微观问题:在一条链里,怎么支持快速修改和查询这条链的 \(DP\) 值 。
我们定义一个 \(g\) 数组:
\(g[i][1]\) 表示 \(i\) 点的轻儿子都不取的最大权独立集
\(g[i][0]\) 表示 \(i\) 点的轻儿子可取可不取形成的最大权独立集。
其中,对于叶子节点 \(g[i][0]=g[i][1]=0\) , \(j\) 为 \(i\) 的重儿子,则有方程:
$$dp[i][0]=g[i][0]+max(dp[j][0],dp[j][1])$$
第二个式子继续优化: \(g[i][1]\) 表示只考虑轻儿子取自己的最大权独立集,所以变成:
$$dp[i][1]=g[i][1]+dp[j][0]$$
进行区间维护
我们考虑像斐波那契一样,维护这个方程的矩阵:
现在我们要从一个点的重儿子 \(j\) 转移到 \(i\) 上,也就是说我们需要构造出一个转移矩阵使: \( \quad \begin{vmatrix} dp[j][0] & dp[j][1] \end{vmatrix} \quad \) 能转移到 \( \quad \begin{vmatrix} dp[i][0] & dp[i][1] \end{vmatrix} \quad \)
好像并不能进行矩阵乘法。 但是,我们可以重新定义:
《他改变了矩乘》: 我们定义一个新的运算符 \(*\) ,对于矩阵 \(A,B\) ,定义 \(A*B\) 的结果为 \(C\) ,满足:
实现到代码中,则有:
struct matrix{
int mat[2][2];
matrix(){memset(mat,-0X3F,sizeof(mat));}
inline matrix operator *(matrix a,matrix b){
matrix c;
for(int i=0;i<2;i++) for(int j=0;j<2;j++) for(int k=0;k<2;k++)
c.mat[i][j]=max(c.mat[i][j],a.mat[i][k]+b.mat[k][j]);
return c;
}
};
为什么这个东西具有结合律?
口胡理解是: \(\max\) 和 \(+\) 操作都是满足结合律的,所以这个也满足。
构造转移矩阵:
我们对转移方程进行变形:
\(dp[i][0]=max(dp[j][0]+g[i][0],dp[j][1]+g[i][0])\)
\(dp[i][1]=max(dp[j][0]+g[i][1],-inf)\)
接着把已知的状态和要转移的状态写在一起,未知矩阵由 \(X\) 代替:
原来是个 \(1*2\) 的矩阵,形成 \(1*2\) 的矩阵,那么 \(X\) 应该是一个 \(2*2\) 的矩阵。
我们设矩阵左上,右上,左下,右下四个位置分别为: \(x_1,x_2,x_3,x_4\) ,把每个位置对应上去:
\(dp[i][0]=\max(dp[j][0]+x_1,dp[j][1]+x_3)\) , 所以 \(x_1=g[i][0]\) , \(x_3=g[i][0]\)
\(dp[i][1]=\max(dp[j][0]+x_2,dp[j][1]+x_4)\) , 所以 \(x_2=g[i][1]\) , \(x_4=-inf\)
重链剖分剖出的 \(DFS\) 序,由于先访问了链头,所以这个区间中,链头在区间左端,链尾在区间右端。
我们存储的初始信息在叶子节点(也就是链尾)上,因此我们的 “矩阵乘法” 应当是转移矩阵 \((g)\) 在前,要维护的值矩阵 \((dp)\) 在后。
因此,则有:
总体流程:
我们对于一条重链,我们的叶子节点就存储了最初始的值,链上每个节点都对应着一个转移矩阵。
因为这个转移矩阵满足结合率,且和重链信息没关系,对于一条重链,我们可以之间线段树维护区间乘积。
然后到了一条重链链头,因为这个点是它父亲的轻儿子,我们需要更新它父亲节点所在的点的转移矩阵。这样子一直跳到根节点就可以了。
细节部分:
-
对于一个点,查找其 \(dp\) 值,需要从这个点一直查到区间的链尾。
因此,树剖时我们多维护一个 \(end[i]\) ( \(i\) 是一条重链的链头) ,表示以 \(i\) 为链头的这条链,链尾(叶子节点) 在 \(DFS\) 序上的位置。 -
更新线段树上某个点的转移矩阵时,传入的如果是矩阵,递归下去常数太大。
解决方法:在线段树外,维护一个矩阵组 \(val[i]\) , 表示每个点对应的转移矩阵,这样更新时,直接赋值进来即可。
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5,M=2e5+5,V=4e5+5,INF=0x7F7F7F7F;
struct matrix{
int mat[2][2];
matrix(){memset(mat,-0X3F,sizeof(mat));}
inline matrix operator * (matrix b){
matrix c;
for(int i=0;i<2;i++) for(int j=0;j<2;j++) for(int k=0;k<2;k++)
c.mat[i][j]=max(c.mat[i][j],mat[i][k]+b.mat[k][j]);
return c;
}
};
int n,m;
int head[N],nxt[M],ver[M],tot;
int A[N];
int fa[N],sizes[N],dep[N],dfn[N],top[N],id[N],son[N],End[N],cnt;
int dp[N][2];
matrix val[N];
struct SegTree{
int L[V],R[V]; matrix M[V];
void push_up(int x){M[x]=M[x<<1]*M[x<<1|1];}//更新就更新对应矩阵
void build(int l,int r,int x){
L[x]=l; R[x]=r;
if(l==r){
M[x]=val[dfn[L[x]]]; return;//赋值方式
}
int mid=l+r>>1;
build(l,mid,x<<1); build(mid+1,r,x<<1|1);
push_up(x);
}
void update(int pos,int x){
if(L[x]==R[x]){
M[x]=val[dfn[pos]];
return;
}
int mid=(L[x]+R[x])>>1;
if(pos<=mid) update(pos,x<<1);
else update(pos,x<<1|1);
push_up(x);
}
matrix query(int l,int r,int x){
if(L[x]==l&&R[x]==r) return M[x];
int mid=(L[x]+R[x])>>1;
if(r<=mid) return query(l,r,x<<1);
else if(l>mid) return query(l,r,x<<1|1);
else return query(l,mid,x<<1)*query(mid+1,r,x<<1|1);//dp即矩阵
}
}T;
void add(int x,int y){
ver[++tot]=y; nxt[tot]=head[x];head[x]=tot;
}
void dfs1(int x,int father){
sizes[x]=1;
dep[x]=dep[father]+1;
for(int i=head[x];i;i=nxt[i]){
int y=ver[i];
if(y==father) continue;
fa[y]=x;
dfs1(y,x);
sizes[x]+=sizes[y];
if(sizes[y]>sizes[son[x]]||!son[x]) son[x]=y;
}
}
void dfs2(int x,int topfather){
id[x]=++cnt; dfn[cnt]=x;
top[x]=topfather;
End[topfather]=max(End[topfather],cnt);//end数组记录链头对应链尾位置
//更新g数组,即更新val
dp[x][0]=0; dp[x][1]=A[x];
val[x].mat[0][0]=val[x].mat[0][1]=0;//记录每个点的矩阵
val[x].mat[1][0]=A[x];
if(son[x]!=0){
dfs2(son[x],topfather);
dp[x][0]+=max(dp[son[x]][0],dp[son[x]][1]);
dp[x][1]+=dp[son[x]][0];
}
for(int i=head[x];i;i=nxt[i]){
int y=ver[i];
if(y==fa[x]||y==son[x]) continue;
dfs2(y,y);
dp[x][0]+=max(dp[y][0],dp[y][1]);
dp[x][1]+=dp[y][0];
val[x].mat[0][0]+=max(dp[y][0],dp[y][1]);//子儿子可选可不选的最大独立集
val[x].mat[0][1]=val[x].mat[0][0];//同上
val[x].mat[1][0]+=dp[y][0];//子儿子不能选
}
}
void update_path(int x,int z){
val[x].mat[1][0]+=z-A[x];//因为这个地方还存的有子儿子不能选的值,因此不能直接赋值为 z,
A[x]=z;//更新对应点的值
matrix last,now;
while(x!=0){//不断进行更新
last=T.query(id[top[x]],End[top[x]],1);//
T.update(id[x],1);//找到需要更改的地方,进行更新
now=T.query(id[top[x]],End[top[x]],1);
x=fa[top[x]];//一直找重链,直到顶端
val[x].mat[0][0]+=max(now.mat[0][0],now.mat[1][0])-max(last.mat[0][0],last.mat[1][0]);
//之前可选可不选-现在可选可不选
val[x].mat[0][1]=val[x].mat[0][0];
val[x].mat[1][0]+=now.mat[0][0]-last.mat[0][0];
//之前不选-现在不选
}
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++) scanf("%d",&A[i]);
for(int i=1,x,y;i<n;i++){
scanf("%d%d",&x,&y); add(x,y); add(y,x);
}
dfs1(1,0);
dfs2(1,1);
T.build(1,n,1);
for(int i=1,x,y;i<=m;i++){
scanf("%d%d",&x,&y);
update_path(x,y);
matrix ans=T.query(id[1],End[1],1);
printf("%d\n",max(ans.mat[0][0],ans.mat[1][0]));
}
system("pause");
return 0;
}