前置知识

在学习如何使用全局平衡二叉树之前,你首先要知道如何使用树链剖分解决动态DP问题。这里仅做一个简单的回顾,建议在有一定基础的情况下看。

首先,维护序列的动态DP我们就不说了,这里只讨论树上的动态DP问题。

然后,目前个人感觉,动态DP往往有一些奇怪的特征。

一般问题是支持动态修改某一个点的权值,以及询问根节点的(也就是全局的)或者是某一个子树的DP值。

而通常是从静态的情况下入手,写出一个结构简单的DP转移式,然后将其中和轻儿子以及子树的根有关的项提出来,然后得到了当前的根和重儿子之间的转移式。有了这个之后,我们就在全局维护每一个子树的根的轻儿子的信息,然后,比如说树链剖分,就是将一条重链上的信息全部快速总和起来,就能够得到这个子树的答案了。

至于将信息合并这一步,还有一些细节。

首先,将有重儿子转移过来的DP式展开之后,要能够形成一个比较简单的形式。每一个子树的根由重儿子转移过来的形式必须是一样的。

然后,如果是线性的式子的话,往往是采用矩阵乘法来进行信息的总和(而往往又采用直接写转移的方式来减小常数);而如果是其它的情况的话,往往又是前缀和,后缀和,或者是所有区间的和、积之类的。

这里说的比较笼统,大家将就理解一下吧~

全局平衡二叉树

大致介绍

在对使用树链剖分解决动态DP问题比较熟悉之后,再来看这个东西就比较好理解了。

因为树链剖分是\(O(n \log^2 n)\)的,所以有可能被卡。而我们熟知的\(O(n \log n)\)的LCT又往往加上常数后比树剖还慢...那么有什么既是\(O(n \log n)\)的,常数又相对较小的方法呢?这个时候全局平衡二叉树就出现了。

它其实和树链剖分很像,都是对于一根重链要特殊处理。下面将详细介绍如何对于一颗给定的树求出这样子的一个全局平衡二叉树。

首先是一个大致的思路,就是对于一根重链而言,将它维护成一棵每一个节点代表一个区间的平衡二叉树(至于根据什么信息来使它平衡,后面再说),然后和树链剖分一样,将原图中一个点的所有轻儿子还是接到它自己这个点上。相当于整个图并不是一个严格的平衡二叉树,只有对于某一根重链而言,它才是一棵二叉树。

建图过程

下面正式开始讲构图的过程:

  1. 跑一遍DFS,预处理出每一个节点的重儿子、子树大小、轻子树大小(即\(1+\sum_{v\in lightson[u]}siz[v]\),记为\(lsiz[u]\));
  2. 从根节点(假定是1号节点)开始遍历,首先将以当前点为顶端的重链整个提出来,先下去处理这根链上所有节点的所有轻子树,然后根据之前所说的,将轻子树接到它们对应的父节点上去就可以了。(此过程中只需要记录\(treefa[]\))就可以了。在这个过程中,将轻儿子的信息统计到存在子树根的某个数据结构上就可以了(这里假定是矩阵,记为\(matr1[]\))
  3. 然后再来看如何处置当前的这根重链。把当前的这根重链看成是一个区间,即一个序列。然后在这个序列上一直做类似于点分治一样的算法,也就是不断的找重心。但是注意了,这里的重心是在\(lsiz[]的定义下的\)。因为是在序列上找重心,我们只需要从左到右枚举,找到一个类似于带权中点的东西就可以了;
  4. 在建立这棵二叉树的过程中,注意将一个点的左右儿子的信息PushUp上来,同时还需要注意上传信息时“计算的方向”(尤其是做矩阵乘法,因为它没有交换律)。然后这个就相当于是维护的一个区间的信息了,存在当前这个点的另一个数据结构里面(这里还是假定是矩阵,记为\(matr2[]\))。

到这里,整个全局平衡二叉树就建好了。再次强调,matr1[]存的是轻儿子以及自己的信息,而matr2[]存的是对于某一根重链上的区间的信息。

修改过程

加入我们当前要把第x个点的权值修改为v,那么我们来看是如何进行操作的:

  1. 首先将x这个点自己的信息修改了,但是只修改matr1[];
  2. 然后模仿树剖,一步一步往上跳,只不过这里是真的"一步一步往上跳"。假如当前节点为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;
}

希望能够对你有所帮助!

动态DP之全局平衡二叉树的更多相关文章

  1. 动态 DP 学习笔记

    不得不承认,去年提高组 D2T3 对动态 DP 起到了良好的普及效果. 动态 DP 主要用于解决一类问题.这类问题一般原本都是较为简单的树上 DP 问题,但是被套上了丧心病狂的修改点权的操作.举个例子 ...

  2. 动态dp初探

    动态dp初探 动态区间最大子段和问题 给出长度为\(n\)的序列和\(m\)次操作,每次修改一个元素的值或查询区间的最大字段和(SP1714 GSS3). 设\(f[i]\)为以下标\(i\)结尾的最 ...

  3. LG4719 【模板】动态dp 及 LG4751 动态dp【加强版】

    题意 题目描述 给定一棵\(n\)个点的树,点带点权. 有\(m\)次操作,每次操作给定\(x,y\),表示修改点\(x\)的权值为\(y\). 你需要在每次操作之后求出这棵树的最大权独立集的权值大小 ...

  4. 动态DP总结

    动态DP 何为动态DP? 将画风正常的DP加上修改操作. 举个例子? 给你一个长度为\(n\)的数列,从中选出一些数,要求选出的数互不相邻,最大化选出的数的和. 考虑DP,状态设计为\(f[i][1/ ...

  5. 【学习笔记】动态 dp 入门简易教程

    序列 dp 引入:最大子段和 给定一个数列 \(a_1, a_2, \cdots, a_n\)(可能为负),求 \(\max\limits_{1\le l\le r\le n}\left\{\sum_ ...

  6. 洛谷P4719 【模板】"动态 DP"&动态树分治

    [模板]"动态 DP"&动态树分治 第一道动态\(DP\)的题,只会用树剖来做,全局平衡二叉树什么的就以后再学吧 所谓动态\(DP\),就是在原本的\(DP\)求解的问题上 ...

  7. [UOJ#268]. 【清华集训2016】数据交互[动态dp+可删堆维护最长链]

    题意 给出 \(n\) 个点的树,每个时刻可能出现一条路径 \(A_i\) 或者之前出现的某条路径 \(A_i\) 消失,每条路径有一个权值,求出在每个时刻过后能够找到的权值最大的路径(指所有和该路径 ...

  8. 【知识总结】动态 DP

    勾起了我悲伤的回忆 -- NOIP2018 316pts -- 主要思想:将 DP 过程分解为方便单点修改和一个区间合并的操作(通常类似矩阵乘法),然后用数据结构(通常为线段树)维护. 例:给定一个长 ...

  9. Luogu P4643 【模板】动态dp

    题目链接 Luogu P4643 题解 猫锟在WC2018讲的黑科技--动态DP,就是一个画风正常的DP问题再加上一个动态修改操作,就像这道题一样.(这道题也是PPT中的例题) 动态DP的一个套路是把 ...

随机推荐

  1. IDEA2019激活码集合(非盈利)

    56ZS5PQ1RF-eyJsaWNlbnNlSWQiOiI1NlpTNVBRMVJGIiwibGljZW5zZWVOYW1lIjoi5q2j54mI5o6I5p2DIC4iLCJhc3NpZ25lZ ...

  2. mysql远程连接很慢问题解决

    mysql开启远程访问发现从远程连接每次都在5秒以上,从本机连接很快. 解决方案: [mysqld] 标签下添加一行配置 skip-name-resolve 重启mysqld服务, 问题解决!

  3. Coursera, Big Data 4, Machine Learning With Big Data (week 1/2)

    Week 1 Machine Learning with Big Data KNime - GUI based Spark MLlib - inside Spark CRISP-DM Week 2, ...

  4. appniu踩坑

    1.pyCharm识别不到appnium-python-client 解决:新建项目注意选择环境,查看Project Interpreter中是否识别到了appnium-python-client 还 ...

  5. Unity AssetBundle的生成、加载和热更新

    当前使用的是unity2018.2.6版本. 生成AssetBundle 这个版本生成AssetBundle有两种方式,一种是在资源的Inspector面板下边配置AssetBundle名称,然后调用 ...

  6. java学习笔记--从c/c++到java转变

    final修饰符1)final变量final表示“最后的,最终的”含义,变量一旦赋值后,不能被重新赋值.被final修饰的实例变量必须显示指定初始值.final修饰符通常和static修饰符一起来创建 ...

  7. Deep face recognition: a survey v4

    http://www.cnblogs.com/shouhuxianjian/p/9789243.html

  8. C# 高级编程05----常用修饰符

    常用修饰符: 1.访问可见性修饰符 修饰符 应用于 说明 public 类型或成员 任何代码都可访问 protected 类型或内嵌类型的成员 只有子类能访问 internal 类型或成员 只能在包含 ...

  9. oracle Data Modeler 使用教程

    由于 powerdesigner 的版权问题.公司要求集体换成 oracle Data Modeler .免费版就够用,哈哈.这有很详细的入门教程,看一看吧: 官方正版教程 ,特详细,只是英文的,也只 ...

  10. centos搭建git服务

    一.服务器yum -y install git git init --bare test.gitcd test.gitpwd //打印当前目录,假设是:/home/root/git/test.gitg ...