动态DP [DDP]
什么是动态树分治我也不知道
有史以来第一次认真写个算法
写在前面:
注意代码细节!注意代码细节!注意代码细节!
(因为多次很小很小的错误导致查了1~2个小时才看到的蒟蒻枯死)
0. 前置知识:
线段树、树链剖分、(树形DP?)
1. 问题简化:
先考虑一下没有修改时怎么操作
题目求的是最大权独立集
说人话:就是如果选择一个点,与其连边的点都不可以选
其实这不就是没有上司的舞会吗
朴素想法,就是设\(f_{i,0/1}\),i表示当前点,0/1表示选或不选
设j为i的儿子,则转移为
每次修改都进行一次DP
2. 考虑优化:
我们不难发现,其实对一个点的修改,影响的只是一条链
如果只对所在的链进行修改,那是不错的选择
可惜,万恶的出题人一定会用一条退化成链的树来把你其实根本没有优化的优秀DP卡成O(\(n^2\))
那有没有什么优秀的优化将每次的操作压缩成O(logn)或O(\(log^2\)n)?
3. 切入正题:
of course!
学过树链剖分的优秀OIer们都知道,树剖可以将树划分成不超过logn条重链
我们能不能每次对链进行整体操作??
我们换个思想先,重新设状态
设\(g_{i,0}\)表示不选自己,轻儿子随意的最大权独立集,
设\(g_{i,1}\)表示要选自己,轻儿子都不选的最大权独立集(也就是说已经加了\(a_i\)了)
(那重儿子怎么办)
别着急,看转移:
我们定义j为i的重儿子
那么现在的转移便是:
接下来终于可以请出本场的重头戏:DDP
动态DP就是对转移连续(或是一段区间)的带修DP,利用矩阵加速优化的DP
(纯属个人理解,有不恰当之处请大佬指出)
但似乎我们刚刚的转移方程并不连续啊?
那我们考虑重新标号,因为树链剖分后,dfs是先进入当前节点的重儿子,所以
在同一条重链上的节点dfs序是连续的
这样就十分好办了
但仔细的小伙伴又发现,矩阵乘法适用于形如 \(C_{i,j}= \sum (a_{i,k}*b_{k,j})\)
似乎转移方程上出现了max?(你问题咋这么多)
那如果我们将“+”降为“max”,“*”降为“+”的矩阵是否满足结合律?
(其实是可以证明的,但本蒟蒻真的很不擅长数论,需要证明的自己上网找吧)
好,既然假设它已经成立了,那我们就将状态化为矩阵的形式:
验算一下,他是符合我们的转移公式的!
4. 进行操作:
先说如何得出DP值
从我们上面的定义,不难看出,我们要的答案其实就是:根节点1所在重链所有矩阵的“乘*积”,取max(\(g_{ans,0}\),\(g_{ans,1}\))
既然重链的序号是连续的,那么我们就考虑用线段树来维护这些矩阵,求答案时就求:根节点1所在重链对应的连续编号在线段树上的矩阵积
解决了查询,再来解决修改:
不难发现,由于我们将重轻儿子分离了,实际上我们要修改的只是当前点所在重链的链头top的父亲fa(也就是top一定是fa的轻儿子)的g值(矩阵),然后在传递到fa所在的重链进行同样修改
然后就 完结撒花!
5. 总结关键点:
对点进行树链剖分;然后将轻重儿子分开处理;每条重链看成一个整体,对应一个大矩阵;线段树存储矩阵及其乘积
6. AC代码
#include<iostream>
#include<fstream>
#include<algorithm>
#include<cmath>
#include<cstdlib>
#include<cstring>
#include<queue>
#include<map>
#include<set>
#include<bitset>
using namespace std;
inline int reads()
{
int sign=1,re=0; char c=getchar();
while(c<'0'||c>'9'){if(c=='-') sign=-1; c=getchar();}
while('0'<=c&&c<='9'){re=re*10+(c-'0'); c=getchar();}
return sign*re;
}
const int INF=1e9;
struct Martin
{
int a[2][2];
Martin(){a[0][0]=a[0][1]=a[1][0]=a[1][1]=-INF;}
Martin operator * (const Martin b) const
{
Martin re;
for(int k=0;k<2;k++) for(int i=0;i<2;i++) for(int j=0;j<2;j++) re.a[i][j]=max(re.a[i][j],a[i][k]+b.a[k][j]);
return re;
}
}g[100005],tr[400005];
int n,m,v[100005];
struct Road
{
int to,next;
}r[200005]; int he[100005];
inline void Edge_add(int u,int v)
{
static int cnt=0; cnt++;
r[cnt].to=v; r[cnt].next=he[u]; he[u]=cnt;
}
int Size[100005],fa[100005],hs[100005],dfn[100005],tn[100005],Top[100005],End[100005];
int dp[100005][2];
void dfs1(int now)
{
Size[now]=1;
for(int i=he[now];i;i=r[i].next)
{
int to=r[i].to;
if(to==fa[now]) continue;
fa[to]=now;
dfs1(to); Size[now]+=Size[to];
if(Size[hs[now]]<Size[to]) hs[now]=to;
}
}
void dfs2(int now,int st)
{
dfn[now]=++dfn[0]; tn[dfn[0]]=now;
Top[now]=st; End[st]=max(End[st],dfn[0]);
g[now].a[0][0]=g[now].a[0][1]=0;
g[now].a[1][0]=dp[now][1]=v[now];
if(!hs[now]) return;
dfs2(hs[now],st);
dp[now][0]+=max(dp[hs[now]][0],dp[hs[now]][1]);
dp[now][1]+=dp[hs[now]][0];
for(int i=he[now];i;i=r[i].next)
{
int to=r[i].to;
if(to==fa[now]||to==hs[now]) continue;
dfs2(to,to);
dp[now][0]+=max(dp[to][0],dp[to][1]);
dp[now][1]+=dp[to][0];
g[now].a[0][0]+=max(dp[to][0],dp[to][1]); g[now].a[0][1]=g[now].a[0][0];
g[now].a[1][0]+=dp[to][0];
}
}
void buildtree(int now,int l,int r)
{
if(l==r)
{
tr[now]=g[tn[l]];
return;
}
int mid=(l+r)>>1;
buildtree(now<<1,l,mid); buildtree((now<<1)|1,mid+1,r);
tr[now]=tr[now<<1]*tr[(now<<1)|1];
}
void modify(int now,int l,int r,int to)
{
if(l==r)
{
tr[now]=g[tn[l]];
return;
}
int mid=(l+r)>>1;
if(to<=mid) modify(now<<1,l,mid,to);
else modify((now<<1)|1,mid+1,r,to);
tr[now]=tr[now<<1]*tr[(now<<1)|1];
}
Martin query(int now,int l,int r,int L,int R)
{
if(L<=l&&r<=R) return tr[now];
int mid=(l+r)>>1;
bool lc=false,rc=false;
Martin la,ra;
if(L<=mid) lc=true,la=query(now<<1,l,mid,L,R);
if(mid<R) rc=true,ra=query((now<<1)|1,mid+1,r,L,R);
if(lc&&rc) return la*ra;
return lc?la:ra;
}
inline void update(int x,int y)
{
g[x].a[1][0]+=y-v[x]; v[x]=y;
Martin bef,aft; int _fa;
while(true)
{
bef=query(1,1,n,dfn[Top[x]],End[Top[x]]); //修改前的重链矩阵
modify(1,1,n,dfn[x]);
aft=query(1,1,n,dfn[Top[x]],End[Top[x]]); //修改后的重链矩阵
_fa=fa[Top[x]];
if(!_fa) break;
g[_fa].a[0][0]-=max(bef.a[0][0],bef.a[1][0]); g[_fa].a[0][0]+=max(aft.a[0][0],aft.a[1][0]); //通过转移方程可以看出,g[i][0]加的是轻儿子g[j][0]与g[j][1]的最大值
g[_fa].a[0][1]=g[_fa].a[0][0];
g[_fa].a[1][0]+=aft.a[0][0]-bef.a[0][0];
x=_fa;
}
}
Martin ans;
int main()
{
n=reads(); m=reads();
for(int i=1;i<=n;i++) v[i]=reads();
for(int i=1;i<n;i++)
{
int u=reads(),v=reads();
Edge_add(u,v); Edge_add(v,u);
}
dfs1(1); dfs2(1,1);
buildtree(1,1,n);
for(int i=1;i<=m;i++)
{
int x=reads(),y=reads();
update(x,y);
ans=query(1,1,n,dfn[1],End[1]);
printf("%d\n",max(ans.a[0][0],ans.a[1][0]));
}
return 0;
}
写在后面:
写了差不多一个小时的题解,感觉非常舒服,本来对此题只是浑浑噩噩的,现在基本上全明白了,而且对DP和树链剖分的理解又深入了不少
希望以后有时间补补之前一些算法的题解&&分析吧
乘风扶摇直上,归来不忘初心!