整体二分笔记

先浅浅的谈一谈二分答案(如果会二分答案请直接调到整体二分部分)

二分答案

二分答案就是在答案满足单调性的时候优化 O ( N ) O(N) O(N) 的枚举答案暴力变为指数级的 O ( log ⁡ n ) O(\log_n) O(logn)(有可能不止 O ( N ) O(N) O(N) 因为还有判断操作所以大部分暴力复杂度均为 O ( M N ) O(MN) O(MN) 甚至更高)适用于题目中让求最大\最小值问题且答案不唯一且有单调性。

以枚举最小值为例
伪代码

int l=1 , r=答案最大的边界 ;// R 不判断是否合规
while(l<=r){
	int mid=(l+r)>>1 ;   //等同于(l+r)/2 ;
  if(mid满足答案要求) r = mid ; // 也可以为 r=mid-1 但是要单独用一个变量来存储 mid 当然也可以直接输出 r-1
  else l = mid+1 ; // 这里是不能写作 l=mid 否则会进入死循环
}
cout << r ;

先来一道例题试试水

例0

P1902 刺杀大使

这是一个很经典的利用答案的单调性枚举答案最小值的一道题目,判断答案就用dfs不会炸。

代码(可能有亿点丑)

#include<bits/stdc++.h>
using namespace std ;
long long n , m , a[1234][1234] , ans=0 ;
bool t[1234][1234] , ant ;
inline void dfs(long long x,long long y,long long num){
	if(x<1||y<1||x>n||y>m||t[x][y]||a[x][y]>num||ant) return ;
	if(x==n){ant=1 ; return;}
	t[x][y] = 1 ;
	dfs(x+1,y,num) ;
	dfs(x-1,y,num) ;
	dfs(x,y+1,num) ;
	dfs(x,y-1,num) ;
}
bool check(int num){
	ant = 0 ;
	memset(t,0,sizeof(t)) ;
	dfs(1,1,num) ;
	return ant ;
}
int main(){
	cin >> n >> m ;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			scanf("%lld",a[i]+j) ;
	int l=1 , r=10000 ;
	while(l<=r){
		int mid = (l+r)>>1 ;
		if(check(mid)){
			r = mid-1 ;
			ans = mid ;
		}else{
			l = mid+1 ;
		}
	}
	cout << ans ;
	return 0 ;
}

看了整体二分前提二分答案,现在让我们来看看整体二分怎么做。(可以去参考一下线段树二分)

整体二分(朴素)

整体二分,其实上是在 M M M 个询问中离线处理答案的算法,差不多算是在线使用 M M M 次二分答案的简便方法,所以整体二分的本质就是在同时二分 M M M 个答案,我们先定义一个函数 s o l v e ( l , r , L , R ) solve(l,r,L,R) solve(l,r,L,R) 其中 l l l r r r 表示目前的答案在这个区间内, L L L R R R 表示是目前操作数组 a i a_i ai 满足在这个区间内的操作,在整体二分的过程中因为插入的数字在哪个区间会影响答案的准确,所以在整体二分过程中可能还需要减(或者其他)贡献。

例如:给定一个序列( 1 ≤ 1\le 1 序列长度 ≤ 1 0 6 \le 10^6 106),让你求其中 l l l r r r K K K 小的值。显然询问只有一次所以有两种做法:

  1. 纯暴力:单独提出区间后排序并输出排序后的第 K K K 个值。排序用 sort 复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn)
  2. 二分答案:check:在 l l l r r r 区间内比他小的个数。复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn)

相信各位都会这两种方法各位都会使用那我们将题面的询问改为 Q Q Q 次( 1 ≤ Q ≤ 1 0 6 1\le Q \le 10^6 1Q106)。

很明显前面的两种方法都无法在 1 s 1s 1s 之内完成运行,显然我们会发现一个事情。暴力无法优化而二分答案的 mid 会有很多的重复且区间为静态区间,我们就可以将这些 m i d mid mid 进行类记忆化操作,让这些 mid 对整个查找答案的贡献不重复。

显然有个直接的问题并不是所有的 mid 相同的询问的 l l l r r r 是相同的,所以我们需要使用一个树状数组存在当前序列 a i a_i ai 是否小于当前数字。

但是如果直接按照这样的思路写的话,复杂度会直接变成 O ( n 2 ) O(n^2) O(n2) 。所以我们还要对其进行优化。

询问、mid 都优化完了,那么序列是否可以优化呢?

思考一个问题:如果当前的 mid=5 那么序列中的一个数字 a=10 他是否对答案会产生贡献?

显然是不会产生贡献的,所以我们可以得到一个结论:在二分的过程中不止需要二分询问,还需要将序列 a i a_i ai 带着走,并在当前二分的是序列数字的时候查看是否修改树状数组的值。

用以上得出的思路我们就可以写出一个 O ( n log ⁡ 3 n ) O(n \log^3n) O(nlog3n) 的非递归式整体二分。(着3个 l o g log log 分比为:二分、树状数组、sort(因为我们要保证该操作是按照输入顺序进行二分的,并且还要将其按照 mid 分类从而保证复杂度))。

那么这时静态区间,那如果是动态区间呢?

我们可以引用前面将 a i a_i ai 带着二分的结论我们可以得到一个新的思路,若修改这个数字也就是将被覆盖的数字通过在二分过程中减去他对答案的贡献,之后在重新按照初设值的方法在加入进二分数组。

着就是正常整体二分的一个例子思路,其余题目都可以通过类似思路将其写出,读者可以自行完成例0并对照代码进行理解,理解透彻之后自行完成例2并再次理解。(在定义存储二分操作数组大小时一定要慎重,因为它不一定是大小为 N N N 有可能是 2 N 2N 2N 甚至更大)

例0

主席树版子

思考可以得出二分答案的写法,之后将其一起二分就为整体二分(在整体二分中会出现查询右区间但是左区间又有数字会对其产生贡献所以需要减(具体情况具体分析)去他们对答案的贡献)

代码

#include<bits/stdc++.h>
#define inf 2147483647
#define lowbit(x) ((x)&(-x))
#define int long long
using namespace std ;
int n , m , cnt , ans[2112345] , tr[412345] ;
struct node{
	int l , r , k , opt , i ;
}a[2234516],ql[2112345],qr[2121345];
long long ask(int x){
	int sum=0 ;
	while(x){
		sum += tr[x] ;
		x -= lowbit(x) ;
	}
	return sum ;	
}
void change(int x,int s){
	while(x<=cnt){
		tr[x] += s ;
		x += lowbit(x) ;
	}
}
void solve(int l,int r,int n,int m){
	if(n>m) return ;
	if(l==r){
		for(int i=n;i<=m;i++)
			if(a[i].opt==2) ans[a[i].i] = l ;
		return ;
	}
	int mid=(l+r)>>1 , tot1=0 , tot2=0 ;
	for(int i=n;i<=m;i++){
		if(a[i].opt==1){
			if(a[i].l<=mid){
				ql[++tot1] = a[i] ;
				change(a[i].i,1) ;
			}else
				qr[++tot2] = a[i] ;
		}else{
			int x=ask(a[i].r)-ask(a[i].l-1) ;
			if(a[i].k<=x){
				ql[++tot1] = a[i] ;
			}else{
				a[i].k -= x ;
				qr[++tot2] = a[i] ;
			}
		}
	}
	for(int i=1;i<=tot1;i++){
		a[i+n-1] = ql[i] ;
		if(ql[i].opt==1)
			change(ql[i].i,-1) ;
	}
	for(int i=1;i<=tot2;i++) a[tot1+n+i-1] = qr[i] ;
	solve(l,mid,n,n+tot1-1) ;
	solve(mid+1,r,n+tot1,m) ;
}
signed main(){
	cin >> n >> m ;
	for(int i=1;i<=n;i++){
		scanf("%lld",&a[i].l) ;
		a[i].opt = 1 ;
		a[i].i = i ;
	}
	cnt = n ;
	for(int i=1;i<=m;i++){
		cnt++ ;
		scanf("%lld%lld%lld",&a[cnt].l,&a[cnt].r,&a[cnt].k) ;
		a[cnt].i = i ;
		a[cnt].opt = 2 ;
	}
	solve(-inf,inf,1,cnt) ;
	for(int i=1;i<=m;i++){
		printf("%lld\n",ans[i]) ;
	}
	return 0 ;
}

例1

[国家集训队] 矩阵乘法

十分简单的一道整体二分的板子,只是需要可以维护二维的数据结构进行维护。

代码

#include<bits/stdc++.h>
#define int long long 
#define lowbit(x) ((x)&(-x))
using namespace std ;
int n , q , temp[511][512] , tr[512][512] , cnt=0 , ans[61234] ;
struct node{
	int a1 , b1 , a2 , b2 , k , opt , i ;
}a[9123456],q1[9123456],q2[9123456];
void change(int x,int y,int num){
	for(int i=x;i<=n;i+=lowbit(i))
		for(int j=y;j<=n;j+=lowbit(j))
		tr[i][j] += num ;
}
int ask(int x,int y){
	int sum=0 ;
	for(int i=x;i;i-=lowbit(i))
		for(int j=y;j;j-=lowbit(j))
			sum += tr[i][j] ;
	return sum ;
}
void solve(int l,int r,int L,int R){
	if(L>R) return ;
	if(l==r){
		for(int i=L;i<=R;i++)
			if(a[i].opt==2) ans[a[i].i] = l  ;
		return ;
	}
	int mid=(l+r)>>1 , tot1=0 , tot2=0 ;
	for(int i=L;i<=R;i++){
		if(a[i].opt==1){
			if(a[i].k<=mid){
				change(a[i].a1,a[i].b1,1) ;
				q1[++tot1] = a[i] ;
			}else
				q2[++tot2] = a[i] ;
		}else{
			int x=ask(a[i].a2,a[i].b2)-ask(a[i].a1-1,a[i].b2)-ask(a[i].a2,a[i].b1-1)+ask(a[i].a1-1,a[i].b1-1) ;
			if(a[i].k<=x)
				q1[++tot1] = a[i] ;
			else{
				a[i].k -= x ;
				q2[++tot2] = a[i] ;
			}
		}
	}
	for(int i=1;i<=tot1;i++){
		a[L+i-1] = q1[i] ;
		if(q1[i].opt==1) change(q1[i].a1,q1[i].b1,-1) ;
	}
	for(int i=1;i<=tot2;i++)
		a[L+tot1+i-1] = q2[i] ;
	solve(l,mid,L,L+tot1-1) ;
	solve(mid+1,r,L+tot1,R) ;
}
signed main(){
	cin >> n >> q ;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++){
			cnt++ ;
			scanf("%lld",temp[i]+j) ;
			a[cnt].opt = 1 ;
			a[cnt].k = temp[i][j] ;
			a[cnt].a1 = i ;
			a[cnt].b1 = j ;
		}
	for(int i=1;i<=q;i++){
		cnt++ ;
		scanf("%lld%lld%lld%lld%lld",&a[cnt].a1,&a[cnt].b1,&a[cnt].a2,&a[cnt].b2,&a[cnt].k) ;
		a[cnt].opt = 2 ;
		a[cnt].i = i ;
	}
	solve(-1e17,1e17,1,cnt) ;
	for(int i=1;i<=q;i++)
		printf("%lld\n",ans[i]) ;
	return 0 ;
}

例2

P3527 [POI2011] MET-Meteors
这道题是十分经典的一道整体二分的题目,这里就不进行讲解。

代码

#include<bits/stdc++.h>
#define lowbit(x) ((x)&(-x))
#define int __int128
using namespace std ;
int n , m , ans[312345] , temp[312345] , somek[312345] , tr[6123456] , k , cnt=0 ;
vector<long> o[312345] ;
struct node{
	int l , r , opt , k , i ;
}a[6123415],q1[3112345],q2[3121345],somea[3112345];
void change(int l,int r,int w){
	while(l<=m){
		tr[l] += w ;
		l += lowbit(l) ;
	}
	r++ ;
	while(r<=m){
		tr[r] -= w ;
		r += lowbit(r) ;
	}
}
int ask(int x){
	int sum=0;
	while(x>0){
		sum += tr[x];
		x -= lowbit(x);
	}
	return sum ;
}
inline void read(int &x){
    bool f=1;x=0;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-') f=!f;ch=getchar();}
    while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
    x=(f?x:-x);return;
}
inline void write(int x){
    if(x<0) putchar('-'),x=-x;
    if(x>9) write(x/10);
    putchar(x%10+'0');return;
}
void solve(int l,int r,int L,int R){
	if(L>R) return ;
	if(l==r){
		for(int i=L;i<=R;i++)
			if(a[i].opt==2&&ans[a[i].i]!=-1) ans[a[i].i] = l ;
		return ;
	}
	int mid=(l+r)>>1 , tot1=0 , tot2=0 ;
	for(int i=L;i<=R;i++){
		if(a[i].opt==1){
			if(a[i].i<=mid){
				if(a[i].l>a[i].r){
					change(a[i].l,m,a[i].k) ;
					change(1,a[i].r,a[i].k) ;
				}else
					change(a[i].l,a[i].r,a[i].k) ;
				q1[++tot1] = a[i] ;
			}else
				q2[++tot2] = a[i] ;
		}else{
			int sum=0 ;
			for(auto x:o[a[i].i])
				sum += ask(x) ;
			if(a[i].k<=sum){
				q1[++tot1] = a[i] ;
			}else
				a[i].k -= sum , q2[++tot2] = a[i] ;
		}
	}
	for(int i=1;i<=tot1;i++){
		a[L+i-1] = q1[i] ;
		if(q1[i].opt==1){
			if(q1[i].l>q1[i].r){
				change(q1[i].l,m,-q1[i].k) ;
				change(1,q1[i].r,-q1[i].k) ;
			}else{
				change(q1[i].l,q1[i].r,-q1[i].k) ;
			}
		}
	}
	for(int i=1;i<=tot2;i++)
		a[tot1+L+i-1] = q2[i] ;
	solve(l,mid,L,L+tot1-1) ;
	solve(mid+1,r,L+tot1,R) ;
}
int find(int num){
	int sum=0 ;
	for(auto x:o[num])
		sum += ask(x) ;
	return sum ;
}
signed main(){
	read(n) ;
	read(m) ;
	for(int i=1;i<=m;i++){
		int temp ;
		read(temp) ;
		o[temp].push_back(i) ;
	}
	for(int i=1;i<=n;i++)
		read(temp[i]) ;
	read(k) ;
	for(int i=1;i<=k;i++){
		read(a[i].l) ;
		read(a[i].r) ;
		read(a[i].k) ;
		a[i].opt = 1 ;
		a[i].i = i ;
		somea[i] = a[i] ;
	}
	cnt = k ;
	for(int i=1;i<=n;i++){
		cnt++ ;
		somek[i] = temp[i] ;
		a[cnt].i = i ;
		a[cnt].k = temp[i] ;
		a[cnt].opt = 2 ;
	}
	for(int i=1;i<=k;i++)
		if(somea[i].l>somea[i].r){
			change(somea[i].l,m,somea[i].k) ;
			change(1,somea[i].r,somea[i].k) ;
		}else
			change(somea[i].l,somea[i].r,somea[i].k) ;
	for(int i=1;i<=n;i++)
		if(find(i)<somek[i]) ans[i] = -1 ;
	memset(tr,0,sizeof(tr)) ;
	solve(1,k,1,cnt) ;
	for(int i=1;i<=n;i++,putchar('\n'))
		if(ans[i]==-1) printf("NIE") ;
	    else write(ans[i]) ;
	return 0 ;
}

例3

[THUPC2017] 天天爱射击
这道题(以我的思路)我们可以想一下他要求什么?我们会发现这道题的答案不好二分所以我们转换一下题意,变成求 M M M
操作后统计 N N N 块木板分别是在那一次被击碎的数量,建议最好提前预处理一下那些木板是不会被击碎的,之后我们就可以定义函数 s o l v e ( l , r , L , R ) solve(l,r,L,R) solve(l,r,L,R) 其中 l l l r r r 表示的当前木板可能会再这个区间内碎掉,然后以求 K K K 小值的方法去做,这道题就做完了。值得注意的一点:这道题卡线段树…………

顺便给一个区间查区间求和的树状数组推导过程。( s u m i sum_i sumi 前缀和数组, c i c_i ci 差分数组, a i a_i ai 正常序列)

a i = ∑ j = 1 i c i a_i=\sum_{j=1}^i c_i ai=j=1ici

s u m i = ∑ j = 1 i a i = ∑ j = 1 i ∑ k = 1 j c k = ∑ j = 1 i c j ∗ j sum_i=\sum_{j=1}^i a_i=\sum_{j=1}^i\sum_{k=1}^j c_k = \sum_{j=1}^ic_j*j sumi=j=1iai=j=1ik=1jck=j=1icjj

(相信区间大家可以一次自推,我就不写了

代码

#include<bits/stdc++.h>
#define int long long
#define lowbit(x) ((x)&(-x))
using namespace std ;
const int N=212345 ;
int n , m , tot=0 , ans[N] , cnt[N] , tr[N<<2] ;
struct node{
	int l , r , opt , i , x ;
}a[N<<1],temp1[N],q1[N<<1],q2[N<<1];
void change(int x,int w){
	while(x<=n){
		tr[x] += w ;
		x += lowbit(x) ;
	}
}	
int ask(int x){
	int sum=0 ;
	while(x>0){
		sum += tr[x] ;
		x -= lowbit(x) ;
	}
	return sum ;
}
void solve(int l,int r,int L,int R){
	if(L>R) return ;
	if(l==r){
		for(int i=L;i<=R;i++)
			if(a[i].opt==2&&ans[a[i].i]!=-1) ans[a[i].i] = l ;
		return ;
	}
	int mid=(l+r)>>1 , tot1=0 , tot2=0 ;
	for(int i=L;i<=R;i++){
		if(a[i].opt==1){
			if(a[i].i<=mid) q1[++tot1] = a[i] , change(a[i].l,1) ;
			else q2[++tot2] = a[i] ;
		}else{
			int x=ask(a[i].r)-ask(a[i].l-1) ;
			if(a[i].x<=x) q1[++tot1] = a[i] ;
			else a[i].x -= x ,q2[++tot2] = a[i] ;
		}
	}
	for(int i=1;i<=tot1;i++){
		a[i+L-1] = q1[i] ;
		if(q1[i].opt==1) change(q1[i].l,-1) ;
	}
	for(int i=1;i<=tot2;i++) a[i+L+tot1-1] = q2[i] ;
	solve(l,mid,L,L+tot1-1) ;
	solve(mid+1,r,L+tot1,R) ;
}
signed main(){
	cin >> n >> m ;
	for(int i=1;i<=n;i++){
		scanf("%lld%lld%lld",&temp1[i].l,&temp1[i].r,&temp1[i].x) ;
		temp1[i].i = i ;
		temp1[i].opt = 2 ;
	}
	for(int i=1;i<=m;a[tot].opt=1,a[tot].i=i,i++)
		scanf("%lld",&a[++tot].l) ;
	for(int i=1;i<=n;i++)
		a[++tot] = temp1[i] ;
	for(int i=1;i<=m;i++)
		change(a[i].l,1) ;
	for(int i=1;i<=n;i++)
		if(temp1[i].x>ask(temp1[i].r)-ask(temp1[i].l-1)) ans[i] = -1 ;
	memset(tr,0,sizeof(tr)) ;
	solve(0,m,1,tot) ;
	for(int i=1;i<=n;i++)
		if(ans[i]>0) cnt[ans[i]]++ ;
	for(int i=1;i<=m;i++)
		printf("%lld\n",cnt[i]) ;
	return 0 ;
}

整体二分套cdq

基本原理就是在二分cdq的分化区间,写法基本与朴素的相似。

例1

树套树
这道题的查排名为 k k k 的操作就等于查区间第 k k k 小,这里就不过多赘述了。
查找一个数的前驱后继显然可以由求一个数的排名来反推。
首先我们需要思考一下求区间第 k k k 小是如何写的。
之后我们就会发现在该数字需要去右区间时,减去左区间的贡献。那么在求一个数的排名时就可以通过类似方式逆推排名。
在这里插入图片描述

代码

#include<bits/stdc++.h>
#define int long long
#define lowbit(x) ((x)&(-x))
using namespace std ;
const int N=101234 ;
int n , m , cnt , t[N] , q , ans[N] , tr_min[N] , tr_big[N] ;
struct node{
	int l , r , k , opt , i ;
	bool ant ;
}a[N<<1],q1[N<<1],q2[N<<1],yuan[N<<1] ;
void change(int c[],int x,int k){
	while(x<=n){
		c[x] += k ;
		x += lowbit(x) ;
	}
}
int ask(int c[],int x){
	int ans=0 ;
	while(x)
		ans += c[x] , x-=lowbit(x) ;
	return ans ;
}
void solve(int l,int r,int L,int R){
	if(L>R) return ;
	if(l==r){
		for(int i=L;i<=R;i++)
			if(a[i].opt!=0&&a[i].opt!=3&&a[i].opt!=1&&a[i].opt!=4) ans[a[i].i] = l ;
		return ;
	}
	int mid=(l+r)>>1 , tot1=0 , tot2=0 ;
	for(int i=L;i<=R;i++){
		if(a[i].opt==1){
			if(a[i].k>mid) ans[a[i].i] += ask(tr_min,a[i].r)-ask(tr_min,a[i].l-1) ,	q2[++tot2] = a[i] ;
			else q1[++tot1] = a[i] ;
		}else if(a[i].opt==2){
			int x=ask(tr_min,a[i].r)-ask(tr_min,a[i].l-1) ;
			if(a[i].k<=x) q1[++tot1] = a[i] ;
			else a[i].k -= x , q2[++tot2] = a[i] ;
		}else if(a[i].opt==3){
			if(a[i].l<=mid) q1[++tot1] = a[i] , change(tr_min,a[i].i,a[i].k) ;
			else q2[++tot2] = a[i] , change(tr_big,a[i].i,a[i].k) ;
		}else if(a[i].opt==4){
			if(a[i].k>=mid) ans[a[i].i] += ask(tr_min,a[i].r)-ask(tr_min,a[i].l-1) , q2[++tot2] = a[i] ;
			else q1[++tot1] = a[i] ;
		}
	}
	for(int i=1;i<=tot1;i++){
		a[i+L-1] = q1[i] ;
		if(q1[i].opt==3) change(tr_min,q1[i].i,-q1[i].k) ;
	}
	for(int i=1;i<=tot2;i++){
		a[i+tot1+L-1] = q2[i] ;
		if(q2[i].opt==3) change(tr_big,q2[i].i,-q2[i].k) ;
	}
	solve(l,mid,L,L+tot1-1) ;
	solve(mid+1,r,tot1+L,R) ;
}
signed main(){
	cin >> n >> m ;
	for(int i=1;i<=n;i++){
		scanf("%lld",t+i) ;
		a[i].l = t[i] ;
		a[i].i = i ;
		a[i].k = 1 ;
		a[i].opt = 3 ;
		yuan[i] = a[i] ;
	}
	cnt = n ;
	for(int i=1;i<=m;i++){
		int opt , l , r , k ;
		cnt++ ;
		scanf("%lld%lld%lld",&opt,&l,&r) ;
		if(opt!=3){
			scanf("%lld",&k) ;
			a[cnt].opt = opt,a[cnt].l = l,a[cnt].r = r,a[cnt].k = k,a[cnt].i=++q ;
			if(opt==1||opt==5) ans[q]++ ;
			if(a[cnt].opt==4){
				a[cnt].opt = 1 ;
				a[cnt].ant = 1 ;
			}else if(a[cnt].opt==5){
				a[cnt].opt = 4 ;
				a[cnt].ant = 1 ;
			}
		}else{
			a[cnt].l = t[l] ;
			a[cnt].i = l ;
			a[cnt].k = -1 ;
			t[l] = r ;
			a[cnt].opt = 3 ;
			yuan[cnt] = a[cnt] ;
			cnt++ ;
			a[cnt].l = t[l] ;
			a[cnt].i = l ;
			a[cnt].k = 1 ;
			a[cnt].opt = 3 ;
		}
		yuan[cnt] = a[cnt] ;
	}
	solve(-2147483647,2147483647,1,cnt) ;
	int temp=0 ;
	for(int i=1;i<=cnt;i++){
		if(yuan[i].ant){
			yuan[i].opt = 2 ;
			yuan[i].k = ans[yuan[i].i] ;
		}else if(yuan[i].opt!=3){
			yuan[i].opt = 0 ;
		}
		if(yuan[i].opt!=0)
			a[++temp] = yuan[i] ;
	}
	solve(-2147483647,2147483647,1,temp) ;
	for(int i=1;i<=q;i++) printf("%lld\n",ans[i]) ;
	return 0 ;
}

整体二分答案序列满足单调性

当答案满足单调性时,我们可以分段进行讨论。

例1(我愿称之为整体二分套贪心)

序列 sequence
直接使用整体二分答案将 N N N 个数字利用贪心策略进行分段。

代码

#include<bits/stdc++.h>
#define int long long
using namespace std ;
int n , temp[512345] , a[512345] , b[512345] , cnt=0 , ans[512345] ;
map<int,int> mp ;
void solve(int l,int r,int L,int R){
	if(l==r||L>R) return ;
	int mid=(l+r)>>1 , sum=0 , maxn=0 , tot=L-1 ;
	for(int i=L;i<=R;i++) sum += abs(a[i]-b[mid+1]) ;
	maxn = sum ;
	for(int i=L;i<=R;i++){
		sum -= abs(a[i]-b[mid+1]) ;
		sum += abs(a[i]-b[mid]) ;
		if(sum<maxn){
			maxn = sum ;
			tot = i ;
		}
	}
	for(int i=L;i<=tot;i++) ans[i] = mid ;
	solve(l,mid,L,tot) ;
	solve(mid+1,r,tot+1,R) ;
}
bool cmp(int a,int b){return a<b ;}
signed main(){
	cin >> n ;
	for(int i=1;i<=n;i++) scanf("%lld",a+i) , temp[i] = a[i] ;
	sort(temp+1,temp+1+n,cmp) ;
	for(int i=1;i<=n;i++){
		while(temp[i]==temp[i+1]&&i<n) i++ ;
		mp[temp[i]] = ++cnt ;
		b[cnt] = temp[i] ;
	}
	b[cnt+1] = 2147483647 ;
	solve(1,cnt+1,1,n) ;
	int thing=0 ;
	for(int i=1;i<=n;i++)
		thing += abs(a[i]-b[ans[i]]) ;
	cout << thing ;
	return 0;
}

例2

[BalticOI 2004] Sequence 数字序列
基本一致,但是要利用一下小学知识,减法的性质。

代码

#include<bits/stdc++.h>
#define int long long
using namespace std ;
const int N=2123456 ;
int n , a[N] , ans[N] ;
void solve(int l,int r,int L,int R){
	if(l==r||L>R) return ;
	int mid=(l+r)>>1 , sum=0 , maxn , tot=L-1 ;
	for(int i=L;i<=R;i++) sum += abs(a[i]-mid-1) ;
	maxn = sum ;
	for(int i=L;i<=R;i++){
		sum -= abs(a[i]-mid-1) ;
		sum += abs(a[i]-mid) ;
		if(sum<maxn){
			maxn = sum ;
			tot = i ;
		}
	}
	for(int i=L;i<=tot;i++) ans[i] = mid ;
	solve(l,mid,L,tot) ;
	solve(mid+1,r,tot+1,R) ;
}
signed main(){
	cin >> n ;
	for(int i=1;i<=n;i++) scanf("%lld",a+i) , a[i] -= i ;
	solve(-2147483647,2147483647,1,n) ;
	int temp=0 ;
	for(int i=1;i<=n;i++) temp += abs(a[i]-ans[i]) ;
	cout << temp << '\n' ;
	for(int i=1;i<=n;i++)
		printf("%lld ",ans[i]+i) ;
	return 0;
}// 本质是二分区间的最佳答案,所以在存储答案时只需要吧满足要求的l一半存进去 

完结撒花~~~~
(如果还有新的整体二分的题目,欢迎私信我)

三维偏序

文章(肯定是我自己的我只是懒得搬……)

  • 46
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值