神秘分治 & 学习笔记

前言

个人认为分治是一种神秘的算法,尤其是你在调代码的时候。

  • 什么是分治?

分治其实是一种思想,通过将一个大范围的问题分成许多小范围问题来求解。
对,就是「分以治之」。
这难免会想到一些与之相似的算法,像二分、动态规划,但这其实还是有区别的。
至于具体怎么个区别,本蒟蒻语文水平有限,还是自己体会吧。

cdq分治

介绍

cdq分治最早由IOI金牌得主陈丹琦在高中整理并总结,cdq分治也因此而得名。
cdq分治并不是一种具体的算法,而是一种思想,有十分强大的扩展性。
其主要应用于以下方面:

  • 多维偏序问题
  • 动态规划优化转移

其主要思想是将一些复杂问题转化为偏序问题进行求解,同时cdq分治是一种离线思想,处理不了强制在线的问题,如果出现了强制在线的偏序问题那本蒟蒻只能洗洗睡了。

二维偏序

  • 停停停,还没有说什么是偏序呢。

其实偏序的定义说了也看不懂

偏序关系

\(R\) 是集合 \(A\) 上的一个二元关系,若 \(R\) 满足:

  • 自反性:\(\forall x\in A,xRx\)
  • 反对称性:\(\forall x,y\in A\)\(xRy,yRx\)\(x=y\)
  • 传递性:\(\forall x,y,z\in A\)\(xRy,yRz\)\(xRz\)

则称 \(R\)\(A\) 上的偏序关系,通常记作 \(≼\)

其实可以把 $R$ 理解成 $\le$、$\ge$ 之类的东西。

下面就开始正式的二位偏序了。

  • 为什么不从一维开始?
  • 因为一维没什么东西

最著名的二位偏序问题就是逆序对个数问题,即求解一个排列中满足:

  • 下标 \(i < j\)
  • \(a_i>a_j\)

这样的数对的对数,这也就是二维偏序中的那「二维」。
想必已经不是第一次看这样的题了,想一想这道题可以用什么方法做?

  • 树状数组/线段树
  • 归并排序

其实这些就是cdq分治的思想。

  • 对于树状数组,统计前面所有点对当前的贡献,我们按下标顺序依次填入(在其所对应的值上添加 \(1\)),在填入一个值之前,查询比当前值小的有多少个,并累计在答案中。
  • 对于归并排序,统计左区间对右区间的贡献,我们将一个区间 \([l,r]\)\([l,mid]\)\([mid+1,r]\) 分成两个子区间向下递归,回溯时,左右两个区间内部已经各自拍好序,用双指针同时扫两个区间,因为左区间的下标一定小于右区间的下标,所以只需要统计对于右区间的每一个数,在左区间内有多少个比它小的,统计在答案中即可。

其实使用归并排序或树状数组的前提都是其中一位偏序——下标是有序的,也就是说归并排序和树状数组都只能干掉其中的一维偏序。

从二位到三维

陌上花开(三维偏序)\(^{luoguP3810}\)

题目中的三维偏序是很显然的,那要怎么做呢?
既然归并排序和树状数组都能干掉一维偏序,那么它俩结合起来不就能干两维了吗,那剩下的一维怎么整?
直接排序就行了。
也就是说,对于三维偏序问题,对于每一维我们分别使用:

  1. sort排序
  2. 归并排序(cdq分治)
  3. 树状数组

就可以轻松干掉这道题了。
唯一注意的一点在排序的cmp函数!?

#include<bits/stdc++.h>
using namespace std;
const int NUM=1e6+10;

struct node{
	int s,c,m,id,op;
}q[NUM],tmp[NUM];
int ans[NUM];
int anss[NUM];
int n,k,tot;
int C[NUM];
void add(int x,int val){
	for(int i=x;i<=k;i+=(i&-i)) C[i]+=val;
}
int sum(int x){
	int ans=0;
	for(int i=x;i;i-=(i&-i)) ans+=C[i];
	return ans;
}

void cdq(int l,int r){
	if(l==r) return;
	int mid=(l+r)>>1;
	cdq(l,mid),cdq(mid+1,r);
	int i=l,j=mid+1,k=i;
	while(i<=mid&&j<=r){
		if(q[i].c<=q[j].c){
			if(!q[i].op) add(q[i].m,1);
			tmp[k++]=q[i++];
		}else{
			if(q[j].op) ans[q[j].id]+=sum(q[j].m);
			tmp[k++]=q[j++];
		}
	}
	while(j<=r){
		if(q[j].op) ans[q[j].id]+=sum(q[j].m);
		tmp[k++]=q[j++];
	}
	for(int p=l;p<i;++p) if(!q[p].op) add(q[p].m,-1);
	while(i<=mid){
		tmp[k++]=q[i++];
	}
	for(int i=l;i<=r;++i) q[i]=tmp[i];
}

int main(){
	cin>>n>>k;
	for(int i=1,x,y,z;i<=n;++i){
		cin>>x>>y>>z;
		q[++tot]={x,y,z,i,0};
		q[++tot]={x,y,z,i,1};
	}
	sort(q+1,q+1+tot,[](node x,node y){
		if(x.s!=y.s) return x.s<y.s;
		if(x.c!=y.c) return x.c<y.c;
		if(x.m!=y.m) return x.m<y.m;
		return x.op<y.op;
	});
	cdq(1,tot);
	for(int i=1;i<=tot;++i){
		if(q[i].op){
			anss[ans[q[i].id]-1]++;
		}
	}
	for(int i=0;i<n;++i){
		cout<<anss[i]<<'\n';
	}
	return 0;
}

从三维偏序到多维偏序

【模板】离线静态四维数点\(^{luoguP14957}\)

还在叠加。
上面说过,使用sort排序、归并排序、树状数组可以干掉三维,现在就只剩下了一维。
可不可以用线段树?
我不是把线段树和树状数组放一块了吗,它俩是一个东西。
不过貌似好像也许真的可以用树套树去做,不过本蒟蒻树套树是贺的。
既然树套树不行,可以cdq套cdq啊!
具体过程如下:

  1. 在主函数中案第一维排好序。
  2. 在cdq1中左区间打上标记 \(0\),右区间打上标记 \(1\),并按第二维排序,传入cdq2。
  3. 在cdq2中使用双指针扫描左右区间,当且仅当左区间的一个值的标记为 \(0\),才能对右区间做出贡献,将其添入树状数组,但且仅当右区间的一个值的标记为 \(1\) 才可以从树状数组中获取贡献并累计到答案中。

其实会了四维,五维、六维什么的也都不在话下了。

#include<bits/stdc++.h>
using namespace std;
const int NUM=10e5+10;
const int NUMM=100e5+10;

int n,m;
struct node{
	int x,y,x2,y2,op,id,tag;
}A[NUM],tmp[NUM];
int tot,num;
int cpy[NUMM],cnt;
int C[NUM];
int ans[NUM];
void add(int x,int val){
	for(int i=x;i<=cnt+10;i+=(i&-i)) C[i]+=val;
}
int sum(int x){
	int res=0;
	for(int i=x;i;i-=(i&-i)) res+=C[i];
	return res;
}
int lb(int x){
	return lower_bound(cpy+1,cpy+1+cnt,x)-cpy;
}
void cdq2(int l,int r){
	if (l == r) return;
	int mid=(l+r)>>1;
	cdq2(l,mid),cdq2(mid+1,r); 
	int i=l,j=mid+1,k=l;
	while(i<=mid&&j<=r){
		if(A[i].x2<=A[j].x2){
			if(!A[i].tag&&!A[i].op) add(A[i].y2,1);
			tmp[k++]=A[i++];
		}else{
			if(A[j].tag&&A[j].op) ans[A[j].id]+=sum(A[j].y2);
			tmp[k++]=A[j++];
		}
	}
	while(j<=r){
		if(A[j].tag&&A[j].op) ans[A[j].id]+=sum(A[j].y2);
		tmp[k++]=A[j++];
	}
	for(int p=l;p<i;++p) if(!A[p].tag&&!A[p].op) add(A[p].y2,-1);
	while(i<=mid) tmp[k++]=A[i++];
	for(int i=l;i<=r;++i) A[i]=tmp[i];
} 

void cdq1(int l,int r){
	if(l==r) return;
	int mid=(l+r)>>1;
	cdq1(l,mid),cdq1(mid+1,r);
	for(int i=l;i<=mid;++i) A[i].tag=0;
	for(int i=mid+1;i<=r;++i) A[i].tag=1;
	stable_sort(A+l,A+r+1,[](node x,node y){
		if(x.y!=y.y) return x.y<y.y;
		if(x.x2!=y.x2) return x.x2<y.x2;
		return x.y2<y.y2;
	});
	cdq2(l,r);
}

signed main(){
	cin>>n>>m;
	for(int i=1,x,y,x2,y2;i<=n;++i){
		cin>>x>>y>>x2>>y2;
		cpy[++cnt]=-y2;
		A[++tot]={x,y,-x2,-y2,0,0,0};
	}
	for(int i=1,x,y,x2,y2;i<=m;++i){
		cin>>x>>y>>x2>>y2;
		cpy[++cnt]=-y2;
		A[++tot]={x,y,-x2,-y2,1,i,0};
	}
	sort(cpy+1,cpy+1+cnt);
	cnt=unique(cpy+1,cpy+1+cnt)-cpy;
	for(int i=1;i<=tot;++i){
		A[i].y2=lb(A[i].y2);
	}
	stable_sort(A+1,A+1+tot,[](node x,node y){
		if(x.x!=x.y) return x.x<y.x;
		if(x.y!=y.y) return x.y<y.y;
		if(x.x2!=y.x2) return x.x2<y.x2;
		return x.y2<y.y2;
	});
	cdq1(1,tot);
	for(int i=1;i<=m;++i){
		cout<<ans[i]<<'\n';
	}
	return 0;
}

cdq分治优化DP

在cdq分治过程中,我们将一个大区间分成了两个小区间,然后考虑左区间对右区间做的贡献。
在DP过程中,我们也可以使用这样的思想,将左区间转移到右区间。
但盲目的转移会出现问题,下面我们举个例子:
现在有四个状态:\(1,2,3,4\)
假设状态转移方程形如:\(f_i=\sum_{j=1}^{i-1}f_j\)
下面模拟一下分治过程:

  1. \(1,2,3,4\) 分成 \(1,2\)\(3,4\)
  2. 向下走到 \(1,2\)
  3. \(1,2\) 分成 \(1\)\(2\)
  4. 回溯到 \(1,2\)
  5. \(1\rightarrow 2\)做一次转移。
  6. 回溯到 \(1,2,3,4\)
  7. 想右走到 \(3,4\)
  8. \(3,4\) 分成 \(3\)\(4\)
  9. 回溯到 \(3,4\)
  10. \(3\rightarrow 4\) 做一次转移。

停!
现在的 \(3\) 还什么都没有呢,怎么能转移到 \(4\) 呢?
这就是问题所在,在cdq分治优化DP的过程中,在左区间算完后,不能急着去递归右区间,应当先让左区间对右区间做转移,然后再递归右区间,这样才能保证转移不重不漏,这才是DP。

[TJOI2016 & HEOI2016] 序列\(^{luoguP4093}\)

对于DP的状态设计,个人感觉是没什么大问题的虽然我当时就没设计出来
\(f_i\) 表示以地 \(i\) 项结尾最大合法子序列长度,择有:

\[f_i=\max_{i<j,max_j\le a_,a_j\le min_i}\{f_j\}+1 \]

其实就是最长上升子序列,只不过多加了两条限制,这三条限制构成了三维偏序,难想到使用cdq优化转移,看看代码:

#include<bits/stdc++.h>
using namespace std;
const int NUM=1e6+10;
const int inf=0x3f3f3f3f;
#define int long long

int n,m;
struct node{
	int val,mx,mn,id;
	void operator = (int x){
		val=mx=mn=x;
	}
	void operator += (int x){
		mx=max(mx,x);
		mn=min(mn,x);
	} 
}A[NUM];
int C[NUM];
void add(int x,int val){
	for(int i=x;i<=NUM;i+=(i&-i)) C[i]=max(C[i],val);
}
int sum(int x){
	int ans=-inf;
	for(int i=x;i;i-=(i&-i)) ans=max(ans,C[i]);
	return ans;
}
void clear(int x){
	for(int i=x;i<=n;i+=(i&-i)) C[i]=-inf;
}
int f[NUM],p[NUM];

void cdq(int l,int r){
	if(l==r){
		f[l]=max(f[l],1ll);
		return;
	}
	int mid=(l+r)>>1;
	cdq(l,mid);
	for(int i=l;i<=r;++i){
		p[i]=i;
	}
	sort(p+l,p+mid+1,[](int x,int y){
		return A[x].mx<A[y].mx;
	});
	sort(p+mid+1,p+r+1,[](int x,int y){
		return A[x].val<A[y].val;
	});
	int i=l,j=mid+1;
	while(i<=mid&&j<=r){
		if(A[p[i]].mx<=A[p[j]].val){
			add(A[p[i]].val,f[p[i]]);i++;
		}else{
			f[p[j]]=max(f[p[j]],sum(A[p[j]].mn)+1);j++;
		}
	}
	while(j<=r){
		f[p[j]]=max(f[p[j]],sum(A[p[j]].mn)+1);j++;
	}
	for(int i=l;i<=mid;++i) clear(A[i].val);
	cdq(mid+1,r);
}

signed main(){
	memset(C,0xcf,sizeof(C));
	cin>>n>>m;
	for(int i=1,x;i<=n;++i){
		cin>>x;A[i]=x;A[i].id=i;
	}
	for(int i=1,x,y;i<=m;++i){
		cin>>x>>y;
		A[x]+=y;
	}
	cdq(1,n);
	int ans=-inf;
	for(int i=1;i<=n;++i){
		ans=max(ans,f[i]);
	}
	cout<<ans<<'\n';
	return 0;
}

注意转移的时候不能改变原数组,改变了要排序回去,这样才能保证转移是对的。

[DBOI2019] 德丽莎世界第一可爱\(^{luoguP5621}\)

不知哪位舰长出的四维偏序DP转移,动态转移方程是个人都能看出来:
\(f_i\) 表示消灭第 \(i\) 只崩坏兽所获得的的最大可爱值。

\[f_i=\max_{j<i,H_j\le H_i,E_j\le E_i,A_j\le A_i,D_j\le D_i}f_j+C_i \]

具体细节看代码:

#include<bits/stdc++.h>
using namespace std;
const int NUM=5e4+10;
const int inf=0x3f3f3f3f;
#define int long long
int T[NUM],cnt;
int n;
struct node{
	int health,energy,attack,defense,collapse,tag,id;
	void read(){
		cin>>health>>energy>>attack>>defense>>collapse;
		T[++cnt]=defense;
	}
}A[NUM],B[NUM],D[NUM];
int C[NUM];

void add(int x,int val){
	for(int i=x;i<=NUM-10;i+=(i&-i)) C[i]=max(C[i],val);
}
int que(int x){
	int ans=-inf;
	for(int i=x;i;i-=(i&-i)) ans=max(ans,C[i]);
	return ans;
}
void clear(int x){
	for(int i=x;i<=NUM-10;i+=(i&-i)) C[i]=-inf;
}

int f[NUM],p[NUM];

void sol(int l,int r){
	if(l==r) return;
	int mid=(l+r)>>1;
	sol(l,mid);
	for(int i=l;i<=r;++i) D[i]=B[i];
	auto cmp=[](node x,node y)->bool{
		if(x.attack!=y.attack) return x.attack<y.attack;
		return x.defense<y.defense;
	};
	stable_sort(D+l,D+mid+1,cmp),stable_sort(D+mid+1,D+r+1,cmp);
	int i=l,j=mid+1;
	while(i<=mid&&j<=r){
		if(D[i].attack<=D[j].attack){
			if(!D[i].tag)add(D[i].defense,f[D[i].id]);++i;
		}else{
			if(D[j].tag)f[D[j].id]=max(f[D[j].id],que(D[j].defense)+D[j].collapse);++j;
		}
	}
	while(j<=r){
		if(D[j].tag)f[D[j].id]=max(f[D[j].id],que(D[j].defense)+D[j].collapse);++j;
	}
	for(int k=l;k<i;++k) clear(D[k].defense);
	sol(mid+1,r);
}

void cdq(int l,int r){
	if(l==r){
		return;
	}
	int mid=(l+r)>>1;
	cdq(l,mid);
	for(int i=l;i<=r;++i) B[i]=A[i];
	for(int i=l;i<=mid;++i) B[i].tag=0;
	for(int i=mid+1;i<=r;++i) B[i].tag=1;
	stable_sort(B+l,B+r+1,[](node x,node y){
		if(x.energy!=y.energy) return x.energy<y.energy;
		if(x.attack!=y.attack) return x.attack<y.attack;
		return x.defense<y.defense;
	});
	sol(l,r);
	cdq(mid+1,r);
}

signed main(){
	cin>>n;
	for(int i=1;i<=n;++i){
		A[i].read();
		f[i]=A[i].collapse;
		A[i].id=i;
	}
	sort(T+1,T+1+cnt);
	cnt=unique(T+1,T+1+cnt)-T-1;
	for(int i=1;i<=n;++i){
		A[i].defense=lower_bound(T+1,T+1+cnt,A[i].defense)-T;
	}
	sort(A+1,A+1+n,[](node x,node y){
		if(x.health!=y.health) return x.health<y.health;
		if(x.energy!=y.energy) return x.energy<y.energy;
		if(x.attack!=y.attack) return x.attack<y.attack;
		if(x.defense!=y.defense) return x.defense<y.defense;
		return x.collapse>y.collapse;
	});
	cdq(1,n);
	int ans=-inf;
	for(int i=1;i<=n;++i){
		ans=max(ans,f[i]);
	}
	cout<<ans<<'\n';
	return 0;
}

评价:非常毒瘤好的题。

整体二分

介绍

二分答案自然是一种很好的算法,虽然这会使你的算法复杂度多加一个 \(log\),但在大多数情况都无伤大雅,但偏偏就有有些时候多加一个 \(log\) 就会死,这就不得不请出整体二分了。
如果说二分答案会让你的程序多加一个 \(log\),那么整体二分则是将那个 \(n\) 换成 \(log\),听起来非常厉害,在实现上其实是将全部寻问一次查完。

实现

其在实现上类似于cdq分治,将一个大区间分成两个小区间,并将寻问分到对应的区间中去。

【模板】可持久化线段树 2\(^{luoguP3834}\)

等等,这不是主席树的题吗?
寻问区间第 \(k\) 大整体二分也可以干呢,而且无论时间还是空间都比主席树更优,唯一的缺点可能就是整体二分是离线算法,而主席树则是在线算法。
这可能就是离线算法的特点了吧。
好了回归正题。
具体做法就是扫描属于当前值域的全部操作(注意在整体二分的时候,将原数列当成添加操作),先扫描添加操作,在扫描查询操作,并用树状数组维护区间元素个数。
设当前值域区间为 \([l,r]\),重点为 \(mid\)
如果当前操作时添加一个数:

  • 若当前元素在 \([l,mid]\) 之间,则将其填入树状数组,并放入左区间的临时数组。
  • 若当前元素在 \([mid+1,r]\) 之间,则直接将其放入右区间的临时数组。

如果当前操作时查询:
首先在树状数组中查询当前操作所要查询的区间的元素个数(当然,时小于 \(mid\) 的元素个数),记为 \(cnt\)

  • \(cnt\ge k\) 说明答案在左区间,将其放入左区间的临时数组。
  • \(cnt < k\) 说明答案在右区间,讲 \(k\) 减去 \(cnt\) ,并将其放入右区间临时数组(相当于在右区间查询 \(k-cnt\),根权值线段树查排名的值一个思路)。

最后别忘了:

  • 将临时数组中的数放到查询数组中。
  • 清空树状数组。

一个好理解但空间会爆炸的代码(vector存储):

#include<bits/stdc++.h>
using namespace std;
const int NUM=5e6+10;

struct oper{
	int t;
	int l,r,k,id;
}; 

int n,m;
int a[NUM],ans[NUM];
int C[NUM];
void add(int x,int val){
	for(int i=x;i<=n;i+=(i&-i)) C[i]+=val;
}
int sum(int x){
	int ans=0;
	for(int i=x;i;i-=(i&-i)) ans+=C[i];
	return ans;
}
int query(int l,int r){
	return sum(r)-sum(l-1);
}
void sol(int l,int r,vector<oper>& ops){
	if(ops.empty()) return;
	if(l==r){
		for(auto& op:ops){
			if(op.t==1){
				ans[op.id]=l;
			}
		}
		return;
	}
	int mid=(l+r)>>1;
	vector<oper> left,right;
	for(oper& op:ops){
		if(op.t==0){
			if(op.k<=mid){
				add(op.l,1);
				left.push_back(op);
			}else{
				right.push_back(op);
			}
		}
	}
	for(oper& op:ops){
		if(op.t==1){
			int cnt=query(op.l,op.r);
			if(cnt>=op.k){
				left.push_back(op);
			}else{
				op.k-=cnt;
				right.push_back(op);
			}
		}
	}
	for(oper& op:ops){
		if(op.t==0&&op.k<=mid) add(op.l,-1);
	}
	sol(l,mid,left);
	left.clear();
	sol(mid+1,r,right);
	right.clear();
}

int main(){
	
	cin>>n>>m;
	int minn=INT_MAX,maxx=INT_MIN;
	for(int i=1;i<=n;++i){
		cin>>a[i];
		minn=min(minn,a[i]);
		maxx=max(maxx,a[i]);
	}
	vector<oper> ops;
	for(int i=1;i<=n;++i){
		ops.push_back({0,i,0,a[i],0});
	}
	for(int i=1;i<=m;++i){
		int l,r,k;
		cin>>l>>r>>k;
		ops.push_back({1,l,r,k,i});
	}
	sol(minn,maxx,ops);
	for(int i=1;i<=n;++i){
		cout<<ans[i]<<'\n';
	}
	return 0;
}

整体二分的竞赛风写法(数组存储):

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

const int NUM=1e6+10;
const int inf=0x3f3f3f3f;

struct node{
	int l,r,v,id;
}q[NUM],tp[NUM];
int n,m,a[NUM],len;
int C[NUM],ans[NUM];
inline void add(int x,int val){
	for(int i=x;i<=n;i+=(i&-i)) C[i]+=val;
}
inline int sum(int x){
	int ans=0;
	for(int i=x;i;i-=(i&-i)) ans+=C[i];
	return ans;
}
void clear(int x){
	for(int i=x;i<=n;i+=(i&-i)) C[i]=0; 
}
inline void sol(int ql,int qr,int l,int r){
	if(l==r){
		while(ql<=qr){
			if(q[ql].id) ans[q[ql].id]=l;++ql;
		}
		return;
	}
	int mid=(l+r)>>1,il=ql,ir=qr;
	for(int i=ql;i<=qr;++i){
		if(!q[i].id){
			if(q[i].v<=mid) add(q[i].l,1),tp[il++]=q[i];
			else tp[ir--]=q[i];
		}else{
			int cnt=sum(q[i].r)-sum(q[i].l-1);
			if(cnt>=q[i].v) tp[il++]=q[i];
			else q[i].v-=cnt,tp[ir--]=q[i];
		}
	}
	for(int i=ql;i<il;++i){
		q[i]=tp[i];
		if(!q[i].id&&q[i].v<=mid) clear(q[i].l);
	}
	for(int i=il;i<=qr;++i){
		q[i]=tp[qr-i+il];
	}
	if(il!=ql) sol(ql,ir,l,mid);
	if(ir!=qr) sol(il,qr,mid+1,r);
}

int main(){
	cin>>n>>m;
	for(int i=1;i<=n;++i){
		cin>>a[i];
		q[i]={i,i,a[i],0};
	}
	sort(a+1,a+1+n);
	len=unique(a+1,a+1+n)-a-1;
	for(int i=1;i<=n;++i){
		q[i].v=lower_bound(a+1,a+1+len,q[i].v)-a;
	}
	for(int i=1;i<=m;++i){
		cin>>q[i+n].l>>q[i+n].r>>q[i+n].v;
		q[i+n].id=i;
	}
	sol(1,n+m,1,len);
	for(int i=1;i<=m;++i){
		cout<<a[ans[i]]<<'\n';
	}

	return 0;
}

提示
双倍经验:可怜的狗狗\(^{louguP1533}\)
你甚至连改都不用改

[POI2011] MET-Meteors\(^{luoguP3527}\)

虽然是个紫题,但也确实是个模版题。
我们将每个国家太空站的位置记成一个vector方便快速查询。
我们考虑对时间(陨石雨的波次)进行整体二分,每次分出一个 \(mid\) 将答案小于 \(mid\) 的国家忘左放,大于 \(mid\) 的往右放。
具体地,将时间小于 \(mid\) 的陨石波次放进树状数组中去,对于每个国家查询其在时间小于 \(mid\) 的陨石波次中能获得的陨石数量,能获得足够的陨石放左区间,否则放右区间。
最后,树桩数组在这里做的事区间修改、单点查询的事,对于环上的问题,稍作差分处理一下就好。
具体细节请看代码:

#include<bits/stdc++.h>
using namespace std;
const int NUM=3e5+10;
#define ll unsigned long long

struct ask{
	int id;
	ll val;
	vector<int> pos;
}A[NUM],tmpa[NUM];
struct oper{
	int l,r;
	ll val,k;
}O[NUM],tmpo[NUM];
int n,m,k;
ll ans[NUM];
ll C[NUM];
inline void add(int x,ll val){
	for(int i=x;i<=m;i+=(i&-i)) C[i]+=val;
}
inline ll sum(int x){
	ll ans=0;
	for(int i=x;i;i-=(i&-i)) ans+=C[i];
	return ans;
}
inline void update(int l,int r,ll val){
	if(r>=l) return add(l,val),add(r+1,-val),void();
	add(l,val);add(1,val);add(r+1,-val);
}

void sol(int l,int r,int al,int ar,int ql,int qr){
	if(l==r){
		for(int i=al;i<=ar;++i) ans[A[i].id]=l;
		return;
	}
	ll mid=(l+r)>>1;
	int il=ql,ir=qr,jl=al,jr=ar;
	for(int i=ql;i<=qr;++i){
		if(O[i].k<=mid){
			update(O[i].l,O[i].r,O[i].val);
			tmpo[il++]=O[i];
		}else{
			tmpo[ir--]=O[i];
		}
	}
	for(int i=al;i<=ar;++i){
		ll cnt=0;
		for(int x:A[i].pos) cnt+=sum(x);
		if(A[i].val<=cnt){
			tmpa[jl++]=A[i];
		}else{
			A[i].val-=cnt;
			tmpa[jr--]=A[i];
		}
	}
	for(int i=ql;i<il;++i){
		O[i]=tmpo[i];
		if(O[i].k<=mid) update(O[i].l,O[i].r,-O[i].val);
	}
	for(int i=il;i<=qr;++i) O[i]=tmpo[qr-i+il];
	for(int j=al;j<jl;++j) A[j]=tmpa[j];
	for(int j=jl;j<=ar;++j) A[j]=tmpa[ar-j+jl];
	if(il!=ql&&jl!=al) sol(l,mid,al,jr,ql,ir);
	if(ir!=qr&&jr!=ar) sol(mid+1,r,jl,ar,il,qr);
}

signed main(){
	cin>>n>>m;
	for(int i=1,x;i<=m;++i){
		cin>>x;
		A[x].pos.push_back(i);
	}
	for(int i=1,val;i<=n;++i){
		cin>>val;
		A[i].val=val,A[i].id=i;
	}
	cin>>k;
	for(int i=1,l,r,val;i<=k;++i){
		cin>>l>>r>>val;
		O[i]={l,r,val,i};
	}
	sol(1,k+1,1,n,1,k);
	for(int i=1;i<=n;++i){
		if(ans[i])cout<<ans[i]<<'\n';
		else cout<<"NIE\n";
	}
	return 0;
}

评价:这道题的空间限制事真死,卡了好几遍才过,讨论版里有证据。

带修整体二分

Dynamic Rankings\(^{luoguP2617}\)

其实也么什么东西,没有修改操作,我们会将原序列抽象为添加操作,并将其至于查询操作之前。
同样的,我们将修改操作与查询操作按照时间顺序进行整体二分,保证不打乱顺序即可。
特别地,为了方便操作,我们可以将修改操作抽象为删除操作和添加操作
具体请看代码:

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

const int NUM=2e6+10;
const int inf=0x3f3f3f3f;

struct node{
	int l,r,v,id,k;
}q[NUM],tp[NUM];
int n,m,a[NUM],len;
int C[NUM],ans[NUM];
inline void add(int x,int val){
	for(int i=x;i<=n;i+=(i&-i)) C[i]+=val;
}
inline int sum(int x){
	int ans=0;
	for(int i=x;i;i-=(i&-i)) ans+=C[i];
	return ans;
}
void clear(int x){
	for(int i=x;i<=n;i+=(i&-i)) C[i]=0; 
}
int b[NUM];
inline void sol(int ql,int qr,int l,int r){
	if(l==r){
		while(ql<=qr){
			if(q[ql].id) ans[q[ql].id]=l;++ql;
		}
		return;
	}
	int mid=(l+r)>>1,il=ql,ir=qr;
	for(int i=ql;i<=qr;++i){
		if(q[i].id==0){
			if(q[i].v<=mid) add(q[i].l,q[i].k),tp[il++]=q[i];
			else tp[ir--]=q[i];
		}else{
			int cnt=sum(q[i].r)-sum(q[i].l-1);
			if(cnt>=q[i].v) tp[il++]=q[i];
			else q[i].v-=cnt,tp[ir--]=q[i];
		}
	}
	for(int i=ql;i<il;++i){
		q[i]=tp[i];
		if(!q[i].id&&q[i].v<=mid) clear(q[i].l);
	}
	for(int i=il;i<=qr;++i){
		q[i]=tp[qr-i+il];
	}
	if(il!=ql) sol(ql,ir,l,mid);
	if(ir!=qr) sol(il,qr,mid+1,r);
}

int main(){
	cin>>n>>m;
	for(int i=1;i<=n;++i){
		cin>>a[i];
		q[i]={i,i,a[i],0,1};
	}
	int tot=n,pos=0;
	for(int i=1,x,y;i<=m;++i){
		char op;cin>>op;
		if(op=='Q'){
			tot++;
			cin>>q[tot].l>>q[tot].r>>q[tot].v;
			q[tot].id=++pos;
		}else{
			cin>>x>>y;
			q[++tot]={x,x,a[x],0,-1};
			a[x]=y;
			q[++tot]={x,x,a[x],0,1};
		}
	}
	sol(1,tot,0,1e9+10);
	
	for(int i=1;i<=pos;++i){
		cout<<ans[i]<<'\n';
	}
	return 0;
}

猫树分治

在学猫树分治前,应先学习猫树

介绍

其实猫树分治没多少东西,类似于前面的分治算法,在跑猫树的时候计算答案即可。

好吃的题目\(^{luoguP6240}\)

一道猫树套DP的题。
数据范围过大,预处理数组开不开?
于是我们考虑在跑猫树的时候计算答案。
对于区间背包的合并,可以这样做:

\[ans=\max_{j=0}^{t_i}\{f[l_i][j]+f[r_i][t_i-j]\} \]

#include <bits/stdc++.h>
using namespace std;
const int NUM1=6e4+10;
const int NUM2=2e2+10;
const int NUM3=2e5+10;
struct node{
	int l,r,t;
}q[NUM3];
int n,m;
int h[NUM3],w[NUM3];
int f[NUM1][NUM2];
int p[NUM3],s[NUM3],tot;
int ans[NUM3];

void sol(int l,int r,int tl,int tr){
	if(tl>tr) return;
	int mid=(l+r)>>1,tmid=tl-1;
	for(int i=0;i<=200;++i) f[mid][i]=0;
	for(int i=mid+1;i<=r;++i){
		for(int j=0;j<h[i];++j) f[i][j]=f[i-1][j];
		for(int j=h[i];j<=200;++j){
			f[i][j]=max(f[i-1][j],f[i-1][j-h[i]]+w[i]);
		}
	}
	for(int i=h[mid];i<=200;++i) f[mid][i]=w[mid];
	for(int i=mid-1;i>=l;--i){
		for(int j=0;j<h[i];++j) f[i][j]=f[i+1][j];
		for(int j=h[i];j<=200;++j){
			f[i][j]=max(f[i+1][j],f[i+1][j-h[i]]+w[i]);
		}
	}
	int tt=0;
	for(int i=tl;i<=tr;++i){
		int x=p[i];
		if(q[x].r<=mid) p[++tmid]=x;
		else if(q[x].l>mid) s[++tt]=x;
		else{
			int res=0;
			for(int j=0;j<=q[x].t;++j){
				res=max(res,f[q[x].l][j]+f[q[x].r][q[x].t-j]);
			}
			ans[x]=res;
		}
	}
	for(int i=1;i<=tt;++i) p[tmid+i]=s[i];
	tr=tmid+tt;
	sol(l,mid,tl,tmid);
	sol(mid+1,r,tmid+1,tr); 
}

signed main() {
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;++i){
		cin>>h[i];
	}
	for(int i=1;i<=n;++i){
		cin>>w[i];
	}
	for(int i=1;i<=m;++i){
		cin>>q[i].l>>q[i].r>>q[i].t;
		if(q[i].l==q[i].r){
			if(q[i].t>=h[q[i].l]) ans[i]=w[q[i].l];
		}else p[++tot]=i;
	}
	sol(1,n,1,tot);
	for(int i=1;i<=m;++i){
		cout<<ans[i]<<'\n';
	}
}

后记

感觉写博客也是件很累的事。
猫树分治那里一是没学太明白,而是累了写不动,过几天继续干。
累了,睡了。

posted @ 2026-07-23 21:59  LZYXT  阅读(7)  评论(0)    收藏  举报