点分治,点分树,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分治

花费了inf天终于看懂了,我真的哭死。

CDQ主要用于解决:

  • 解决和点对有关的问题。
  • 1D 动态规划的优化与转移。
  • 通过 CDQ 分治,将一些动态问题转化为静态问题。

学这个东西首先要掌握归并排序,大概就是这么一个分成三部分,只考虑其中一部分的答案,然后再合并的过程。

具体的,对于当前区间[L,R],设中间的数为mid,先分治[L,mid],[mid+1,R],那么对于这两个区间内部的所有答案我们都处理出来了,现在只用考虑横跨mid的情况,也就是左区间对右区间产生贡献的个数

先来考虑一下普通逆序对的问题!(逆序对本质就是一个二维偏序的问题,有两个维度的限制)

考虑当我们拿到当前这个区间[L,R],[L,mid]和[mid+1,R]的元素都各自是升序的,对于右区间的每个点i,通过双指针能找到左区间第一个比它大的位置j。那么以i结尾的逆序对有mid-j+1个,每次累计即可。

顺序对只需修改一下判断即可。

那么接下来考虑这种问题!

批注 2026-01-07 222535

这种类型的题就是根据操作时间和查询位置离线转化为偏序问题。然后再用一下前缀和的思想处理查询操作。大致思路如下:

首先明确时间是排序的第一关键字,就如同逆序对要满足i<j一样。一开始加入元素时就处理好。

批注 2026-01-09 221210

cdq内怎么写?

  • 考虑影响某次查询必须满足两个要求,第一个是修改时间早于此次查询,第二个是修改位置必须在此次查询位置之前。
  • 那么左右两个区间就拿位置再排一下序。
  • 然后用双指针,将影响右区间的修改操作用树状数组处理好。然后在查询操作对应的答案上进行累加。

批注 2026-01-09 221340

当然我并不是喜欢这样的cdq写法的。(只不过懒得自己再写一遍了)所以请移步下面的练习部分 ↓

例题:【模板】三维偏序 / 陌上花开

三维限制。第一位提前处理好,第二维cdq时左右排序,第三维用树状数组查询即可。

注意去重操作:这道题没有规定j<i!!也就是完全相同的元素可以相互贡献!!看看代码里是怎么处理的吧。

点击查看代码
#include<bits/stdc++.h>
#define lowbit(x) x&(-x)
using namespace std;
const int maxn=2e5+5;
int n, k, c[maxn], num[maxn];
struct flower{
	int a, b, c, cnt, ans;
}a[maxn], b[maxn];
void add(int x,int y)
{
	while(x<=k)
	{
		c[x]+=y;
		x+=lowbit(x);
	} 
}
int query(int x)
{
	int sum=0;
	while(x)
	{
		sum+=c[x];
		x-=lowbit(x);
	}
	return sum;
}
bool cmp1(flower a,flower b)
{
	if(a.a==b.a)
	{
		if(a.b==b.b) return a.c<b.c;
		else return a.b<b.b;
	}
	else return a.a<b.a;
}
bool cmp2(flower a,flower b)
{
	if(a.b==b.b) return a.c<b.c;
	else return a.b<b.b;
}
void CDQ(int l,int r)
{
	if(l==r) return ;
	int mid=(l+r)>>1;
	CDQ(l, mid);
	CDQ(mid+1, r);
	sort(b+l, b+mid+1, cmp2);
	sort(b+mid+1, b+r+1, cmp2);
	int j=l, sum=0;
	for(int i=mid+1;i<=r;i++)
	{
		while(b[i].b>=b[j].b&&j<=mid)
		{
			add(b[j].c, b[j].cnt);
			j++;
		}
		b[i].ans+=query(b[i].c);
	}
	for(int i=l;i<j;i++)
		add(b[i].c, -b[i].cnt);
}
int main()
{
	cin>>n>>k;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i].a>>a[i].b>>a[i].c;
	}
	sort(a+1, a+n+1, cmp1);
	int top=0, tot=0;
	for(int i=1;i<=n;i++)
	{
		top++;
		if(a[i].a!=a[i+1].a||a[i].b!=a[i+1].b||a[i].c!=a[i+1].c)
		{
			b[++tot].a=a[i].a, b[tot].b=a[i].b, b[tot].c=a[i].c;
			b[tot].cnt=top;
			top=0;
		}
	}
	CDQ(1, tot);
	for(int i=1;i<=tot;i++)
	{
		num[b[i].ans+b[i].cnt-1]+=b[i].cnt;
	}
	for(int i=0;i<n;i++) cout<<num[i]<<endl;
	return 0;
}

练1:[Violet] 天使玩偶/SJY摆棋子

看到曼哈顿距离,我第一想到的就是将它们拆开。然后分四种情况讨论。但是显然只有ax+ay-bx-by这种情况比较好做,而这种情况对应的是所有点在所求点的左下角。因为这对于每一个点都是固定的值,可以很快求出距离。这部分点就直接拿cdq分治做了。

然后考虑剩下的三部分,这些其实只需要将坐标系翻折一下。其实就是用 最大范围 - 横坐标或纵坐标。

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define lowbit(x) (x&(-x))
using namespace std;
const int maxn=1e6+5;
int n, m, maxx, maxy, ans[maxn];
struct node{
	int x, y, t, opt;
	inline bool operator < (const node &b) const
    {
        return x<=b.x;
    }
}a[maxn], b[maxn], p[maxn];
int mx[maxn];
void add(int pos,int val)
{
	while(pos<=maxy)
	{
		mx[pos]=max(mx[pos], val);
		pos+=lowbit(pos);
	}
}
int query(int pos)
{
	int res=0;
	while(pos>0)
	{
		res=max(res, mx[pos]);
		pos-=lowbit(pos);
	}
	return res;
}
void clear(int pos)
{
	while(pos<=maxy)
	{
		if(mx[pos]) mx[pos]=0;
		pos+=lowbit(pos);
	}
}
bool cmp(node a,node b)
{
	return a.x<=b.x;
}
void CDQ(int l,int r)
{
	if (l > r) return;
	if(l==r) return ;
	int mid=(l+r)>>1;
	CDQ(l, mid), CDQ(mid+1, r);
//	sort(a+l, a+mid+1, cmp);
//	sort(a+mid+1, a+r+1, cmp);
	int j=l;
//			cout<<mid<<" awa"<<endl;
	for(int i=mid+1;i<=r;i++)
	{
		if(a[i].opt==2)
		{
			while(j<=mid&&a[j].x<=a[i].x)
			{
				if(a[j].opt==1)
				{
					add(a[j].y, a[j].x+a[j].y);
				}
				j++;
			}
			int tmp=query(a[i].y);
			if(tmp) ans[a[i].t]=min(a[i].x+a[i].y-tmp, ans[a[i].t]);
		}
	}
	for(int i=l;i<j;i++) if(a[i].opt==1) clear(a[i].y);
	merge(a+l, a+mid+1, a+mid+1, a+r+1, p+l);
    for(int i=l;i<=r;i++) a[i]=p[i];
}
void del()
{
	int lx=0, ly=0;
	m=0;
	for(int i=1;i<=n;i++)
	{
		if(a[i].opt==2) lx=max(lx, a[i].x), ly=max(ly, a[i].y);
	}
	for(int i=1;i<=n;i++)
	{
		if(a[i].x<=lx&&a[i].y<=ly) p[++m]=a[i]; 
	}
	for(int i=1;i<=m;i++) a[i]=p[i];
}
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i].x>>a[i].y;
		a[i].x++, a[i].y++;
		a[i].t=i;
		a[i].opt=1;
		maxx=max(maxx, a[i].x);
		maxy=max(maxy, a[i].y);
	}
	while(m--)
	{
		int opt, x, y;
		cin>>opt>>x>>y;
		x++, y++;
		a[++n]=(node){x, y, n, opt};
		maxx=max(maxx, x);
		maxy=max(maxy, y);
	}
	for(int i=1;i<=n;i++) b[i]=a[i];
	memset(ans, 0x3f3f3f3f, sizeof(ans));
	maxx++, maxy++;
//	cout<<"qwq";
	del();
	CDQ(1, m);
	for(int i=1;i<=n;i++)
	{
		a[i]=b[i];
		a[i].x=maxx-a[i].x;
	}
	del();
	CDQ(1, m); 
	for(int i=1;i<=n;i++)
	{
		a[i]=b[i];
		a[i].y=maxy-a[i].y;
	}
	del();
	CDQ(1, m);
	for(int i=1;i<=n;i++)
	{
		a[i]=b[i];
		a[i].x=maxx-a[i].x, a[i].y=maxy-a[i].y;
//		cout<<a[i].x<<" "<<a[i].y<<endl;
	}
	del();
	CDQ(1, m);
	for(int i=1;i<=n;i++)
	{
		if(b[i].opt==2) cout<<ans[i]<<endl;
	}
	return 0;
}

练2:[BalkanOI 2007] Mokia 摩基亚

这道题就是上面写带修情况的cdq分治。只不过这道题转化到二维上了,用到的是二维前缀和,剩下的就没什么太大区别啦。

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=2e6+5;
int n, m, maxx, maxy, ans[maxn], idx;
struct node{
	int x, y, t, opt, val;
}a[maxn];
#define lowbit(x) (x&(-x))
int sum[maxn];
void add(int pos,int val)
{
	while(pos<=m)
	{
		sum[pos]+=val;
		pos+=lowbit(pos);
	}
}
int query(int pos)
{
	int res=0;
	while(pos>0)
	{
		res+=sum[pos];
		pos-=lowbit(pos);
	}
	return res;
}
bool cmp(node a,node b)
{
	if(a.x==b.x) return a.y<b.y;
	return a.x<b.x;
}
void CDQ(int l,int r)
{
	if(l==r) return ;
	int mid=(l+r)>>1;
	CDQ(l, mid), CDQ(mid+1, r);
	sort(a+l, a+mid+1, cmp);
	sort(a+mid+1, a+r+1, cmp);
	int j=l;
//			cout<<mid<<" awa"<<endl;
	for(int i=mid+1;i<=r;i++)
	{
		while(j<=mid&&a[j].x<=a[i].x)
		{
			if(a[j].opt==1)
			{
				add(a[j].y, a[j].val);
			}
			j++;
		}
		if(a[i].opt==2)
		{
			ans[a[i].t]+=query(a[i].y);
		}
		else if(a[i].opt==3)
		{
			ans[a[i].t]-=query(a[i].y);
		}
	}
	for(int i=l;i<j;i++) if(a[i].opt==1) add(a[i].y, -a[i].val);
}
signed main()
{
//	freopen("1.in","r",stdin);
//	freopen("my.out","w",stdout);
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	int op;
	while(cin>>op)
	{
		int x, y, val;
		if(op==3) break;
		if(op==0) cin>>m;
		else if(op==1)
		{
			cin>>x>>y>>val;
			x++, y++;
			a[++n]=(node){x, y, idx, op, val};
		}
		else if(op==2)
		{
			idx++;
			int xx, yy;
			cin>>x>>y>>xx>>yy;
			x++, y++, xx++, yy++;
			a[++n]=(node){xx, yy, idx, op, 0};
			a[++n]=(node){x-1, y-1, idx, op, 0};
			a[++n]=(node){x-1, yy, idx, op+1, 0};
			a[++n]=(node){xx, y-1, idx, op+1, 0};
		}
	}
	m++;
	CDQ(1, n);
	for(int i=1;i<=idx;i++)
	{
		cout<<ans[i]<<endl;
	}
	return 0;
}

练3:[HEOI2016/TJOI2016] 序列

首先这道题可以推出一个简单的dp,也就长这个样子:

批注 2026-01-10 170556

会发现它也是满足一个偏序关系的dp,于是拿cdq分治来优化。

设第i为最大值为bi,最小值为ci,原本值为ai。那就要求:

i<j
ai<cj
bi<aj

同样的思路,i<j提前已处理好,那么只需在cdq时满足后两个式子即可。我们可以让左区间按照a排序,右区间按照c排序,然后双指针解决第二个限制,用树状数组解决第三个限制。注意,在给左右两个区间排序时,一定是根据偏序符号两边的值分别排序,例如左边全按a排,右边全按c排。一定要记住,cdq里只需解决左边对右边产生贡献的部分

然后还有一个很重要的点是不能像原来一样把左右两边都分治了再解决左对右的。因为正常dp顺序是按照从前往后来的。

点击查看代码
#include<bits/stdc++.h>
#define lowbit(x) x&(-x)
using namespace std;
const int maxn=1e5+5;
int n, m, a[maxn], dp[maxn], b[maxn], bb[maxn], ans, c[maxn];
struct node{
	int a, b, c, id;//yuan da xiao
}t[maxn];
void add(int x,int y)
{
	while(x<=n)
	{
		c[x]=max(c[x], y);
		x+=lowbit(x);
	} 
}
int query(int x)
{
	int sum=0;
	while(x)
	{
		sum=max(sum, c[x]);
		x-=lowbit(x);
	}
	return sum;
}
void clear(int x)
{
	while(x<=n)
	{
		c[x]=0;
		x+=lowbit(x);
	} 
}
bool cmp(node a,node b)
{
	return a.a<b.a;
} 
bool cmp1(node a,node b)
{
	return a.c<b.c;
}
bool cmp2(node a,node b)
{
	return a.id<b.id;
}
void CDQ(int l,int r)
{
	if(l==r) 
	{
		dp[l]=max(dp[l], 1);
		return ;
	}
	int mid=(l+r)>>1;
	CDQ(l, mid);
	sort(t+l, t+mid+1, cmp);
	sort(t+mid+1, t+r+1, cmp1);
	int j=l;
	for(int i=mid+1;i<=r;i++)
	{
		while(t[i].c>=t[j].a&&j<=mid)
		{
			add(t[j].b, dp[t[j].id]);
			j++;
		}
		dp[t[i].id]=max(dp[t[i].id], query(t[i].a)+1);
	}
	for(int i=l;i<=mid;i++)
	{
		clear(t[i].b);
	}
	sort(t+mid+1, t+r+1, cmp2);
	CDQ(mid+1, r);
}
int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
		bb[i]=b[i]=a[i];
	}
	while(m--)
	{
		int x, y;
		cin>>x>>y;
		b[x]=max(b[x], y);
		bb[x]=min(bb[x], y);
	} 
	for(int i=1;i<=n;i++)
	{
		t[i].a=a[i];
		t[i].b=b[i];
		t[i].c=bb[i];
		t[i].id=i;
	}
	CDQ(1, n);
	for(int i=1;i<=n;i++) ans=max(ans, dp[i]);
	cout<<ans;
	return 0;
} 

总结一下cdq分治,先转化为偏序问题(一般都有3个),然后分别处理这3层限制,只需考虑左对右,双指针左指针记录左边产生的影响,在右指针记录对答案的贡献。

整体二分

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