分治专题

1. 根号分治

1.1. 算法简介

根号分治,就是在预处理与询问的复杂度之间寻找平衡的一个算法。通常以根号作为问题规模的分界线,规模小于根号的询问可以 nn 预处理求出,而回答一次规模为 Bn 的询问的时间只需要 nBn,那么整个题目就可以做到 nn

1.2. 例题

I. CF797E Array Queries

题意简述:给出 {ai},多次询问给出 p,k,求至少执行多少次 pp+ap+k 才能使 p>n

注意到如果 k>n 那么答案必定不大于 n,那么对于所有位置预处理出所有 kn 的答案,若 k>n 直接暴力查询即可。时间复杂度 O(nn)

/*
	Powered by C++11.
	Author : Alex_Wei.
*/

#include <bits/stdc++.h>
using namespace std;

const int N=1e5+5;
const int B=333;

int n,m,b,a[N],s[N][B];

int main(){
	cin>>n,b=sqrt(n);
	for(int i=1;i<=n;i++)cin>>a[i];
	for(int i=1;i<=b;i++)for(int j=n;j;j--)
		s[j][i]=j+a[j]+i>n?1:(s[j+a[j]+i][i]+1);
	cin>>m;
	for(int i=1;i<=m;i++){
		int p,k; cin>>p>>k;
		if(k<=b)cout<<s[p][k]<<endl;
		else{
			int ans=0;
			while(p<=n)ans++,p+=a[p]+k;
			cout<<ans<<endl;
		}
	}
	return 0;
}

II. *CF1039D You Are Given a Tree

题意简述:给出一棵树,对每个 k[1,n],求出最多能找出多少条没有公共点的至少经过 k 个点的链。

注意到若 k>n 则答案一定不大于 n(怎么和上一题那么像,笑)。那么对于 1kn,直接暴力树形 DP。然后再枚举 1ansn,不过这次枚举的是链的条数,即答案。显然答案单调不升,于是二分出答案为 ansk 的区间即可(实际上不需要右端点,只需要左端点)。

树形 DP 求链上经过的点 k 的答案:该部分比较类似 赛道修建,不过也有一些区别:因为一个点只能被一条链经过(而不是赛道修建中的一条边),于是分两种情况讨论:记 mx1,mx2i 的儿子所传入的最长的两条链(所经过点的个数),若 mx1+mx2+1k,那么显然是将 i 与它的两个儿子配成一条链,答案加 1;否则将 mx+1 传上去到 fai 即可。这样一次 DP 就是 O(n) 的。

因此,总时间复杂度为 O(nnlogn)

卡常技巧:将每个节点的父亲预处理出来,然后按照 dfs 序排序,可以直接循环树形 DP,不需要 dfs。

/*
	Powered by C++11.
	Author : Alex_Wei.
*/


//#pragma GCC optimize(3)

#include <bits/stdc++.h>
using namespace std;

#define mem(x,v) memset(x,v,sizeof(x))

namespace IO{
	char buf[1<<21],*p1=buf,*p2=buf,obuf[1<<23],*O=obuf;
	#ifdef __WIN32
		#define gc getchar()
	#else
		#define gc (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<20,stdin),p1==p2)?EOF:*p1++)
	#endif
	#define pc(x) (*O++=x)
	#define flush() fwrite(obuf,O-obuf,1,stdout)

	inline int read(){
		int x=0,sign=0; char s=gc;
		while(!isdigit(s))sign|=s=='-',s=gc;
		while(isdigit(s))x=(x<<1)+(x<<3)+(s-'0'),s=gc;
		return sign?-x:x;
	}
	void print(ll x) {if(x>9)print(x/10); pc(x%10+'0');}
}

using namespace IO;

const int N=1e5+5;

int ed,ed2,hd[N],nxt[N<<1],to[N<<1];
pii nw[N];
void add(int u,int v){
	nxt[++ed]=hd[u],hd[u]=ed,to[ed]=v;
}
int n,p,cnt,ans[N];

void dfs0(int id,int f){
	for(int i=hd[id];i;i=nxt[i]){
		if(to[i]==f)continue;
		nw[++ed2]={id,to[i]},dfs0(to[i],id);
	}
}
int dfs(int id){
	int mx=0,mx2=0;
	for(int i=hd[id];i;i=nxt[i]){
		int v=dfs(to[i]);
		if(v>=p){cnt++; return 0;}
		if(v>=mx)mx2=mx,mx=v;
		else if(v>=mx2)mx2=v;
	} if(mx+mx2+1>=p){cnt++; return 0;}
	return mx+1;
} int run(int x){
	cnt=0,p=x,dfs(1);
	return cnt;
}

int main(){
	cin>>n;
	for(int i=1;i<n;i++){
		int a=read(),b=read();
		add(a,b),add(b,a);
	} int m=sqrt(n*log2(n));
	dfs0(1,0),mem(hd,0),mem(nxt,0),ed=0;
	for(int i=1;i<n;i++)add(nw[i].fi,nw[i].se);
	for(int i=1;i<=m;i++)ans[i]=run(i);
	for(int i=1,pre=n+1;i<=n/m+1;i++){
		int l=1,r=pre;
		while(l<r){
			int m=(l+r>>1)+1;
			if(run(m)>=i)l=m;
			else r=m-1;
		} for(int j=l+1;j<pre;j++)ans[j]=i-1; pre=l+1;
	} for(int i=1;i<=n;i++)cout<<ans[i]<<endl;
	return flush(),0;
}

2. cdq 分治

人类智慧算法。

在一个序列中,需要计算满足某些限制的点对 (i,j)(这里 i,j 都表示位置)对答案的贡献,通常这些点对都有 O(n2) 个。cdq 分治的核心思想就是将所有需要计算贡献的点对 (i,j) 分成三类:第一类 i,j[1,mid];第二类 i,j(mid,n];第三类 i[1,mid],j(mid,n]。这样一来就可以先递归处理第一、二类点对的答案,再运用一些方法快速求第三类的答案。

2.1. 例题

I. P3810 【模板】三维偏序(陌上花开)

题意简述:对每个 d,求使 ajai,bjbi,cjci,ijj 的个数有 d 个的 i 的个数。

首先去重,cdq 一般解决不了有重复元素的问题,除非重复元素之间不算贡献。接着将所有点按 ai,bi,ci 分别为第一、二、三关键字从小到大排序。

这样做,排除了 ai 对答案的限制。因为右区间的任何一个点都不会对左区间中的任何一个点有贡献。这样一来,需要求的就变成了对右区间的每个点 i,求左区间的所有点 j 中,满足 bjbi,cjcij 有多少个。

先将区间内部的点按照 bi,ci 分别为第一、二关键字从小到大排序,那么对于每个 i,可能符合条件(bjbi)的 j 一定是左区间的一段随着 i 的增大单调不缩的前缀。对于一段前缀,求有多少个 j 满足 cjci 就是树状数组的拿手好戏了。

视值域与序列大小同阶(离散化一下即可),则时间复杂度为 O(nlog2n)

一些注意点:

  • 树状数组在添加 / 删除时权值为点的个数而不是 1
  • 别忘了考虑重复元素之间的贡献,即最终答案还要加上该重复元素个数 1
#include<bits/stdc++.h>
using namespace std;

const int N=1e5+5;

int n,m,k,f[N];
struct pt{
	int a,b,c,ans,cnt;
}d[N],u[N];
bool cmp1(pt a,pt b){return a.a!=b.a?a.a<b.a:a.b!=b.b?a.b<b.b:a.c<b.c;}
bool cmp2(pt a,pt b){return a.b!=b.b?a.b<b.b:a.c<b.c;}

int c[N<<1];
void add(int x,int v){while(x<=k)c[x]+=v,x+=x&-x;}
int query(int x){int s=0; while(x)s+=c[x],x-=x&-x; return s;}

void solve(int l,int r){
	if(l==r)return;
	int m=l+r>>1,le=l;
	solve(l,m),solve(m+1,r);
	sort(u+l,u+m+1,cmp2),sort(u+m+1,u+r+1,cmp2);
	for(int i=m+1;i<=r;i++){
		while(le<=m&&u[le].b<=u[i].b)add(u[le].c,u[le].cnt),le++;
		u[i].ans+=query(u[i].c);
	} for(int i=l;i<le;i++)add(u[i].c,-u[i].cnt);
}

int main(){
	cin>>n>>k;
	for(int i=1;i<=n;i++)d[i].a=read(),d[i].b=read(),d[i].c=read();
	sort(d+1,d+n+1,cmp1);
	for(int i=1;i<=n;i++){
		if(d[i].a!=d[i-1].a||d[i].b!=d[i-1].b||d[i].c!=d[i-1].c)u[++m]=d[i];
		u[m].cnt++;
	} solve(1,m);
	for(int i=1;i<=m;i++)f[u[i].ans+u[i].cnt-1]+=u[i].cnt;
	for(int i=1;i<=n;i++)print(f[i-1]),pc('\n');
	return flush(),0;
}

II. P4755 Beautiful Pair

首先对其进行 cdq 分治,设当前区间为 [l,r]m=l+r2

对于每个位置 i,若 i[l,m] 则记 sufi=maxj=imaj,若 i[m+1,r] 则记 prei=maxj=m+1iaj

分别考虑最大值在 [l,m] 之间与在 [m+1,r] 之间的情况:若最大值在左侧,则枚举 i[l,m],找到右侧的分界点 p 使得对于 j[m+1,p] 都有 prejsufi,那么查询 [m+1,p] 有多少个 j 使得 ajsufiai(不等号右边是定值),这个可以用主席树或者 BIT 做到。反之同理。

别忘了离散化。注意最大值在右边时要找分界点 p 使得对于 j[p,m] 都有 sufj<prei,而不是 ,因为后者会多加上最大值在两边都出现的情况,而这种情况在考虑最大值在左边时已经计算过。

#include<bits/stdc++.h>
using namespace std;

#define ll long long

const int N=1e5+5;

ll n,ans,c,a[N],b[N],d[N],suf[N],pre[N];
ll node,R[N],ls[N<<5],rs[N<<5],val[N<<5];
void modify(int pre,ll &x,int l,int r,int p){
	val[x=++node]=val[pre]+1,ls[x]=ls[pre],rs[x]=rs[pre];
	if(l==r)return;
	int m=l+r>>1;
	if(p<=m)modify(ls[pre],ls[x],l,m,p);
	else modify(rs[pre],rs[x],m+1,r,p);
} ll query(int l,int r,int p,int x,int y){
	if(l==r)return val[y]-val[x];
	int m=l+r>>1;
	if(p<=m)return query(l,m,p,ls[x],ls[y]);
	return val[ls[y]]-val[ls[x]]+query(m+1,r,p,rs[x],rs[y]);
}

ll solve(int l,int r){
	if(l==r)return b[a[l]]==1;
	ll m=l+r>>1,ans=solve(l,m)+solve(m+1,r);
	for(int i=m+1;i<=r;i++)pre[i]=max(pre[i-1],a[i]);
	for(int i=m;i>=l;i--)d[m-i+1]=suf[i]=max(suf[i+1],a[i]);
	for(int i=l;i<=m;i++){
		int p=upper_bound(pre+m+1,pre+r+1,suf[i])-pre-1;
		if(p>m){
			int nd=upper_bound(b+1,b+c+1,b[suf[i]]/b[a[i]])-b-1;
			if(nd)ans+=query(1,c,nd,R[m],R[p]);
		}
	} for(int i=m+1;i<=r;i++){
		int p=m+1-(lower_bound(d+1,d+m-l+2,pre[i])-d-1);
		if(p<=m){
			int nd=upper_bound(b+1,b+c+1,b[pre[i]]/b[a[i]])-b-1;
			if(nd)ans+=query(1,c,nd,R[p-1],R[m]);
		}
	}
	for(int i=l;i<=r;i++)pre[i]=suf[i]=0;
	return ans;
}

int main(){
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i],b[i]=a[i];
	sort(b+1,b+n+1),c=unique(b+1,b+n+1)-b-1;
	for(int i=1;i<=n;i++){
		a[i]=lower_bound(b+1,b+c+1,a[i])-b;
		modify(R[i-1],R[i],1,c,a[i]);
	}
	cout<<solve(1,n)<<endl;
	return 0;
}

3. 猫树分治

3.1. 算法简介

posted @   qAlex_Weiq  阅读(1906)  评论(0编辑  收藏  举报
编辑推荐:
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示