zoukankan      html  css  js  c++  java
  • 点分治及题目

    点分治

    一般可以用于处理大规模树上路径问题

    既然是处理路径问题,那么可以把路径分成两种,经过当前根节点的路径,不经过当前根节点的路径
    处理完经过当前根节点的路径,然后删掉根节点,此时肯定会形成一个或多个子树,那么剩下的不经过当前根节点的路径,递归到这些子树中处理
    删掉的节点肯定在接下来的处理中就不会被考虑了,这就意味着并不会有路径被重复统计

    然而如果图是一个链,那么一共要处理 (n) 个根节点,每次的处理一般不会低于 (O(n)),那么复杂度大于 (O(n^2)) 不够优秀
    所以在选取根节点的时候,要选取当前子树的重心,即当前子树中,最大的子树最小的一个节点
    重心的性质:以重心为根,重心的每个子树的大小,都不会超过整个树大小的一半,证明一下

    • 假设重心是 (u),它的一个子树大小超过整个树大小的一半,设这个子树的根是 (v)(与 (u) 相邻)
    • (size_i) 表示以 (i) 为根的子树大小,则 (size_v>frac{size_u}{2})
    • 那么,(u) 除了子树 (v) 以外的其它所有子树(把 (u) 本身也计算在内)的大小是 (size_u-size_v)
    • 所以,如果以 (v) 为重心,则它的一个子树是 (size_u-size_v),这个子树就是以 (u) 为根的那个。(size_u-size_v<size_v),此时,可以说明以 (u) 为根的那个子树,就是以 (v) 的所有子树中大小最大的那个,同时,也说明如果以 (v) 为重心,最大子树的大小小于以 (u) 为根最大子树的大小,则矛盾。得证

    因此,最多把整个树递归 (log n) 层即可,然后可以类比在数列上的分治,同一递归深度处理的 总复杂度(n) 有关,并且都相同,则复杂度是 (O( ext{每次处理的复杂度}log n))

    可以从这里看树的重心的其他性质的证明:https://www.cnblogs.com/suxxsfe/p/13543253.html

    模板题:P3806 【模板】点分治1
    给定一棵有 (n) 个点的树,(m) 次询问树上距离为 (k) 的点对是否存在。
    (kle 10^7,nle 10^4,mle 100)

    离线做,考虑能不能 (O(n)) 求出对于每一个询问,有没有一条符合要求的穿过当前根节点的路径
    枚举当前根的每一个子树,算出子树中每一个点距离根的距离,存入 (tmp) 数组,看一看之前是不是存在一个长度为 (k-tmp_i) 的路径
    如何判断是否存在?用一个 (judge) 数组,(judge_i) 表示目前有没有一条从某个已经访问过的子树到根节点的,长度为 (i) 的路径
    就判断 (judge_{k-tmp_i}) 就行,当 (tmp) 所有元素判断完之后,对于所有元素 judege[tmp[i]]=1
    这样就能统计到所有的路径,同时又不会出现路径”折返“的情况,就是两个在同一子树中的节点被计算(因为他们的路径不会经过当前的根)
    然后每个 (tmp) 中的距离都要存一下,用来清空 (judge),不能 memset

    找重心的过程用 find 函数实现,注意要调用两遍,原因在注释里
    复杂度 (O(nmlog n))

    #include<cstdio>
    #include<algorithm>
    #include<iostream>
    #include<cmath>
    #include<map>
    #include<iomanip>
    #include<cstring>
    #define reg register
    #define EN std::puts("")
    #define LL long long
    inline int read(){
    	register int x=0;register int y=1;
    	register char c=std::getchar();
    	while(c<'0'||c>'9'){if(c=='-') y=0;c=std::getchar();}
    	while(c>='0'&&c<='9'){x=x*10+(c^48);c=std::getchar();}
    	return y?x:-x;
    }
    #define N 10006
    #define M 20006
    struct graph{
    	int fir[N],nex[M],to[M],w[M],tot;
    	inline void add(int u,int v,int z){
    		to[++tot]=v;w[tot]=z;
    		nex[tot]=fir[u];fir[u]=tot;
    	}
    }G;
    int n,m,sum,root;//sum 记录当前树的大小,root 记录当前找到的重心 
    int ask[106],ans[106];
    int size[N],max[N],vis[N];//vis 记录一个节点是不是已经被删除 
    int tmp[N],dis[N],que[N];
    bool judge[10000006];
    void find(int x,int fa){
    	size[x]=1;max[x]=0;//函数会多次调用,所以这里要初始化 
    	for(reg int v,i=G.fir[x];i;i=G.nex[i]){
    		v=G.to[i];
    		if(v==fa||vis[v]) continue;
    		find(v,x);
    		size[x]+=size[v];
    		if(max[x]<size[v]) max[x]=size[v];
    	}
    	if(max[x]<sum-size[x]) max[x]=n-size[x];
    	if(max[x]<max[root]) root=x;
    }
    void get_dis(int u,int fa){
    	tmp[++tmp[0]]=dis[u];
    	for(reg int v,i=G.fir[u];i;i=G.nex[i]){
    		v=G.to[i];
    		if(v==fa||vis[v]) continue;
    		dis[v]=dis[u]+G.w[i];
    		get_dis(v,u);
    	}
    }
    inline void calc(int u){
    	que[0]=0;
    	for(reg int v,i=G.fir[u];i;i=G.nex[i]){
    		v=G.to[i];
    		if(vis[v]) continue;
    		tmp[0]=0;dis[v]=G.w[i];
    		get_dis(v,u);
    		for(reg int j=1;j<=tmp[0];j++){
    			for(reg int k=1;k<=m;k++)//遍历询问
    				if(ask[k]>=tmp[j]) ans[k]|=judge[ask[k]-tmp[j]];
    		}
    		for(reg int j=1;j<=tmp[0];j++)if(tmp[j]<=1e7){
    			//更改 judge,为后面的子树的查询使用,同时加入队列 
    			judge[tmp[j]]=1;que[++que[0]]=tmp[j];
    		}
    	}
    	for(reg int i=1;i<=que[0];i++) judge[que[i]]=0;
    }
    void divide(int u){
    //		std::printf("now : %d
    ",u);
    	vis[u]=judge[0]=1;
    	calc(u);
    	for(reg int v,i=G.fir[u];i;i=G.nex[i]){
    		v=G.to[i];
    		if(vis[v]) continue;
    		root=0;sum=size[v];
    		find(v,0);find(root,0);
    		divide(root);
    	}
    }
    int main(){
    	n=read();m=read();
    	for(reg int u,v,z,i=1;i<n;i++){
    		u=read();v=read();z=read();
    		G.add(u,v,z);G.add(v,u,z);
    	}
    	for(reg int i=1;i<=m;i++) ask[i]=read();
    	max[0]=sum=n;
    	find(1,0);find(root,0);
    	//此处要调用两遍,因为在 divide 函数中,要用到 size[v]
    	//而如果不以 root 为再调用一遍,size[v] 的值就是错的(以 1 为根,而实际上应该是以 root 为根)
    	//所以 divide 里再找到的重心也就是错的 
    	divide(root);
    	for(reg int i=1;i<=m;i++) std::puts(ans[i]?"AYE":"NAY");
    	return 0;
    }
    
    

    CF161D Distance in Tree

    传送门
    输入点数为 (n) 一棵树
    求树上长度恰好为 (k) 的路径个数

    和模板几乎一样,就把 (judge) 原来的记录”有没有“,改成记录”有几条”就行了
    代码

    P4178 Tree

    here
    给定一棵 (n) 个节点的树,每条边有边权,求出树上两点距离小于等于 (k) 的点对数量。
    (nle 4cdot 10^4,kle 2cdot 10^4)

    一开始想的是对每个 (k'le k),按上个题求出长度恰好等于 (k') 的答案,但是复杂度不行
    其实要把 (judge) 数组改成线段树,把每个 (tmp_i) 作为下标单点加一下就好,然后清空的时候是单点赋值,对于区间 ([0,k-tmp_i]) 用一个区间求和加到答案里就行了
    code

    P2634 [国家集训队]聪聪可可

    here
    树上任选两点,求这两点间距离是 (3) 的倍数的概率是多少

    问题转化为求有多少对点,之间的距离是 (3) 的倍数,然后除以 (n^2)
    就是把CF那题放在 (mod 3) 意义下,注意在这里 ((u,v),(v,u)) 算两个,且 ((u,u)) 也算,那么答案要乘二再加 (n)
    code

    P2664 树上游戏

    here
    前面几个都太水了,这个稍微有点难度
    一棵树,树的每个节点有个颜色。给一个长度为 (n) 的颜色序列,定义 (s(i,j))(i)(j) 的颜色数量。以及

    [sum_i=sum_{j=1}^n s(i, j) ]

    求出所有的 (sum_i)

    用点分治以后,问题变成如何求出以根为 LCA 的点对的答案贡献
    如果有一个点的颜色,在从根到它的路径上是第一次出现,那么他(设为 (u))可以为根的 其它 子树的答案都贡献上 (size_u)
    因为其它的节点都可以有一个到他子树的路径,会经过他,而他的颜色又是第一次出现
    (all=sum size_u)
    再设 (color\_sum_i),为颜色 (i)(all) 的贡献的总值是多少,就是所有颜色在从根到它的路径上是第一次出现,并且颜色是 (i) 的节点的 (size) 之和
    我们可以通过一遍 dfs 来求出上面的信息

    然后枚举每一个子树,尝试求出这个子树中所有点的答案
    首先肯定要把这个子树对 (all,color\_sum) 的贡献都去掉,因为上面说了这是对其它子树答案的贡献
    去掉的过程稍有麻烦,具体看代码能想明白
    然后再来一遍 dfs,做出答案
    我们设 (num)(u) 到根路径上的不同的颜色数,(all')(all)(减去此子树贡献的)减掉从 (u) 到路径上每个不同颜色,的 (color\_sum)
    则对答案的贡献是 (numcdot (size_{root}-size_x)-sum color\_sum),此处 (x) 是当前子树的根
    不难理解,(sum color\_sum) 意思就是 (u) 想要到达根,就要经过这些颜色,所以再在其它子树中经过,就不能再算了,贡献不能计算进去
    而一共有 (size_{root}-size_x) 个点可以到达,没到达一个点,在从 (u) 到根的路径中,都有 (num) 个不同颜色为答案产生贡献,所以加上 (numcdot (size_{root}-size_x))
    最后做完这遍 dfs 要把刚才去掉的贡献再加回去

    然后枚举完清零,也是 dfs 实现

    根节点的答案单独计算,是 (all-color\_sum(color_{root})+size_{root}),这点挺好理解

    code

    P4149 [IOI2011]Race

    here
    给一棵树,每条边有权。求一条简单路径,权值和等于 (k),且边的数量最小。

    裸的很,就维护距离的同时维护一个 (num_i) 表示距离为 (i) 的点,最少经过几条边
    然后把 (tmp)(judge) 里存的时候,再维护一个 (numed),维护的是当前根的所有子树的 (num),然后清空 (num) 数组,更新答案
    code

  • 相关阅读:
    (原)Lazarus 异构平台下多层架构思路、DataSet转换核心代码
    (学)新版动态表单研发,阶段成果3
    (学) 如何将 Oracle 序列 重置 清零 How to reset an Oracle sequence
    (学)XtraReport WebService Print 报错
    (原)三星 i6410 刷机 短信 无法 保存 解决 办法
    (原) Devexpress 汉化包 制作工具、测试程序
    linux下网络配置
    apache自带ab.exe小工具使用小结
    Yii::app()用法小结
    PDO使用小结
  • 原文地址:https://www.cnblogs.com/suxxsfe/p/12870120.html
Copyright © 2011-2022 走看看