点分治学习随笔

点分治学习随笔

0 前言

本蒟蒻看这个点分树模板看了3天才看懂

十分气愤

于是决定写点分治和点分树的学习随笔巩固一下

1点分治

1.1引入

思考:如何统计树上满足某条件的路径数量

(例如:求树上满足u到v最短路径长度小于等于k的二元组(u,v)数量)

显然,如果我们直接暴力搜索复杂度\(O(n^3)\)

那样我们不就直接T飞了吗?

所以我们需要惊人的注意力来优化一下这个东西

对于二元组,我们很自然地想到可以先提前处理出一个点造出的贡献

最后加在一起就可以了

那么怎么处理呢?

1.2做法

1.2.1问题转化

对于一个根节点u,对于u的子树,我们可以将路径分为两类:

  1. 过u
  2. 不过u

这样分有什么用呢?

as we all know 通过dfs我们可以将树上每一个点扫到

所以对于类型2我们可以在继续向下dfs时将其转化为类型1

那么类型1又该怎么办呢?

我们画一棵树来观察一下

经过u的路径长度

\(dis(x,y)=dis(x,u)+dis(y,u)\)

比如图中过u的合法路径是:(只看s1,s2,s3)

s1->u->s2(4+7=11),s1->u->s3(4+6=10)

s1->u(4+0=4),s2->u(7+0=7),s3->u(6+0=6)

(直接到根节点也是可以的)

那么我们就将问题转化为: 求二元组(x,y)满足\(dis(x,u)+dis(y,u)<=k\)\(b[x]!=b[y]\)的数量

d[i]=dis(i,u) i到根节点u的距离

b[i]=(i属于u的哪一个子树s1/s2/s3...)

这两个东西我们可以提前dfs预处理出来

为什么是\(b[x]!=b[y]\)

还是上面的图 如果我们将k改为20

不加上这个条件我们x->u->x,x->u->fa[x],x->u->s1就都为u所能产生的合法路径

但是这些都不是最短路径,所以我们需要舍去

1.2.2答案统计优化

那么又有人问了:主播主播 你这里枚举d[i]不还是\(O(n^2)\)吗?

别慌 我们已经转化成了如此精美的东西

那么用上一点神奇的小优化就可以了

看这个东西:\(dis(x,u)+dis(y,u)<=k\)

就是我们预处理出的d[x]+d[y]<=k

还看不出来什么的话我们先给这些d[i]排个序

现在是不是有点双指针的感觉了?

那么我们将所有点按d[i]排序

设双指针区间是[L,R]

若d[L]+d[R]<=k,说明L可以和L+1到R间所有长度匹配

ans+=R-L

然后L++ ,操作R直到满足上面的条件

但是我们不想要在同一子树内的长度

于是维护cnt[b[L]]

表示当前双指针区间内和L在同一子树内的点

这个很好维护,一开始cnt[b[i]]都++

然后在操作L,R时对应加减cnt即可

最后就变成了ans+=R-L-cnt[b[L]]

那么我们直接就干掉了一个\(O(n)\)

现在的一个子树的处理复杂度来到了惊人的 \(O(n+log n)\)=\(O(n)\)

1.2.3重心

点分治还有另一个重要操作 找重心

为什么呢?

先看我们现在的复杂度:

我们现在是一个点一个点地处理 处理完后就将这个点删去

假如说我们以U为根

一共是114514+3个点

然后删掉U

剩下S1以及S2和它的子树

S1一个点 但是S2数量达到惊人的114514+1个点

我们如果每次找的根都出现这种情况

最后的复杂度相当于来到了\(O(n^2)\)

那怎么办呢?

我们发现这里的n其实是以为子树最大大小过大导致的

那么我们让它的最大子树大小尽可能小不就行了

于是我们直接扫一遍当前处理区块中每一个点的子树大小和当前点下最大子树大小

最后找出最大子树大小最小的点作为重心即可

这一步放dfs里面一起预处理一下丝毫不影响时间复杂度

由于每次到找的重心 它的每个子树大小最大都为当前大小的一半

所以最后的复杂度就是\(O(nlogn)\)

最后附上猎奇重口的模板代码:

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=40005;
const int INF=0x3f3f3f3f;
int n,k;
int aa,bb,cc;
struct node{
	int v,w;
};
vector<node> child[N];
void add(int a,int b,int c){
	child[a].push_back({b,c});
	child[b].push_back({a,c});
}
int dep[N];//深度
int dd[N];//到根节点距离
int vis[N];//被分治过没
node dis[N];//存所有距离
int cnt;
int root;
int Max[N];//该节点下最大子树大小
int siz[N];//子树大小
int Ans;//最后答案 
int s;//当前子树大小 
int cn[N];//cnt 容斥
int b[N];//u下哪一个子树 
int q[N];
int p=0;
bool cmp(node x,node y){
	return x.w<y.w;
}
void getroot(int u,int fa){//dfs+找重心 
	siz[u]=1;
	Max[u]=0;
	for(auto to:child[u]){
		int v=to.v;
		int w=to.w;
		if(v==fa||vis[v])continue;
		getroot(v,u);
		siz[u]+=siz[v];
		Max[u]=max(Max[u],siz[v]);
	}
	Max[u]=max(Max[u],s-Max[u]);
	if(Max[u]<Max[root]){
		root=u;
	}
}
void getdeep(int u,int fa){//找所有距离 
	dis[++cnt]={u,dd[u]};
	if(dep[u]>1){
		b[u]=b[fa];
	}else if(dep[u]==1){
		b[u]=u;
	}
	for(auto to:child[u]){
		int v=to.v;
		int w=to.w;
		if(vis[v]||v==fa)continue;
		dd[v]=dd[u]+w;
		dep[v]=dep[u]+1;
		getdeep(v,u);
	}
	return;
}
int getans(int u){//找经过当前点答案 
	dd[u]=dep[u]=s=0;
	p=0;
	cnt=0;
	b[u]=0;
	getdeep(u,0);
	sort(dis+1,dis+cnt+1,cmp);
	memset(cn,0,sizeof(cn));
	int l=1,r=cnt,ans=0;
	for(int i=2;i<=cnt;i++){
		cn[b[dis[i].v]]++;
	}
	while(l<r){
		if(dis[l].w+dis[r].w<=k){
			ans+=r-l-cn[b[dis[l].v]];
			l++;
			cn[b[dis[l].v]]--;
		}else{
			cn[b[dis[r].v]]--;
			r--;
		}
	}
	return ans;
}
void divide(int u){//分治 
	vis[u]=1;
	b[u]=u;
	Ans+=getans(u);
	memset(dep,0,sizeof(dep));
	memset(dd,0,sizeof(dd));
	for(auto to:child[u]){
		int v=to.v;
		int w=to.w;
		if(vis[v])continue;
		s=siz[v];
		Max[root=0]=INF;
		getroot(v,0);		
		divide(root);
	}
}
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	cin>>n;
	for(int i=1;i<n;i++){
		cin>>aa>>bb>>cc;
		add(aa,bb,cc);
	}
	cin>>k;
	s=n;
	Max[root=0]=INF;
	getroot(1,0);
	getroot(root,0);
	divide(root);
	cout<<Ans<<endl;
} 

完结撒花!!!

posted @ 2026-01-12 21:02  xwy114514  阅读(1)  评论(0)    收藏  举报