点分治和cdq分治

点分治

前置知识:树的重心

void dfs(int u,int fa)
{
	siz[u]=1;
	int maxx=0;
	for(int i=head[u];i;i=edge[i].next)
	{
		int v=edge[i].to;
		if(v==fa) continue;
		dfs(v, u);
		siz[u]+=siz[v];
		maxx=max(maxx, siz[v]);
	}
	ans=min(ans, max(maxx, n-siz[u]));
}

定义

点分治就是将一棵树不断分治为不同子树,然后递归处理问题。一般点分治适用于 在树上求解对长度有一定限制的路径个数等问题。点分治将路径分为经过根不经过根两类。在当前以rt的子树内,只需求出经过rt的路径数,剩下的递归到它的子树求解。

例:【模板】点分治 1

可能判judge那个地方有点抽象,但是点分治除了分治操作以外,都是纯纯的暴力。当时看bobo课件真的震惊到我了,代码真的不难写。
注意在清空一些数组的时候不能直接用 memset,而应将之前占用过的位置加入一个队列中,进行清空,这样才能保证时间复杂度。
点分治过程中,每一层的所有递归过程合计对每个点处理一次,假设共递归 h 层,则总时间复杂度为 O(hn)。
若我们 每次选择子树的重心作为根节点,可以保证递归层数最少(感觉这个有点像splay操作),时间复杂度为 \(O(n\log n)\)

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e4+5, inf=1e7+5;
int n, m, ask[maxn], del[maxn], d[maxn], siz[maxn], cnt, dis[maxn], sum, mxs, root;
int head[maxn], edgenum, q[inf], judge[inf], ans[maxn];
struct edge{
    int next;
    int to;
    int w;
}edge[maxn<<1];

void add(int from,int to,int w)
{
    edge[++edgenum].next=head[from];
    edge[edgenum].to=to;
    edge[edgenum].w=w;
    head[from]=edgenum;
}
void getrt(int u,int fa)
{
	siz[u]=1;
	int maxx=0;
	for(int i=head[u];i;i=edge[i].next)
	{
		int v=edge[i].to;
		if(v==fa||del[v]) continue;
		getrt(v, u);
		siz[u]+=siz[v];
		maxx=max(maxx, siz[v]);
	}
	maxx=max(maxx, sum-siz[u]);
	if(maxx<mxs)
	{
		mxs=maxx, root=u;
	}
}
void getdis(int u,int fa)
{
	dis[++cnt]=d[u];
	for(int i=head[u];i;i=edge[i].next)
	{
		int v=edge[i].to;
		if(v==fa||del[v]) continue;//注意! 
		d[v]=d[u]+edge[i].w;
		getdis(v, u);
	}
}
void calc(int u)
{
	del[u]=judge[0]=1;//注意! 
	int p=0;
	for(int i=head[u];i;i=edge[i].next)
	{
		int v=edge[i].to;
		if(del[v]) continue;
		cnt=0;
		d[v]=edge[i].w;
		getdis(v, u);
		for(int j=1;j<=cnt;j++)
			for(int k=1;k<=m;k++)
				if(ask[k]>=dis[j])
					ans[k]|=judge[ask[k]-dis[j]];
		for(int j=1;j<=cnt;j++)
			if(dis[j]<inf)
				q[++p]=dis[j], judge[q[p]]=1;
	}
	for(int i=1;i<=p;i++) judge[q[i]]=0;//注意!点分治的每篇代码这里都要清空 
}
void divide(int u)
{
	calc(u);
	del[u]=1;
	for(int i=head[u];i;i=edge[i].next)
	{
		int v=edge[i].to;
		if(del[v]) continue;
		mxs=sum=siz[v];
		getrt(v, 0);
		divide(root);//注意!这里是root不是v 
	}
}
signed main()
{
	cin>>n>>m;
	for(int i=1;i<n;i++)
	{
		int u, v, w;
		cin>>u>>v>>w;
		add(u, v, w), add(v, u, w);
	}
	for(int i=1;i<=m;i++)
	{
		cin>>ask[i];
	}
	mxs=sum=n;
	getrt(1, 0);
	getrt(root, 0);//注意!要跑两遍 
	divide(root);
	for(int i=1;i<=m;i++)
	{
		if(ans[i]) cout<<"AYE"<<endl;
		else cout<<"NAY"<<endl;
	}
	return 0;
}

练习

练1:[bzoj1468]Tree

这里放的不是洛谷上的链接因为这个题和洛谷上的那个不一样qwq。洛谷数据还是太水了点。。
洛谷上直接枚举判断就可以水过去了,但复杂度较高,考虑用树状数组优化,用树状数组维护每个值的个数,修改、查询比较基础。

练2:聪聪可可

比较套路的题,算出两点间路径和为3的倍数的路径数量ans,那么他赢的概率即为 \((ans*2+n)/(n*n)\)

练3:[bzoj2599][IOI2011]Race

看起来还是比较套路地写法,但是比较值得注意的就是q数组的变化,可以像例题里的judge数组一样记录权值为()的路径的变数最小值,答案存储时不断更新。

像这样子:

for(int i=head[u];i;i=edge[i].next)
{
	int v=edge[i].to;
	if(del[v]) continue;
	cnt=0;
	d[v]=edge[i].w;
	getdis(v, u, 1);
	for(int j=1;j<=cnt;j++)
		if(K>=dis[j])
			ans=min(ans, dep[j]+q[K-dis[j]]);
	for(int j=1;j<=cnt;j++)
		if(dis[j]<=K)
			q[dis[j]]=min(dep[j], q[dis[j]]);
}

记得清空哦。

练4:采药人的路径

这个题还是蛮有意思的。设阴的边权为-1,阳的为1,那么满足阴阳平衡的即为路径和为0的。这个就可以判断整条路是否符合要求。现在考虑这个休息站,记每个点到根的距离为dis[i],加入从这个点到根的路径上有另一个节点满足dis[j]=dis[i],就给i这一节点打上标记,分为以下4种情况:

感觉这个标记真的挺巧妙的,主要就是要观察这个休息站的特点(即连接的两点dis值相同),发现这一性质后就能好想一点了。

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=2e5+5, inf=0x3f3f3f3f;
int n, K, ask[maxn], del[maxn], d[maxn], siz[maxn], cnt, sum, mxs, root, ans;
int head[maxn], edgenum, q[maxn], t1, t2, t[maxn][2], tot, dp[maxn][2], vis[maxn];
struct edge{
    int next;
    int to;
    int w;
}edge[maxn<<1];

inline void add(int from,int to,int w)
{
    edge[++edgenum].next=head[from];
    edge[edgenum].to=to;
    edge[edgenum].w=w;
    head[from]=edgenum;
}
inline void getrt(int u,int fa)
{
	siz[u]=1;
	int maxx=0;
	for(int i=head[u];i;i=edge[i].next)
	{
		int v=edge[i].to;
		if(v==fa||del[v]) continue;
		getrt(v, u);
		siz[u]+=siz[v];
		maxx=max(maxx, siz[v]);
	}
	maxx=max(maxx, sum-siz[u]);
	if(maxx<mxs)
	{
		mxs=maxx, root=u;
	}
}
inline void getdis(int u,int fa)
{
	if(vis[d[u]+n]) t[++t2][1]=d[u]+n;
	else t[++t1][0]=d[u]+n;
	vis[d[u]+n]++;
	for(int i=head[u];i;i=edge[i].next)
	{
		int v=edge[i].to;
		if(v==fa||del[v]) continue;
		d[v]=d[u]+edge[i].w;
		getdis(v, u);
	}
	vis[d[u]+n]--;
}
inline void calc(int u)
{
	del[u]=1;
	int p=0;
	q[0]=0;
	for(int i=head[u];i;i=edge[i].next)
	{
		int v=edge[i].to;
		if(del[v]) continue;
		cnt=t1=t2=0;
		d[v]=edge[i].w;
		getdis(v, u);
		for(int j=1;j<=t1;j++)
		{
			int tmp=t[j][0];
			if(tmp==n) ans+=dp[tmp][0]+dp[tmp][1];
			else ans+=dp[2*n-tmp][1];
		}
		for(int j=1;j<=t2;j++)
		{
			int tmp=t[j][1];
			if(tmp==n) ans+=dp[tmp][0]+dp[tmp][1]+1;
			else ans+=dp[2*n-tmp][0]+dp[2*n-tmp][1];
		}
		for(int j=1;j<=t1;j++)
			dp[t[j][0]][0]++, q[++p]=t[j][0];
		for(int j=1;j<=t2;j++)
			dp[t[j][1]][1]++, q[++p]=t[j][1];
	}
	for(int k=1;k<=p;k++) dp[q[k]][0]=dp[q[k]][1]=0;
}
inline void divide(int u)
{
	calc(u);
	del[u]=1;
	for(int i=head[u];i;i=edge[i].next)
	{
		int v=edge[i].to;
		if(del[v]) continue;
		mxs=sum=siz[v];
		getrt(v, 0);
		divide(root);
	}
}
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin>>n;
	for(int i=1;i<n;i++)
	{
		int u, v, w;
		cin>>u>>v>>w;
		if(w==0) w=-1;
		add(u, v, w), add(v, u, w);
	}
	mxs=sum=n;
	getrt(1, 0);
	getrt(root, 0);
	divide(root);
	cout<<ans;
	return 0;
}

练5:「BJOI2017」树的难题

当时想到了一些合并的思路什么的,显然从一个根分治的话,最棘手的问题就是这个根和它子节点所连的第一条边的颜色。然后

动态点分治/点分树

在做了极大的思想斗争后决定一定要写博客证明自己学会了。

动态,即带修,和淀粉质的区别就是①有修改②多次查询。因为在淀粉质中我们是先求重心然后处理问题,那么基于这种思路我们将原树的重心作为新的rt,对于rt的每一个直接相连的子节点 构成的子树中的 重心与rt相连,然后这样递归下去重构出一棵树,这棵树就叫做点分树

点分树还是和点分治一样有一定暴力性质的,就比如暴力跳点分树修改。在点分树上的每个节点维护一些值(一般有两个:当前节点(u)答案、其父节点中u子树内的答案),查询时暴力跳点分树,根据维护的值计算答案。然而一般的题因为题目中的一些限制,需要用线段树、前缀和等优化,所以码量还是很大的!

点分树性质:

  • 点分树最大深度是log级别的。
  • 点分树上两点的lca在原树上两点之间的路径上。

例:[BZOJ3730]点分树 | 震波(模板)

注意到题目中的 距离不超过k 这一限制,再注意到k的范围居然是1e5,我们想到了线段树。
批注 2025-07-02 214940

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e5+5, inf=1e18;
int n, m, a[maxn];
int head[maxn], edgenum;
struct edge{
    int next;
    int to;
    int w;
}edge[maxn<<1];
inline void add(int from,int to,int w)
{
    edge[++edgenum].next=head[from];
    edge[edgenum].to=to;
    edge[edgenum].w=w;
    head[from]=edgenum;
}
struct LCA{
	int dep[maxn], f[maxn][20];
	void dfs(int u,int fa)
	{
		dep[u]=dep[fa]+1;
		f[u][0]=fa;
		for(int i=1;i<=19;i++)
		{
			f[u][i]=f[f[u][i-1]][i-1];
		}
		for(int i=head[u];i;i=edge[i].next)
		{
			int v=edge[i].to;
			if(v==fa) continue;
			dfs(v, u);
		}
	}
	int lca(int u,int v)
	{
		if(dep[u]<dep[v]) swap(u, v);
		for(int i=19;i>=0;i--)
		{
			if(dep[f[u][i]]>=dep[v]) u=f[u][i];
		}
		if(u==v) return v;
		for(int i=19;i>=0;i--)
		{
			if(f[u][i]!=f[v][i])
			{
				u=f[u][i], v=f[v][i];
			}
		}
		return f[u][0];
	}
	int getdis(int x,int y)
	{
		return dep[x]+dep[y]-2*dep[lca(x, y)];
	}
}lca;
struct tree{
	int idx, root[maxn];
	struct seg_tree{
		int l, r, sum;
	}tr[maxn*40];
	void pushup(int id)
	{
		tr[id].sum=tr[tr[id].l].sum+tr[tr[id].r].sum;
	}
	void update(int &id,int l,int r,int d,int v)
	{
		if(!id) id=++idx;
		if(l==r)
		{
			tr[id].sum+=v;
			return ;
		}
		int mid=(l+r)>>1;
		if(d<=mid) update(tr[id].l, l, mid, d, v);
		else update(tr[id].r, mid+1, r, d, v);
		pushup(id);
	}
	int query(int id,int l,int r,int x,int y)
	{
		if(!id||r<x||l>y) return 0;
		if(x<=l&&r<=y) return tr[id].sum;
		int mid=(l+r)>>1;
		return query(tr[id].l, l, mid, x, y)+query(tr[id].r, mid+1, r, x, y);
	}
}sg, ch;
struct pointree{
	int root, sum, siz[maxn], maxx[maxn], del[maxn];
	void getrt(int u,int fa)
	{
		siz[u]=1;
		maxx[u]=0;
		for(int i=head[u];i;i=edge[i].next)
		{
			int v=edge[i].to;
			if(v==fa||del[v]) continue;
			getrt(v, u);
			siz[u]+=siz[v];
			maxx[u]=max(maxx[u], siz[v]);
		}
		maxx[u]=max(maxx[u], sum-siz[u]);
		if(maxx[u]<maxx[root]) root=u;
	} 
	void getroot(int u,int size)
	{
		maxx[root]=sum=size;
		getrt(u, 0);
		getrt(root, 0);
	}
	int dis[maxn][20], dep[maxn], f[maxn];
	void build_sg(int u,int fa,int rt,int d)
	{
		sg.update(sg.root[rt], 0, n, d, a[u]);
		for(int i=head[u];i;i=edge[i].next)
		{
			int v=edge[i].to;
			if(v==fa||del[v]) continue;
			build_sg(v, u, rt, d+1);
		}
	}
	void build_ch(int u,int fa,int rt,int d)
	{
		ch.update(ch.root[rt], 0, n, d, a[u]);
		for(int i=head[u];i;i=edge[i].next)
		{
			int v=edge[i].to;
			if(v==fa||del[v]) continue;
			build_ch(v, u, rt, d+1); 
		} 
	}
	void build(int u)
	{
		del[u]=1;
		build_sg(u, 0, u, 0);
		for(int i=head[u];i;i=edge[i].next)
		{
			int v=edge[i].to;
			if(del[v]) continue;
			getroot(v, siz[v]);
			build_ch(v, 0, root, 1);
			f[root]=u;
			dep[root]=dep[u]+1;
			build(root);
		}
	}
	void init()
	{
		getroot(1, n);
		build(root);
		lca.dfs(1, 0);
		for(int i=1;i<=n;i++)
		{
			for(int j=i;j;j=f[j])
			{
				dis[i][dep[i]-dep[j]]=lca.getdis(i, j);
			}
		}
	}
	int query(int x,int y)
	{
		int res=sg.query(sg.root[x], 0, n, 0, y);
		for(int i=x;f[i];i=f[i])
		{
			int d=dis[x][dep[x]-dep[f[i]]];
			res+=sg.query(sg.root[f[i]], 0, n, 0, y-d);
			res-=ch.query(ch.root[i], 0, n, 0, y-d);
		}
		return res;
	}
	void update(int x,int y)
	{
		sg.update(sg.root[x], 0, n, 0, y-a[x]);
		for(int i=x;f[i];i=f[i])
		{
			int d=dis[x][dep[x]-dep[f[i]]];
			sg.update(sg.root[f[i]], 0, n, d, y-a[x]);
			ch.update(ch.root[i], 0, n, d, y-a[x]);
		}
	}
}t;

signed main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
	}
	for(int i=1;i<n;i++)
	{
		int u, v;
		cin>>u>>v;
		add(u, v, 1), add(v, u, 1);
	}
	t.init();
	int lst=0;
	while(m--)
	{
		int opt, x, y;
		cin>>opt>>x>>y;
		x^=lst, y^=lst;
		if(opt==0) 
		{
			lst=t.query(x, y);
			cout<<lst<<endl;
		}
		else t.update(x, y), a[x]=y;
	}
	return 0;
}

练习记得补上

CDQ分治

posted @ 2025-05-25 20:58  zhouyiran2011  阅读(30)  评论(0)    收藏  举报