静态点分治

基本概念

点分治,分动静两类,我所说的是静态的,他又叫树分治。

静态点分治,所谓分治,就是“分而治之”,把大的问题转化为若干个小的问题去解决。

他是一种能高效计算树上路径信息的算法。
静态点分治只查询不修改;动态点分治能修改加查询。

它是 树分治 ,之所以这样说,是因为他以重心将树分成相对平均的两个子树,使得不管是“链状”树,“菊花”树等特殊的树,他们的时间复杂度都相对平均。一般来说,分治可以将暴力的复杂度 O ( n 2 ) O(n^2) O(n2) 降到 O ( n l o g n ) O(n logn) O(nlogn)

我知道你很急着学会点分治,但你先别急,我们要一步一步来。

子树大小

下面有这样一个问题:
给出一个无根树,假如以 i i i 结点为根,结点 j j j i i i 的儿子结点,且j子树的结点数量是最多的,那么称结点 j j j 是结点 i i i 的"大儿子"。求每个节点的“大儿子”子树的节点数量。

定义 f i f_i fi 表示:结点 i i i 的"大儿子"子树的结点数量。
点分治1
由上图得:
f 1 = 4 f_1=4 f1=4 , f 2 = 5 f_2=5 f2=5 , f 3 = 3 f_3=3 f3=3 , f 4 = 5 f_4=5 f4=5 , f 5 = 3 f_5=3 f5=3 , f 6 = 5 f_6=5 f6=5 .

注意: f i f_i fi 是以 i i i 为根,不要被图迷惑,并不是以 1 1 1 为根。
以下是计算子树的代码:

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
int n,idx,head[maxn],size[maxn],f[maxn];
struct NODE{ int v,next; }e[maxn*2];

void Insert(int u,int v){
	e[++idx].v=v;
	e[idx].next=head[u];
	head[u]=idx;
}

void Dfs(int now,int fa,int nodeCnt){
	size[now]=1;//nodeCnt在这里虽然没用,但后面点分治时会用到
	f[now]=0;
	for(int i=head[now];i;i=e[i].next){
		int son=e[i].v;
		if(son!=fa){
			Dfs(son,now,nodeCnt);
			size[now]+=size[son];
			f[now]=max(f[now],size[son]);
		}
	}
	f[now]=max(f[now],nodeCnt-size[now]);
}

int main(){
	cin>>n;
	for(int i=1;i<n;i++){
		int x,y;
		cin>>x>>y;
		Insert(x,y);
		Insert(y,x);
	}
	Dfs(1,0,n);
	for(int i=1;i<=n;i++)
		cout<<f[i]<<" ";
	return 0;
}

到这里,我们已经回了如何计算子树。那接着需要在树上进行分治,以什么来分? 那当然是 树的重心 啦!

树的重心

树的重心 也叫 树的质心 ,它用于无根树(所以说点分治也是用于无根树)。

树的重心 u u u 是这样的一个节点 :以树上任意节点为根计算他子树的节点数,如果节点 u u u 的最大子树的节点数最少,那么 u u u 就是树的重心。——《算法竞赛》

若有一个问题:让你求一棵 n n n 个节点 n − 1 n-1 n1 条边的无根树的重心以及树上各个节点到重心的距离。(给定边权)
则代码如下:

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
int n,idx,head[maxn],hson_s,root,size[maxn],dis[maxn];
struct TREE{ int v,w,next; }e[maxn*2];

void Insert(int u,int v,int w){
	e[++idx].v=v;
	e[idx].w=w;
	e[idx].next=head[u];
	head[u]=idx;
}

void Find_root(int now,int fa,int nodeCnt){
	size[now]=1;
	int s=0; //s记录以now为根的子树节点数量的最大值 
	for(int i=head[now];i;i=e[i].next){
		int son=e[i].v;
		if(son!=fa){
			Find_root(son,now,nodeCnt);
			size[now]+=size[son];
			s=max(s,size[son]);
		}
	}
	s=max(s,nodeCnt-size[now]);
	if(s<hson_s) hson_s=s,root=now;
}

void Get_dis(int now,int fa,int d){
	dis[now]=d;
	for(int i=head[now];i;i=e[i].next){
		int son=e[i].v;
		if(son!=fa) Get_dis(son,now,d+e[i].w);
	}
}

void Solve(int now,int fa,int nodeCnt){
	hson_s=nodeCnt-1,root=now;//hson_s为重心的重儿子的节点数量, root为重心 
	Find_root(now,fa,nodeCnt);
	dis[root]=0;
	for(int i=head[root];i;i=e[i].next){
		int son=e[i].v;
		Get_dis(son,root,e[i].w);
	}
}

int main(){
	cin>>n;
	for(int i=1;i<n;i++){
		int x,y,z;
		cin>>x>>y>>z;
		Insert(x,y,z);
		Insert(y,x,z);
	}
	Solve(1,0,n);
	cout<<root<<"\n";
	for(int i=1;i<=n;i++)
		cout<<dis[i]<<" ";
	return 0;
}

!!!树的重心的性质: !!!

s i z e [ i ] size[i] size[i] 为记录子树i的节点数量。
假设以树的重心 r o o t root root 为根的所有儿子中,儿子 i i i 为根的子树的节点数最多,记为 s i z e [ i ] size[i] size[i] ,那么 s i z e [ i ] ≤ s i z e [ r o o t ] / 2 size[i] \le size[root]/2 size[i]size[root]/2

证明:可使用 反证法 。若 s i z e [ i ] > s i z e [ r o o t ] / 2 size[i] > size[root]/2 size[i]>size[root]/2 则不满足 r o o t root root 为树的重心,此时重心就不为 r o o t root root 了,所以得证。

求经过重心的合法路径数量

下面给出一个问题:
给出一棵 n n n 个节点 n − 1 n-1 n1 条边的无根树,边权都是 1 1 1 ,求出重心 r o o t root root 之后(如果有多个 r o o t root root ,只要结点编号最小的那个 r o o t root root ),求有多少条简单路径满足如下条件:
(1)简单路径经过重心 r o o t root root
(2)简单路径的起点和终点不能在 r o o t root root 的同一颗子树内。
(3)简单路径的长度等于 k k k ,路径长度是指经过的边的权值之和。
1 ≤ n ≤ 50000 1 \le n \le 50000 1n50000 , 1 ≤ k ≤ 500 1 \le k \le 500 1k500 .

对于这个问题,一般我们有 2 2 2 种做法。
两种做法的最大区别在于:

  • 一种是直接计算答案
  • 一种是所有答案 − - 不合法的答案(适由于求方案数)

当然,两种做法各有所长,要因题而异。

F1:

这种做法是 直接计算答案 。
题目说两个端节点不能在同一棵子树内,那我们就分别计算以重心为根的每一棵子树对答案的贡献。

#include<bits/stdc++.h>
using namespace std;
const int maxn=5e4+5;
int n,k,idx,head[maxn],size[maxn],dis[maxn],ans,hson_s,root,p,cnt[maxn];
bool vis[maxn];
struct TREE{ int v,next; }e[maxn*2];

void Insert(int u,int v){
	e[++idx].v=v;
	e[idx].next=head[u];
	head[u]=idx;
}

void Find_root(int now,int fa,int nodeCnt){
	size[now]=1;
	int s=0;
	for(int i=head[now];i;i=e[i].next){
		int son=e[i].v;
		if(son!=fa&&!vis[son]){
			Find_root(son,now,nodeCnt);
			size[now]+=size[son];
			s=max(s,size[son]);
		}
	}
	s=max(s,nodeCnt-size[now]);
	if(s<hson_s) hson_s=s,root=now;
}

void Get_dis(int now,int fa,int d){
	size[now]=1;
	dis[++p]=d;//dis数组保存各个子树的节点到root的长度的数量,dis[i]=j表示有j个节点到root节点的长度为i 
	for(int i=head[now];i;i=e[i].next){
		int son=e[i].v;
		if(son!=fa&&!vis[son]){
			Get_dis(son,now,d+1);
			size[now]+=size[son];
		}
	}
}

void Solve(int now,int fa,int nodeCnt){
	hson_s=nodeCnt-1,root=now;
	Find_root(now,fa,nodeCnt);//找当前now所在的树的重心 
	vis[root]=true;//相当于删掉重心节点 
	p=0;
	int l=0;
	cnt[0]=1;//cnt[i]=j表示有j个节点到root节点的长度为i 
	for(int i=head[root];i;i=e[i].next){//!!统计答案的时候,要确保路径的两个端点来自于root的不同子树 
		int son=e[i].v;
		if(son!=fa&&!vis[son]){
			Get_dis(son,root,1);
			//至此,son子树内所有节点到root的长度,保存在dis[l+1...p]
			//接下来先统计答案,在合并子树的信息 
			for(int j=l+1;j<=p;j++)
				if(k>=dis[j]) ans+=cnt[k-dis[j]];
			for(int j=l+1;j<=p;j++)
				cnt[dis[j]]++;//更新cnt数组,这样就保证了答案都来自于不同子树 
			l=p;
		}
	}
	for(int i=1;i<=p;i++)//还原cnt数组,因为后面会再次调用Solve函数 
		cnt[dis[i]]--;
}

int main(){
	cin>>n>>k;
	for(int i=1;i<n;i++){
		int x,y;
		cin>>x>>y;
		Insert(x,y);
		Insert(y,x);
	}
	Solve(1,0,n);
	cout<<ans;
	return 0;
}

F2:

这种做法是 所有答案 − - 不合法的答案 (适由于求方案数)。
以这道题来说就是 (两个端点在不同的子树 + + + 两个端点在同一个子树) − - 两个端点在同一个子树;
这种方法看似没用,但他计算答案的时候序列是具有单调性的(序列是有序的),这样就可以用到二分。我这样说或许不太清晰,那就直接看程序吧。

#include<bits/stdc++.h>
using namespace std;
const int maxn=5e4+5;
int n,k,idx,head[maxn],size[maxn],dis[maxn],ans,hson_s,root,p;
bool vis[maxn];
struct TREE{ int v,next; }e[maxn*2];

void Insert(int u,int v){
	e[++idx].v=v;
	e[idx].next=head[u];
	head[u]=idx;
}

void Find_root(int now,int fa,int nodeCnt){
	size[now]=1;
	int s=0;
	for(int i=head[now];i;i=e[i].next){
		int son=e[i].v;
		if(son!=fa&&!vis[son]){
			Find_root(son,now,nodeCnt);
			size[now]+=size[son];
			s=max(s,size[son]);
		}
	}
	s=max(s,nodeCnt-size[now]);
	if(s<hson_s) hson_s=s,root=now;
}

void Get_dis(int now,int fa,int d){
	size[now]=1;
	dis[++p]=d;//dis数组保存各个子树的节点到root的长度的数量,dis[i]=j表示有j个节点到root节点的长度为i 
	for(int i=head[now];i;i=e[i].next){
		int son=e[i].v;
		if(son!=fa&&!vis[son]){
			Get_dis(son,now,d+1);
			size[now]+=size[son];
		}
	}
}

void Calc(int flag){
	sort(dis+1,dis+p+1);//排序 
	for(int i=1;i<=p;i++){//二分 
		int x=upper_bound(dis+1,dis+i,k-dis[i])-(dis+1);
		int y=upper_bound(dis+1,dis+i,k-dis[i]-1)-(dis+1);
		ans+=flag*(x-y);
	}
	//也可用利用双指针,单调处理 
}

void Solve(int now,int fa,int nodeCnt){
	hson_s=nodeCnt-1,root=now;
	Find_root(now,fa,nodeCnt);//找当前now所在的树的重心 
	vis[root]=true;//相当于删掉重心节点 
	p=0;
	Get_dis(root,0,0);//计算各个节点到root的长度,保存在dis[1...p];并重新计算size数组 
	Calc(1);//统计经过重心root的点对(i,j)且dis[i]+dis[j]=k的数量,1表示是加,-1表示减
	for(int i=head[root];i;i=e[i].next){
		int son=e[i].v;
		if(son!=fa&&!vis[son]){
			p=0;//记得每次都要重置为0 
			Get_dis(son,root,1);//计算以son为根的子树内所有节点到root的距离,保存在dis[1...p] 
			Calc(-1);//相当于减去两个端点都在同一颗子树内的方案数 
		}
	} 
}

int main(){
	cin>>n>>k;
	for(int i=1;i<n;i++){
		int x,y;
		cin>>x>>y;
		Insert(x,y);
		Insert(y,x);
	}
	Solve(1,0,n);
	cout<<ans;
	return 0;
}

求路径长度等于k的数量

(这道题应该就是一道正宗的点分治题了吧)纯属个人猜测

这道题是这样的:
给出一棵无根树,边权都是 1 1 1 ,求有多少条简单路径的长度等于 k k k ,路径长度是指经过的边的权值之和。
1 ≤ n ≤ 50000 1 \le n \le 50000 1n50000 , 1 ≤ k ≤ 500 1 \le k \le 500 1k500 .

这道题与上一题相比,无非就是这条路径上的起点和终点没有限制,就他们可以在同一子树内,也可以在不同子树内。
那根据题目, 答案 = 路径上起点和终点不在同一子树内的数量 + 路径上起点和终点在同一子树内的数量 答案=路径上起点和终点不在同一子树内的数量+路径上起点和终点在同一子树内的数量 答案=路径上起点和终点不在同一子树内的数量+路径上起点和终点在同一子树内的数量 ; 那前半段我们在上一题就解决了,后半段可以这样想:把重心 r o o t root root 删除后,这棵树就分成了他的几棵子树,问题就变成了——计算各棵子树有多少条简单路径的长度等于 k k k(这不就又回到了我们刚开始的问题了吗)。所以我们可以用 递归 多次调用函数,不断的删除树的重心。
我们直接在上题程序中加上删除 r o o t root root 在递归子问题就好了。(我只写了 F 1 F1 F1 的程序)
这个的时间复杂度应该是 O ( n l o g n ) O(n logn) O(nlogn) 吧! 谁能告诉我正确的复杂度啊~

#include<bits/stdc++.h>
using namespace std;
const int maxn=5e4+5;
int n,k,idx,head[maxn],size[maxn],dis[maxn],ans,hson_s,root,p,cnt[maxn];
bool vis[maxn];
struct TREE{ int v,next; }e[maxn*2];

void Insert(int u,int v){
	e[++idx].v=v;
	e[idx].next=head[u];
	head[u]=idx;
}

void Find_root(int now,int fa,int nodeCnt){
	size[now]=1;
	int s=0;
	for(int i=head[now];i;i=e[i].next){
		int son=e[i].v;
		if(son!=fa&&!vis[son]){
			Find_root(son,now,nodeCnt);
			size[now]+=size[son];
			s=max(s,size[son]);
		}
	}
	s=max(s,nodeCnt-size[now]);
	if(s<hson_s) hson_s=s,root=now;
}

void Get_dis(int now,int fa,int d){
	size[now]=1;
	dis[++p]=d;//dis数组保存各个子树的节点到root的长度的数量,dis[i]=j表示有j个节点到root节点的长度为i 
	for(int i=head[now];i;i=e[i].next){
		int son=e[i].v;
		if(son!=fa&&!vis[son]){
			Get_dis(son,now,d+1);
			size[now]+=size[son];
		}
	}
}

void Solve(int now,int fa,int nodeCnt){
	hson_s=nodeCnt-1,root=now;
	Find_root(now,fa,nodeCnt);//找当前now所在的树的重心 
	vis[root]=true;//相当于删掉重心节点 
	p=0;
	int l=0;
	cnt[0]=1;//cnt[i]=j表示有j个节点到root节点的长度为i 
	for(int i=head[root];i;i=e[i].next){//!!统计答案的时候,要确保路径的两个端点来自于root的不同子树 
		int son=e[i].v;
		if(son!=fa&&!vis[son]){
			Get_dis(son,root,1);
			//至此,son子树内所有节点到root的长度,保存在dis[l+1...p]
			//接下来先统计答案,在合并子树的信息 
			for(int j=l+1;j<=p;j++)
				if(k>=dis[j]) ans+=cnt[k-dis[j]];
			for(int j=l+1;j<=p;j++)
				cnt[dis[j]]++;//更新cnt数组,这样就保证了答案都来自于不同子树 
			l=p;
		}
	}
	for(int i=1;i<=p;i++)//还原cnt数组,因为后面会再次调用Solve函数 
		cnt[dis[i]]--;
	for(int i=head[root];i;i=e[i].next){//分治,处理子问题 
		int son=e[i].v;
		if(son!=fa&&!vis[son]) Solve(son,root,size[son]);
		//!!!下面是我调了很久的错误:!!! 
		//上面son的父亲是root而不是now; 
		//子树的变化导致整棵树的节点数量也在变,不再是n,而是size[son]。 
	}
}

int main(){
	cin>>n>>k;
	for(int i=1;i<n;i++){
		int x,y;
		cin>>x>>y;
		Insert(x,y);
		Insert(y,x);
	}
	Solve(1,0,n);
	cout<<ans;
	return 0;
}

注意:上面程序中有本人调了很久才调出来的细节错误,请注意。

到此,我们终于大体明白了点分治。但要掌握,还需要跨过点分治的练习题海。

练习题

1. Tree

很特别,我做的第一道点分治题居然不是洛谷上的模板题
但我还是要好好纪念一下,毕竟这题我足足调了3个小时;当然,最后这题不是我调出来的,是sst大佬,%%%感谢感谢。

言归正传,这题我用了上述的 F 2 F2 F2
计算答案的时候直接算当前子树每个点对答案的贡献,就是在已经算好的子树的结点中找有多少个结点到重心 r o o t root root 的距离 ≤ ( k − 当前结点到重心 r o o t 的距离 ) \le (k-当前结点到重心root的距离) (k当前结点到重心root的距离)
我们可以用个数组记录已经算好的子树的各个结点到重心 r o o t root root 的距离,并保证数组有序,查询时就直接二分就好啦!
∴ \therefore 这种方法的时间复杂度大致为: O ( n l o g 2 n ) O(n log^2 n) O(nlog2n)

#include<bits/stdc++.h>
using namespace std;
const int maxn=4e4+5;
int n,k,idx,head[maxn],size[maxn],dis[maxn],ans,hson_s,root,p;
bool vis[maxn];
struct TREE{ int v,w,next; }e[maxn*2];

void Insert(int u,int v,int w){
	e[++idx].v=v;
	e[idx].w=w;
	e[idx].next=head[u];
	head[u]=idx;
}

void Find_root(int now,int fa,int nodeCnt){
	size[now]=1;
	int s=0;
	for(int i=head[now];i;i=e[i].next){
		int son=e[i].v;
		if(son!=fa&&!vis[son]){
			Find_root(son,now,nodeCnt);
			size[now]+=size[son];
			s=max(s,size[son]);
		}
	}
	s=max(s,nodeCnt-size[now]);
	if(s<hson_s) hson_s=s,root=now;
}

void Get_dis(int now,int fa,int d){
	size[now]=1;
	dis[++p]=d;
	for(int i=head[now];i;i=e[i].next){
		int son=e[i].v;
		if(son!=fa&&!vis[son]){
			Get_dis(son,now,d+e[i].w);
			size[now]+=size[son];
		}
	}
}

void Calc(int flag){
	sort(dis+1,dis+p+1);
	for(int i=1;i<=p;i++){
		int x=upper_bound(dis+1,dis+i,k-dis[i])-(dis+1);
		ans+=flag*x;
	}
}

void Solve(int now,int fa,int nodeCnt){ //1
	hson_s=nodeCnt-1,root=now;
	Find_root(now,fa,nodeCnt);
	vis[root]=true;
	p=0;
	Get_dis(root,0,0);
	Calc(1);
	for(int i=head[root];i;i=e[i].next){
		int son=e[i].v;
		if(son!=fa&&!vis[son]){
			p=0;
			Get_dis(son,root,e[i].w);
			Calc(-1);
		}
	}
	for(int i=head[root];i;i=e[i].next){
		int son=e[i].v;
		if(son!=fa&&!vis[son]) Solve(son,root,size[son]); //2
	}
}

int main(){
	cin>>n;
	for(int i=1;i<n;i++){
		int x,y,z;
		cin>>x>>y>>z;
		Insert(x,y,z);
		Insert(y,x,z);
	}
	cin>>k;
	Solve(1,0,n);
	cout<<ans;
	return 0;
}

注意两个小细节就好了:
1.第 50 50 50 行的 n o d e C n t nodeCnt nodeCnt ,记得每次调用的 n o d e C n t nodeCnt nodeCnt 都不同,不是一成不变的 n n n
2.第 67 67 67 行的 S o l v e Solve Solve 函数中,第二个参数是 r o o t root root ,不要误写成 n o w now now s o n son son 的父亲不是 n o w now now

2. [IOI2011]Race

这道题不是求 方案数 ,所以我们不能继续沿用 F 2 F2 F2 ,只好用 F 1 F1 F1
这题我们在 G e t Get Get_ d i s dis dis 中求出每个点到重心 r o o t root root 的边权和所经过的边的数量,计算答案时把求和改为最小值就好了。
有一点需要注意:在算答案的时候,我们需要以每个结点到 r o o t root root 的边权和作为下标,此时下标可能会很大,超出可定范围;但好在对答案可能有贡献的边权和一定是 ≤ k \le k k 的,只有边权和符合条件情况下的我们才会加入 c n t cnt cnt 数组,所以在操作或更新 c n t cnt cnt 数组时先判断下标 ≤ k \le k k 的再执行。

#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+5,INF=0x3f3f3f3f,maxk=1e6+5;
int n,k,idx,head[maxn],size[maxn],dis[maxn],ans,hson_s,root,p,dis2[maxn],cnt[maxk];
bool vis[maxn];
struct TREE{ int v,w,next; }e[maxn*2];

void Insert(int u,int v,int w){
	e[++idx].v=v;
	e[idx].w=w;
	e[idx].next=head[u];
	head[u]=idx;
}

void Find_root(int now,int fa,int nodeCnt){
	size[now]=1;
	int s=0;
	for(int i=head[now];i;i=e[i].next){
		int son=e[i].v;
		if(son!=fa&&!vis[son]){
			Find_root(son,now,nodeCnt);
			size[now]+=size[son];
			s=max(s,size[son]);
		}
	}
	s=max(s,nodeCnt-size[now]);
	if(s<hson_s) hson_s=s,root=now;
}

void Get_dis(int now,int fa,int d,int d2){
	size[now]=1;
	dis[++p]=d;
	dis2[p]=d2;
	for(int i=head[now];i;i=e[i].next){
		int son=e[i].v;
		if(son!=fa&&!vis[son]){
			Get_dis(son,now,d+e[i].w,d2+1);
			size[now]+=size[son];
		}
	}
}

void Solve(int now,int fa,int nodeCnt){
	hson_s=nodeCnt-1,root=now;
	Find_root(now,fa,nodeCnt);
	vis[root]=true;
	p=0;
	int l=0;
	cnt[0]=0;
	for(int i=head[root];i;i=e[i].next){
		int son=e[i].v;
		if(son!=fa&&!vis[son]){
			Get_dis(son,root,e[i].w,1);
			for(int j=l+1;j<=p;j++)
				if(k>=dis[j]) ans=min(ans,cnt[k-dis[j]]+dis2[j]);//1 如上文,就这里(1)和下面(2)加个判断
			for(int j=l+1;j<=p;j++)
				if(dis[j]<=maxk) cnt[dis[j]]=min(cnt[dis[j]],dis2[j]);//2
			l=p;
		}
	}
	for(int i=1;i<=p;i++)
		if(dis[i]<=maxk) cnt[dis[i]]=INF;
	for(int i=head[root];i;i=e[i].next){
		int son=e[i].v;
		if(son!=fa&&!vis[son]) Solve(son,root,size[son]);
	}
}

int main(){
	cin>>n>>k;
	for(int i=1;i<n;i++){
		int x,y,z;
		cin>>x>>y>>z;
		x++,y++;
		Insert(x,y,z);
		Insert(y,x,z);
	}
	for(int i=1;i<maxk;i++)
		cnt[i]=INF;
	ans=INF;
	Solve(1,0,n);
	if(ans==INF) cout<<-1;
	else cout<<ans;
	return 0;
}

3. 树上黑点路径

点分治2
点分治3
这道题也是用 F 1 F1 F1 ,算完每条路径上的黑点个数后,就直接查询在已经算完的结点中黑点个数 ≤ ( k − 当前这条路径的黑点个数的所有节点之中的最大边权值 ) \le (k - 当前这条路径的黑点个数的所有节点之中的最大边权值) (k当前这条路径的黑点个数的所有节点之中的最大边权值)
相较于上一题,就把单点查询改为区间查询,可以用树状数组或线段树进行存储。(我用了前者,因为码量相对来说小些)
注意:计算重心 r o o t root root 的不同子树间的两条路径的黑点个数的时候还要算上 r o o t root root 是否为黑点,是的话黑点个数还要加一!

#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+5;
int n,k,m,idx,head[maxn],size[maxn],dis[maxn],ans,hson_s,root,p,dis2[maxn],f[maxn];
bool vis[maxn],hd[maxn];
struct TREE{ int v,w,next; }e[maxn*2];

void Insert(int u,int v,int w){
	e[++idx].v=v;
	e[idx].w=w;
	e[idx].next=head[u];
	head[u]=idx;
}

void Find_root(int now,int fa,int nodeCnt){
	size[now]=1;
	int s=0;
	for(int i=head[now];i;i=e[i].next){
		int son=e[i].v;
		if(son!=fa&&!vis[son]){
			Find_root(son,now,nodeCnt);
			size[now]+=size[son];
			s=max(s,size[son]);
		}
	}
	s=max(s,nodeCnt-size[now]);
	if(s<hson_s) hson_s=s,root=now;
}

void Get_dis(int now,int fa,int d,int d2){
	size[now]=1;
	dis[++p]=d;
	dis2[p]=d2;
	for(int i=head[now];i;i=e[i].next){
		int son=e[i].v;
		if(son!=fa&&!vis[son]){
			Get_dis(son,now,d+hd[son],d2+e[i].w);
			size[now]+=size[son];
		}
	}
}

void Add(int x,int y){
	if(!x) return;
	while(x<=k){
		f[x]=max(f[x],y);
		x+=x&(-x);
	}
}

int Ask(int x){
	int sum=0;
	if(!x) sum=f[x];
	while(x>0){
		sum+=f[x];
		x-=x&(-x);
	}
	return sum;
}

void Del(int x){
	if(!x){
		f[x]=0;
		return;
	}
	while(x<=k){
		f[x]=0;
		x+=x&(-x);
	}
}

void Solve(int now,int fa,int nodeCnt){
	hson_s=nodeCnt-1,root=now;
	Find_root(now,fa,nodeCnt);
	vis[root]=true;
	p=0;
	int l=0;
	for(int i=head[root];i;i=e[i].next){
		int son=e[i].v;
		if(son!=fa&&!vis[son]){
			Get_dis(son,root,hd[son],e[i].w);
			for(int j=l+1;j<=p;j++)
				if(k>=dis[j]+hd[root]) ans=max(ans,Ask(k-dis[j]-hd[root])+dis2[j]);//这里就是上文所说的注意地方
			for(int j=l+1;j<=p;j++)
				Add(dis[j],dis2[j]);
			l=p;
		}
	}
	for(int i=1;i<=p;i++)
		Del(dis[i]);
	for(int i=head[root];i;i=e[i].next){
		int son=e[i].v;
		if(son!=fa&&!vis[son]) Solve(son,root,size[son]);
	}
}

int main(){
	cin>>n>>k>>m;
	for(int i=1;i<=m;i++){
		int x;
		cin>>x;
		hd[x]=1;
	}
	for(int i=1;i<n;i++){
		int x,y,z;
		cin>>x>>y>>z;
		Insert(x,y,z);
		Insert(y,x,z);
	}
	Solve(1,0,n);
	cout<<ans;
	return 0;
}

4. [BJOI2017] 树的难题

这题的细节很多,我还没做。

5. Ruri Loves Maschera

这题我用的是 F 2 F2 F2 ,虽然说是求和,但我觉得 F 2 F2 F2 好写些。
此题也是区间查询,运用了二分+树状数组。
!!写的时候要注意下标,思路在脑子里尽量保持清晰。
!!记得 a n s ans ans 要开 l o n g l o n g long long longlong不开long long见祖宗

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
int n,L,R,idx,head[maxn],size[maxn],hson_s,root,p,f[maxn];
long long ans; //注意点,要开long long
bool vis[maxn];
struct TREE{ int v,next; long long w; }e[maxn*2];
struct NODE{ int d; long long d2; }dis[maxn];

void Insert(int u,int v,long long w){
	e[++idx].v=v;
	e[idx].w=w;
	e[idx].next=head[u];
	head[u]=idx;
}

void Find_root(int now,int fa,int nodeCnt){
	size[now]=1;
	int s=0;
	for(int i=head[now];i;i=e[i].next){
		int son=e[i].v;
		if(son!=fa&&!vis[son]){
			Find_root(son,now,nodeCnt);
			size[now]+=size[son];
			s=max(s,size[son]);
		}
	}
	s=max(s,nodeCnt-size[now]);
	if(s<hson_s) hson_s=s,root=now;
}

void Get_dis(int now,int fa,int d,long long d2){
	size[now]=1;
	dis[++p]={d,d2};
	for(int i=head[now];i;i=e[i].next){
		int son=e[i].v;
		if(son!=fa&&!vis[son]){
			Get_dis(son,now,d+1,max(d2,e[i].w));
			size[now]+=size[son];
		}
	}
}

bool cmp(NODE a,NODE b){
	return a.d2<b.d2;
}

void Add(int x,int y){
	x++;
	while(x<=n){
		f[x]+=y;
		x+=x&(-x);
	}
}

int Ask(int x){
	x++;
	int s=0;
	while(x>0){
		s+=f[x];
		x-=x&(-x);
	}
	return s;
} 

void Calc(int flag){
	sort(dis+1,dis+p+1,cmp);
	for(int i=1;i<=p;i++){
		ans+=flag*((Ask(R-dis[i].d)-Ask(L-dis[i].d-1))*dis[i].d2);
		Add(dis[i].d,1);
	}
	for(int i=1;i<=p;i++)
		Add(dis[i].d,-1);
}

void Solve(int now,int fa,int nodeCnt){
	hson_s=nodeCnt-1,root=now;
	Find_root(now,fa,nodeCnt);
	vis[root]=true;
	p=0;
	Get_dis(root,0,0,0);
	Calc(1);
	for(int i=head[root];i;i=e[i].next){
		int son=e[i].v;
		if(son!=fa&&!vis[son]){
			p=0;
			Get_dis(son,root,1,e[i].w);
			Calc(-1);
		}
	}
	for(int i=head[root];i;i=e[i].next){
		int son=e[i].v;
		if(son!=fa&&!vis[son]) Solve(son,root,size[son]);
	}
}

int main(){
	cin>>n>>L>>R;
	for(int i=1;i<n;i++){
		int x,y,z;
		cin>>x>>y>>z;
		Insert(x,y,z);
		Insert(y,x,z);
	}
	Solve(1,0,n);
	cout<<ans*2; //答案记得*2,因为路径(x,y)与(y,x)是不同的两条路径
	return 0;
}

6. Close Vertices

这道题和第 5 5 5 题大体一样,还是用 F 2 F2 F2
就查询答案的时候不能用二分,用双指针,单调处理。
!!! C a l c Calc Calc 函数中的双指针统计答案需要注意下。

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
int n,L,W,idx,head[maxn],size[maxn],hson_s,root,p,f[maxn];
long long ans;
bool vis[maxn];
struct TREE{ int v,w,next; }e[maxn*2];
struct NODE{ int d,d2; }dis[maxn];

void Insert(int u,int v,int w){
	e[++idx].v=v;
	e[idx].w=w;
	e[idx].next=head[u];
	head[u]=idx;
}

void Find_root(int now,int fa,int nodeCnt){
	size[now]=1;
	int s=0;
	for(int i=head[now];i;i=e[i].next){
		int son=e[i].v;
		if(son!=fa&&!vis[son]){
			Find_root(son,now,nodeCnt);
			size[now]+=size[son];
			s=max(s,size[son]);
		}
	}
	s=max(s,nodeCnt-size[now]);
	if(s<hson_s) hson_s=s,root=now;
}

void Get_dis(int now,int fa,int d,int d2){
	size[now]=1;
	dis[++p]={d,d2};
	for(int i=head[now];i;i=e[i].next){
		int son=e[i].v;
		if(son!=fa&&!vis[son]){
			Get_dis(son,now,d+1,d2+e[i].w);
			size[now]+=size[son];
		}
	}
}

bool cmp(NODE a,NODE b){
	return a.d2<b.d2;
}

void Add(int x,int y){
	x++;
	while(x<=n+1){
		f[x]+=y;
		x+=x&(-x);
	}
}

int Ask(int x){
	x++;
	int s=0;
	while(x>0){
		s+=f[x];
		x-=x&(-x);
	}
	return s;
} 

void Calc(int flag){ //这个函数注意一下就好了
	sort(dis+1,dis+p+1,cmp);
	for(int i=1;i<=p;i++)
		Add(dis[i].d,1);
	int r=p;
	for(int l=1;l<=p;l++){
		Add(dis[l].d,-1);
		while(dis[l].d2+dis[r].d2>W&&l<r){
			Add(dis[r].d,-1);
			r--;
		}
		if(l==r) break;
		ans+=flag*Ask(L-dis[l].d);
	}
}

void Solve(int now,int fa,int nodeCnt){
	hson_s=nodeCnt-1,root=now;
	Find_root(now,fa,nodeCnt);
	vis[root]=true;
	p=0;
	Get_dis(root,0,0,0);
	Calc(1);
	for(int i=head[root];i;i=e[i].next){
		int son=e[i].v;
		if(son!=fa&&!vis[son]){
			p=0;
			Get_dis(son,root,1,e[i].w);
			Calc(-1);
		}
	}
	for(int i=head[root];i;i=e[i].next){
		int son=e[i].v;
		if(son!=fa&&!vis[son]) Solve(son,root,size[son]);
	}
}

int main(){
	cin>>n>>L>>W;
	for(int i=2;i<=n;i++){
		int y,z;
		cin>>y>>z;
		Insert(i,y,z);
		Insert(y,i,z);
	}
	Solve(1,0,n);
	cout<<ans;
	return 0;
}

7. GCD Counting

还未做,但是这题我已知的有3种解法,可以上洛谷see see。

总结

静态点分治是一个比较难的算法,需要多次练习。
所谓:“温故而知新,可以成才为师矣”!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值