动态点分治学习笔记

学习动态点分治之前要先弄清楚点分治的原理,二者的应用范围的不同就在于动态的支持在线修改操作,而实现的不同就在于动态点分治要建点分树。

OI中有很多树上统计问题,这类问题往往都有一个比较容易实现的暴力做法,而用高级数据结构维护信息有显得过于复杂,有没有一种“优美的暴力”,能既保证思维的简单性,又有更高效的时间复杂度保证呢?这就是点分治的思想。

点分治的实现过程是:每次找到当前树的重心,然后以这个重心为根统计这个树的信息,然后对重心的每个孩子分别递归,同样用将重心作为根的方法统计子树的信息(这里可能要除去不合法的重复影响)。为什么复杂度有保证呢?我们先来看重心的性质。

以一棵树的重心作为根,则根的最大子树的size不超过整个树size的一半。考虑证明,因为重心的定义是使以它作为根的树的最大子树size最小,那么如果重心某一个子树size超过整个树的一半,则一定能找到另一个节点使这个节点作为根树的最大子树size比重心更小,矛盾。

那么我们可以得到,每个重心都有自己的管辖范围(事实上因为是分治,所以所有点都是某一块(或仅仅是它自己)的重心),而管辖一个点的重心最多有$O(\log n)$个。所以如果每个点被管辖自己的重心处理一次,那么总次数是$O(n \log n)$个的。

基于这个思想,我们得到了点分治的实现方法。

关于如何找重心,直接DP即可,注意传入S为这棵树的size,以及f[rt=0]=inf。

下面是一道裸题:POJ1741

 1 #include<cstdio>
 2 #include<cstring>
 3 #include<algorithm>
 4 #define rep(i,l,r) for (int i=l; i<=r; i++)
 5 #define For(i,x) for (int i=h[x],k; i; i=nxt[i])
 6 typedef long long ll;
 7 using namespace std;
 8
 9 const int N=20100,inf=1000000000;
10 int ans,n,cnt,tot,S,k,u,v,w,rt;
11 int sz[N],vis[N],d[N],val[N],h[N],nxt[N],to[N],a[N],f[N];
12
13 void add(int u,int v,int w)
14 { to[++cnt]=v; val[cnt]=w; nxt[cnt]=h[u]; h[u]=cnt; }
15
16 void find(int x,int fa){
17     sz[x]=1; f[x]=0;
18     For(i,x) if ((k=to[i])!=fa && !vis[k]){
19         find(k,x); sz[x]+=sz[k]; f[x]=max(f[x],sz[k]);
20     }
21     f[x]=max(f[x],S-sz[x]);
22     if (f[x]<f[rt]) rt=x;
23 }
24
25 void deep(int x,int fa){
26     a[++tot]=d[x];
27     For(i,x) if ((k=to[i])!=fa && !vis[k]) d[k]=d[x]+val[i],deep(k,x);
28 }
29
30 int cal(int x,int v){
31     d[x]=v; tot=0; deep(x,0);
32     sort(a+1,a+tot+1);
33     int l=1,r=tot,sum=0;
34     while (l<r)
35         if (a[l]+a[r]>k) r--; else sum+=r-l,l++;
36     return sum;
37 }
38
39 void solve(int x){
40     ans+=cal(x,0); vis[x]=1;
41     For(i,x) if (!vis[k=to[i]])
42         ans-=cal(k,val[i]),S=sz[k],f[rt=0]=inf,find(k,x),solve(rt);
43 }
44
45 int main(){
46     while (scanf("%d%d",&n,&k),n+k){
47         ans=cnt=0; memset(vis,0,sizeof(vis)); memset(h,0,sizeof(h));
48         rep(i,1,n-1) scanf("%d%d%d",&u,&v,&w),add(u,v,w),add(v,u,w);
49         S=n; f[rt=0]=inf; solve(1); printf("%d\n",ans);
50     }
51     return 0;
52 }

接着我们来看动态点分治。首先介绍点分树的概念,对于一个重心,将它与所有子树的重心连边(也就是按照分治的根的顺序连边),就得到了点分树。我们可以发现,每个重心记录的是自己管辖范围的所有点的信息,实际上也就是点分树上以这个重心为根的子树的信息。而如果修改某个点的值,它影响到的也就是点分树上这个点到根的路径上的所有点的信息。

根据上面对于点分治复杂度的分析可知,点分树的层数是$O(\log n)$层的。这就保证了修改的复杂度。

“树上的动态点分治相当于序列上的线段树"

我们再看一道模板题:BZOJ1095

这题如果不带修改就是简单的点分治或者直接DP的题目,现在带了修改,显然就是需要建立点分树。

建立点分树有一个需要注意的地方,一定要分清点在点分树上的父亲和在原树上的父亲。有的时候我们需要把点分树给建出来,有时则不需要。

还有动态点分治经常要用到两点间LCA,这个不要用树剖或者倍增LCA,因为单次询问是$O(\log n)$的。求出dfs序的深度序列,然后用RMQ求区间最小值就可以使单次询问复杂度降到$O(1)$。

还有,一般动态点分治都会与数据结构(STL)结合,一般每个节点用两个数据结构需要记录两个信息:以它为根的子树对它的父亲的影响,和所有以它的儿子为根的子树对它的影响,显然后者可以直接使用前者的信息(这里的父亲儿子都是指在点分树上)。

具体到这一题上,我们将找重心需要的所有参数全部传到find函数内部去而不是作为全局变量以免递归时出现冲突。

另外,下面这份代码在BZOJ上TLE了,因为multiset的常数过大。较为高效的实现是使用priority_queue,通过建立一个“垃圾堆”实现删除功能(具体实现见hzwer博客)

 1 #include<set>
 2 #include<cstdio>
 3 #include<algorithm>
 4 #pragma GCC optimize(3)
 5 #define rep(i,l,r) for (register int i=l; i<=r; i++)
 6 #define For(i,x) for (register int i=h[x],k; i; i=nxt[i])
 7 using namespace std;
 8
 9 const int N=200100,inf=1000000000;
10 char s[10];
11 int n,m,u,v,x,cnt,tot,pos[N],mv[N],lg[N<<1],a[N<<2],b[N],to[N<<1],nxt[N<<1],fa[N],h[N],sz[N],f[N],d[N],st[N][20],vis[N];
12 multiset<int>A[N],B[N],C;
13 multiset<int>::iterator it;
14
15 void add(int u,int v){ to[++cnt]=v; nxt[cnt]=h[u]; h[u]=cnt; }
16 void ins(multiset<int>a){ if (a.size()>=2) it=--a.end(),C.insert(*it+*(--it)); }
17 void del(multiset<int>a){ if (a.size()>=2) it=--a.end(),C.erase(C.find(*it+*(--it))); }
18
19 void find(int x,int fa,int S,int &rt){
20     sz[x]=1; f[x]=0;
21     For(i,x) if ((k=to[i])!=fa && !vis[k])
22         find(k,x,S,rt),sz[x]+=sz[k],f[x]=max(f[x],sz[k]);
23     f[x]=max(f[x],S-sz[x]);
24     if (f[x]<=f[rt]) rt=x;
25 }
26
27 void dfs(int x,int fa){
28     pos[x]=++tot; a[tot]=d[x];
29     For(i,x) if (fa!=(k=to[i])) d[k]=d[x]+1,dfs(k,x),a[++tot]=d[x];
30 }
31
32 void getst(){
33     rep(i,1,tot) st[i][0]=a[i];
34     for (int j=1; j<=18; j++)
35         rep(i,1,tot) st[i][j]=min(st[i][j-1],st[i+(1<<(j-1))][j-1]);
36 }
37
38 int que(int l,int r){
39     int t=lg[r-l+1];
40     return min(st[l][t],st[r-(1<<t)+1][t]);
41 }
42
43 int dis(int x,int y){ int a=pos[x],b=pos[y]; if (a>b) swap(a,b); return d[x]+d[y]-2*que(a,b); }
44
45 void get(int x,int fa,int dep,multiset<int>&s){
46     s.insert(dep);
47     For(i,x) if (!vis[k=to[i]] && k!=fa) get(k,x,dep+1,s);
48 }
49
50 int work(int x){
51     int rt=0; f[0]=inf; find(x,0,sz[x],rt); vis[rt]=1;
52     B[rt].insert(0);
53     For(i,rt) if (!vis[k=to[i]]){
54         multiset<int> s; get(k,0,1,s);
55         int p=work(k); fa[p]=rt; A[p]=s;
56         B[rt].insert(*(--A[p].end()));
57     }
58     ins(B[rt]); return rt;
59 }
60
61 void mdf(int x,bool f){
62     del(B[x]); if (f) B[x].erase(B[x].find(0)); else B[x].insert(0); ins(B[x]);
63     for (int i=x; fa[i]; i=fa[i]){
64         int y=fa[i]; del(B[y]);
65         if (A[i].size()) B[y].erase(B[y].find(*(--A[i].end())));
66         if (f) A[i].erase(A[i].find(dis(y,x))); else A[i].insert(dis(y,x));
67         if (A[i].size()) B[y].insert(*(--A[i].end()));
68         ins(B[y]);
69     }
70 }
71
72 int main(){
73     freopen("bzoj1095.in","r",stdin);
74     freopen("bzoj1095.out","w",stdout);
75     scanf("%d",&n);
76     rep(i,1,n-1) scanf("%d%d",&u,&v),add(u,v),add(v,u);
77     lg[1]=0; rep(i,2,N+100) lg[i]=lg[i>>1]+1;
78     dfs(1,0); getst(); tot=n; work(1); scanf("%d",&m);
79     rep(i,1,m){
80         scanf("%s",s);
81         if (s[0]==‘G‘) if (tot<=1) printf("%d\n",tot-1); else printf("%d\n",*(--C.end()));
82             else scanf("%d",&x),tot+=(b[x])?1:-1,b[x]^=1,mdf(x,b[x]);
83     }
84     return 0;
85 }

原文地址:https://www.cnblogs.com/HocRiser/p/8505627.html

时间: 2024-11-11 09:39:15

动态点分治学习笔记的相关文章

bzoj1095: [ZJOI2007]Hide 捉迷藏 动态点分治学习

好迷啊...感觉动态点分治就是个玄学,蜜汁把树的深度缩到logn (静态)点分治大概是递归的时候分类讨论: 1.答案经过当前点,暴力(雾)算 2.答案不经过当前点,继续递归 由于原树可以长的奇形怪状(菊花啊..链啊..扫把啊..)这就导致各种方法都会被卡 于是通过每次找重心保证最大深度 动态怎么解决呢? 不妨考虑线段树是二分的固态版本(只可意会),那么我们把每次找到的重心固定下来长成一棵树就可以把点分治凝固(不可言传) 原来点分治该维护什么现在就维护什么... (事实上我并没有写过静态点分治..

动态内存分配 学习笔记

#include<stdio.h> #include<stdlib.h> char *substr(const char *s, int n1, int n2) { char *p = (char *) malloc(n2-n1+2); int i,j=0; for(i=n1;i<=n2;i++,j++) p[j] = s[i]; p[j] = '\0'; return p; } void main(void) { char s[80], *sub; int n1, n2;

动态内存分配 学习笔记2

#include<stdio.h> #include<stdlib.h> void main() { struct stu_type{ char num[15]; char name[10]; int age; int c; int math; int en; int sum; float ave; } *p,*p1; int n,i; printf("请输入学生人数:"); scanf("%d",&n) ; p=(struct st

[ZJOI2007]捉迷藏 解题报告 (动态点分治)

[ZJOI2007]捉迷藏 近期做过的码量最大的一题 (当然也是我写丑了....) 题意 有一个 \(n\) 个节点的树 (\(n \le 10^5\)), 每个节点为黑色或白色. 有 \(m\) 个操作 (\(m \le 5 \times 10^5\)), 操作有两种, 将点 \(x\) 的的颜色翻转. 查询树上距离最远的黑色点对之间的距离. 思路 首先, 如果没有修改操作的话, 就是一个裸的点分治 (点分治学习笔记). 有修改操作, 那就 动态点分治. 动态点分治的基本思路是 (个人总结的)

java学习笔记14--多线程编程基础1

本文地址:http://www.cnblogs.com/archimedes/p/java-study-note14.html,转载请注明源地址. 多线程编程基础 多进程 一个独立程序的每一次运行称为一个进程,例如:用字处理软件编辑文稿时,同时打开mp3播放程序听音乐,这两个独立的程序在同时运行,称为两个进程 进程要占用相当一部分处理器时间和内存资源 进程具有独立的内存空间 通信很不方便,编程模型比较复杂 多线程 一个程序中多段代码同时并发执行,称为多线程,线程比进程开销小,协作和数据交换容易

java学习笔记16--I/O流和文件

本文地址:http://www.cnblogs.com/archimedes/p/java-study-note16.html,转载请注明源地址. IO(Input  Output)流 IO流用来处理设备之间的数据传输,对数据的操作是通过流的方式,Java用于操作流的对象都在IO包中 输入/输出流可以从以下几个方面进行分类 从流的方向划分: 输入流.输出流 从流的分工划分: 节点流.处理流 从流的内容划分: 面向字符的流.面向字节的流 字符流和字节流 字符流的由来: 因为数据编码的不同,而有了对

学习笔记: cdq分治

今年的课程有很大一部分内容是cdq分治及其扩展(也就是二进制分组),拜读后觉得还是蛮有用的,这里小小地总结一下.(话说自己草稿箱里还有好多学习笔记的半成品呢,真是弱爆了.顺便感谢下fy与wxl向我介绍了那么好的东西) 推荐论文: 1 <从<Cash>谈一类分治算法的应用> 陈丹琦 2 <浅谈数据结构题的几个非经典解法>  许昊然 Q: cdq分治和普通的分治有什么区别? A: 在我们平常使用的分治中,每一个子问题只解决它本身(可以说是封闭的).而在cdq分治中,对于划分

C++ Primer 学习笔记_34_面向对象编程(5)--虚函数与多态(二):纯虚函数、抽象类、虚析构函数、动态创建对象

C++ Primer 学习笔记_34_面向对象编程(5)--虚函数与多态(二):纯虚函数.抽象类.虚析构函数.动态创建对象 一.纯虚函数 1.虚函数是实现多态性的前提 需要在基类中定义共同的接口 接口要定义为虚函数 2.如果基类的接口没办法实现怎么办? 如形状类Shape 解决方法 将这些接口定义为纯虚函数 3.在基类中不能给出有意义的虚函数定义,这时可以把它声明成纯虚函数,把它的定义留给派生类来做 4.定义纯虚函数: class <类名> { virtual <类型> <函

Dynamic CRM 2013学习笔记(二十八)用JS动态设置字段的change事件、必填以及可见

我们知道通过界面设置字段的change事件,是否是必填,是否可见非常容易.但有时我们需要动态地根据某些条件来设置,这时有需要通过js来动态地控制了. 下面分别介绍如何用js来动态设置.   一.动态设置字段的change事件 // form on load event function onLoad() { init();   pageAttr.delivery_from.addOnChange(deliveryFromChange); pageAttr.type.addOnChange(typ