动态DP之全局平衡二叉树
前置知识
在学习如何使用全局平衡二叉树之前,你首先要知道如何使用树链剖分解决动态DP问题。这里仅做一个简单的回顾,建议在有一定基础的情况下看。
首先,维护序列的动态DP我们就不说了,这里只讨论树上的动态DP问题。
然后,目前个人感觉,动态DP往往有一些奇怪的特征。
一般问题是支持动态修改某一个点的权值,以及询问根节点的(也就是全局的)或者是某一个子树的DP值。
而通常是从静态的情况下入手,写出一个结构简单的DP转移式,然后将其中和轻儿子以及子树的根有关的项提出来,然后得到了当前的根和重儿子之间的转移式。有了这个之后,我们就在全局维护每一个子树的根的轻儿子的信息,然后,比如说树链剖分,就是将一条重链上的信息全部快速总和起来,就能够得到这个子树的答案了。
至于将信息合并这一步,还有一些细节。
首先,将有重儿子转移过来的DP式展开之后,要能够形成一个比较简单的形式。每一个子树的根由重儿子转移过来的形式必须是一样的。
然后,如果是线性的式子的话,往往是采用矩阵乘法来进行信息的总和(而往往又采用直接写转移的方式来减小常数);而如果是其它的情况的话,往往又是前缀和,后缀和,或者是所有区间的和、积之类的。
这里说的比较笼统,大家将就理解一下吧~
全局平衡二叉树
大致介绍
在对使用树链剖分解决动态DP问题比较熟悉之后,再来看这个东西就比较好理解了。
因为树链剖分是\(O(n \log^2 n)\)的,所以有可能被卡。而我们熟知的\(O(n \log n)\)的LCT又往往加上常数后比树剖还慢...那么有什么既是\(O(n \log n)\)的,常数又相对较小的方法呢?这个时候全局平衡二叉树就出现了。
它其实和树链剖分很像,都是对于一根重链要特殊处理。下面将详细介绍如何对于一颗给定的树求出这样子的一个全局平衡二叉树。
首先是一个大致的思路,就是对于一根重链而言,将它维护成一棵每一个节点代表一个区间的平衡二叉树(至于根据什么信息来使它平衡,后面再说),然后和树链剖分一样,将原图中一个点的所有轻儿子还是接到它自己这个点上。相当于整个图并不是一个严格的平衡二叉树,只有对于某一根重链而言,它才是一棵二叉树。
建图过程
下面正式开始讲构图的过程:
- 跑一遍DFS,预处理出每一个节点的重儿子、子树大小、轻子树大小(即\(1+\sum_{v\in lightson[u]}siz[v]\),记为\(lsiz[u]\));
- 从根节点(假定是1号节点)开始遍历,首先将以当前点为顶端的重链整个提出来,先下去处理这根链上所有节点的所有轻子树,然后根据之前所说的,将轻子树接到它们对应的父节点上去就可以了。(此过程中只需要记录\(treefa[]\))就可以了。在这个过程中,将轻儿子的信息统计到存在子树根的某个数据结构上就可以了(这里假定是矩阵,记为\(matr1[]\))
- 然后再来看如何处置当前的这根重链。把当前的这根重链看成是一个区间,即一个序列。然后在这个序列上一直做类似于点分治一样的算法,也就是不断的找重心。但是注意了,这里的重心是在\(lsiz[]的定义下的\)。因为是在序列上找重心,我们只需要从左到右枚举,找到一个类似于带权中点的东西就可以了;
- 在建立这棵二叉树的过程中,注意将一个点的左右儿子的信息PushUp上来,同时还需要注意上传信息时“计算的方向”(尤其是做矩阵乘法,因为它没有交换律)。然后这个就相当于是维护的一个区间的信息了,存在当前这个点的另一个数据结构里面(这里还是假定是矩阵,记为\(matr2[]\))。
到这里,整个全局平衡二叉树就建好了。再次强调,matr1[]存的是轻儿子以及自己的信息,而matr2[]存的是对于某一根重链上的区间的信息。
修改过程
加入我们当前要把第x个点的权值修改为v,那么我们来看是如何进行操作的:
- 首先将x这个点自己的信息修改了,但是只修改matr1[];
- 然后模仿树剖,一步一步往上跳,只不过这里是真的"一步一步往上跳"。假如当前节点为p,假如treefa[p]到p这条边是轻边的话,在修改对当前点做PushUp(这是用来合并区间信息的)之前,先把当前点对于treefa[p]原来的贡献先去掉(这里往往是根据矩阵的构造方式直接进行修修改,而不要想着什么矩阵除法之类的...),然后对当前的点做PushUp,然后在将新的贡献假如到treefa[p]中;否则的话,就直接对于当前的点做PushUp就可以了。
然后这里就做完了修改操作。
询问过程
询问这里分为两种,一种是询问全局的,也就是整棵树的根的DP信息,那么这个时候就直接将根(1号节点)所在重链的二叉树的根所维护的区间信息直接拿出来用就好了。
而第二种情况,也就是询问某一个子树的DP信息的时候,就稍微麻烦一点。
大致的思想还是,模仿树剖,在这个点所在的重链序列上,将它及它下面的链上的点信息合并上来即可。
画个图:
图中红色的部分就是需要统计的信息。观察之后,可以发现,只有当x!=ch[treefa[x]][1]时,treefa[x]以及ch[treefa[x]][1]的信息才需要被统计。
这个可以根据平衡树的性质自行推倒的。
时间复杂度的证明
分成两部分进行考虑:
首先是轻边,根据重儿子的定义,很显然,向下走一层,子树的大小至少会减少一半;
然后是重边,由于我们是找的重心,那么lsiz[]也至少会减少一半。
根据以上伪证,我们可以发现这个东西是大致\(O(n \log n)\)的...
而实测起来虽然每道题是要比树剖快一点,但是大多数情况下都差不多...但至少能够保证绝对不会比树剖慢...
有人说代码复杂度差不多,但是我觉得,以我菜鸡的实现能力来看,代码和树剖的代码长度差不多一样的...
板题
既然树剖解决动态DP的板题是洛谷 P4719 【模板】动态dp,那么我们全局平衡二叉树的板题就是洛谷 P4751 动态dp【加强版】啦~
下面是这道题的代码,以及一些批注。至于矩阵长啥样,可以参考一下其他博主的树剖的矩阵,长得一模一样...,就懒得推了...
#include<cstdio>
#include<cstring>
#include<algorithm>
#define MAXN 1000000
#define MAXM 3000000
#define INF 0x3FFFFFFF
using namespace std;
struct edge
{
int to;
edge *nxt;
}edges[MAXN*2+5];
edge *ncnt=&edges[0],*Adj[MAXN+5];
int n,m;
struct Matrix
{
int M[2][2];
Matrix operator * (const Matrix &B)
{
static Matrix ret;
for(int i=0;i<2;i++)
for(int j=0;j<2;j++)
{
ret.M[i][j]=-INF;
for(int k=0;k<2;k++)
ret.M[i][j]=max(ret.M[i][j],M[i][k]+B.M[k][j]);
}
return ret;
}
}matr1[MAXN+5],matr2[MAXN+5];//每个点维护两个矩阵
int root;
int w[MAXN+5],dep[MAXN+5],son[MAXN+5],siz[MAXN+5],lsiz[MAXN+5];
int g[MAXN+5][2],f[MAXN+5][2],trfa[MAXN+5],bstch[MAXN+5][2];
int stk[MAXN+5],tp;
bool vis[MAXN+5];
void AddEdge(int u,int v)
{
edge *p=++ncnt;
p->to=v;p->nxt=Adj[u];Adj[u]=p;
edge *q=++ncnt;
q->to=u;q->nxt=Adj[v];Adj[v]=q;
}
void DFS(int u,int fa)
{
siz[u]=1;
for(edge *p=Adj[u];p!=NULL;p=p->nxt)
{
int v=p->to;
if(v==fa)
continue;
dep[v]=dep[u]+1;
DFS(v,u);
siz[u]+=siz[v];
if(!son[u]||siz[son[u]]<siz[v])
son[u]=v;
}
lsiz[u]=siz[u]-siz[son[u]];//轻儿子的siz和+1
}
void DFS2(int u,int fa)
{
f[u][1]=w[u],f[u][0]=0;
g[u][1]=w[u],g[u][0]=0;
if(son[u])
{
DFS2(son[u],u);
f[u][0]+=max(f[son[u]][0],f[son[u]][1]);
f[u][1]+=f[son[u]][0];
}
for(edge *p=Adj[u];p!=NULL;p=p->nxt)
{
int v=p->to;
if(v==fa||v==son[u])
continue;
DFS2(v,u);
f[u][0]+=max(f[v][0],f[v][1]);//f[][]就是正常的DP数组
f[u][1]+=f[v][0];
g[u][0]+=max(f[v][0],f[v][1]);//g[][]数组只统计了自己和轻儿子的信息
g[u][1]+=f[v][0];
}
}
void PushUp(int u)
{
matr2[u]=matr1[u];//matr1是单点加上轻儿子的信息,matr2是区间信息
if(bstch[u][0])
matr2[u]=matr2[bstch[u][0]]*matr2[u];
//注意转移的方向,但是如果我们的矩乘定义不同,可能方向也会不同
if(bstch[u][1])
matr2[u]=matr2[u]*matr2[bstch[u][1]];
}
int getmx2(int u)
{
return max(matr2[u].M[0][0],matr2[u].M[0][1]);
}
int getmx1(int u)
{
return max(getmx2(u),matr2[u].M[1][0]);
}
int SBuild(int l,int r)
{
if(l>r)
return 0;
int tot=0;
for(int i=l;i<=r;i++)
tot+=lsiz[stk[i]];
for(int i=l,sumn=lsiz[stk[l]];i<=r;i++,sumn+=lsiz[stk[i]])
if(sumn*2>=tot)//是重心了
{
int lch=SBuild(l,i-1),rch=SBuild(i+1,r);
bstch[stk[i]][0]=lch;bstch[stk[i]][1]=rch;
trfa[lch]=trfa[rch]=stk[i];
PushUp(stk[i]);//将区间的信息统计上来
return stk[i];
}
return 0;
}
int Build(int u)
{
for(int pos=u;pos;pos=son[pos])
vis[pos]=true;
for(int pos=u;pos;pos=son[pos])
for(edge *p=Adj[pos];p!=NULL;p=p->nxt)
if(!vis[p->to])//是轻儿子
{
int v=p->to,ret=Build(v);
trfa[ret]=pos;//轻儿子的treefa[]接上来
}
tp=0;
for(int pos=u;pos;pos=son[pos])
stk[++tp]=pos;//把重链取出来
int ret=SBuild(1,tp);//对重链进行单独的SBuild(我猜是Special Build?)
return ret;//返回当前重链的二叉树的根
}
void Modify(int u,int val)
{
matr1[u].M[1][0]+=val-w[u];
w[u]=val;
for(int pos=u;pos;pos=trfa[pos])
if(trfa[pos]&&bstch[trfa[pos]][0]!=pos&&bstch[trfa[pos]][1]!=pos)
{
matr1[trfa[pos]].M[0][0]-=getmx1(pos);
matr1[trfa[pos]].M[0][1]=matr1[trfa[pos]].M[0][0];
matr1[trfa[pos]].M[1][0]-=getmx2(pos);
PushUp(pos);
matr1[trfa[pos]].M[0][0]+=getmx1(pos);
matr1[trfa[pos]].M[0][1]=matr1[trfa[pos]].M[0][0];
matr1[trfa[pos]].M[1][0]+=getmx2(pos);
}
else
PushUp(pos);
}
inline int read()
{
int ret=0,f=1;char c=0;
while(c<'0'||c>'9'){c=getchar();if(c=='-')f=-f;}
ret=10*ret+c-'0';
while(true){c=getchar();if(c<'0'||c>'9')break;ret=10*ret+c-'0';}
return ret*f;
}
inline void print(int x)
{
if(x==0) return;
print(x/10);putchar(x%10+'0');
}
int main()
{
scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++)
w[i]=read();
int u,v;
for(int i=1;i<n;i++)
{
u=read(),v=read();
AddEdge(u,v);
}
DFS(1,-1);
//求重儿子
DFS2(1,-1);
//求初始的DP值,也可以在Build()里面求,但是这样写就和树剖的写法统一了
for(int i=1;i<=n;i++)
{
matr1[i].M[0][0]=matr1[i].M[0][1]=g[i][0];
matr1[i].M[1][0]=g[i][1],matr1[i].M[1][1]=-INF; //初始化矩阵
}
root=Build(1);//root即为根节点所在重链的重心
int lastans=0;
for(int i=1;i<=m;i++)
{
u=read(),v=read();
u^=lastans;//强制在线
Modify(u,v);
lastans=getmx1(root);//直接取值
if(lastans==0) putchar('0');
else print(lastans);
putchar('\n');
}
return 0;
}
希望能够对你有所帮助!