CDQ分治(基础)

天使玩偶Violet

先按照时间维度分治理,然后只考虑一个点左下角的点,剩下的点旋转坐标系,把一个点转化为\(vx+vy\),就变成了在 \(vx_1<vx_2\)\(vy_1<vy_2\) 的情况下求 \(vx_1+vx_2\) 最大。

我们把在 \(mid\) 左边的点的 \(op=1\) 的改成\(3\),右边同理,再按照\(x\)轴排序,这样对于一个在操作\(4\)前面的操作\(3\),他时间轴上显然早于\(4\),在\(x\)轴上显然小于\(4\),那么还剩一维\(y\)就考虑树状数组即可。

#include <bits/stdc++.h>
using namespace std;
const int N=600009;
int n,m,x,y;
struct point{
	int ti,x,y,type;
}ls[N],q[N];
int pt,c[2000055],ans[N];
int lowbit(int p){return p&(-p);}
void update(int p,int x){
	for(int i=p;i<=1000010;i+=lowbit(i)) c[i]=max(c[i],x);
}
void remove(int p){
	for(int i=p;i<=1000010;i+=lowbit(i)){
		if(c[i]!=-0x3f3f3f3f) c[i]=-0x3f3f3f3f;
		else return;
	}
}
int query(int p){
	int res=-0x3f3f3f3f;
	for(int i=p;i>=1;i-=lowbit(i)) res=max(res,c[i]);
	return res;
}
void copy(){
	int maxx=0,maxy=0;
	pt=0;
	for(int i=1;i<=n+m;i++){
		if(q[i].type==2){
			maxx=max(maxx,q[i].x);
			maxy=max(maxy,q[i].y);
		}
	}
	for(int i=1;i<=n+m;i++){
		if(q[i].type==2 || (q[i].x<=maxx && q[i].y<=maxy))
			ls[++pt]=q[i];
	}
}
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;
	int j=mid+1;
	vector<point> vc;
	while(i<=mid && j<=r){
		if(ls[i].x<=ls[j].x){
			if(ls[i].type==1) update(ls[i].y,ls[i].x+ls[i].y);
			vc.push_back(ls[i]);
			i++;
		}
		else{
			if(ls[j].type==2) ans[ls[j].ti]=min(ans[ls[j].ti],ls[j].x+ls[j].y-query(ls[j].y));
			vc.push_back(ls[j]);
			j++;
		}
	}
	while(i<=mid)
		vc.push_back(ls[i]),i++;
	while(j<=r){
		if(ls[j].type==2){
			ans[ls[j].ti]=min(ans[ls[j].ti],ls[j].x+ls[j].y-query(ls[j].y));
		}
		vc.push_back(ls[j]);
		j++;
	}
	for(int i=l;i<=mid;i++){
		if(ls[i].type==1)remove(ls[i].y);
	}
	for(int i=l;i<=r;i++) ls[i]=vc[i-l];
}
int main(){
	cin >> n >> m;
	for(int i=1;i<=n;i++){
		cin >> x >> y;
		x++,y++;q[i].ti=0,q[i].x=x,q[i].y=y,q[i].type=1;
	}
	for(int i=1;i<=m;i++){
		int op;
		cin >> op >> x >> y;
		x++,y++;q[i+n].ti=i,q[i+n].x=x,q[i+n].y=y,q[i+n].type=op;
	}
	memset(ans,0x3f,sizeof(ans));
	for(int i=1;i<=1000010;i++) c[i]=-0x3f3f3f3f;
	copy(),cdq(1,pt);
	for(int i=1;i<=n+m;i++) q[i].x=1000002-q[i].x;
	copy(),cdq(1,pt);
	for(int i=1;i<=n+m;i++) q[i].y=1000002-q[i].y;
	copy(),cdq(1,pt);
	for(int i=1;i<=n+m;i++) q[i].x=1000002-q[i].x;
	copy(),cdq(1,pt);
	for(int i=1;i<=m;i++)
		if(ans[i]!=0x3f3f3f3f) 
			cout << ans[i] << endl;
	return 0;
}

P3769 [CH弱省胡策R2]TATT

题意:四维偏序

三维偏序的 \(CDQ\) 本质就是把每个 \((x,y,z)\) 转化若干为 \((0,y,z)\)\((1,y,z)\) 的贡献。剩下的就是个经典二维偏序问题。

四维偏序中珂以沿用三维偏序中的思路。

把若干 \((x,y,z,w)\) 分成 \((0/1,0/1,z,w)\),然后计算 \((0,0,z,w)\)\((1,1,z,w)\)的贡献。

注意以下两点:

  • 树状数组维护区间 \(max\) 是在只加点不删除点的状况下是正确的。

  • \(CDQ\) 时我们必须先递归计算左区间,算完左区间对右区间的贡献后在递归计算右区间,不然得出的值会变小。考虑这样一种贡献:左边对右边,右边对右边。

#include <bits/stdc++.h>
#define int long long
using namespace std;
int n;
struct node{
	int a,b,c,d,id;
	int w,ans;
	int grd;
}s[50009],pt[50009],ls[50009];
int l[50009];
int w[50009];
int pos1[50009];
int pos2[50009];
int tp;
bool cmp(node A,node B){
	if(A.a==B.a){
		if(A.b==B.b){
			if(A.c==B.c)
				return A.d<B.d;
			return A.c<B.c;
		}
		return A.b<B.b;
	}
	return A.a<B.a;
}
bool cmp2(node A,node B){
	if(A.b==B.b){
        if(A.a==B.a){
            if(A.c==B.c)
                return A.d<B.d;
            return A.c<B.c;
        }
        return A.a<B.a;
		
	}
	return A.b<B.b;
}
bool cmp3(node A,node B){
	if(A.c==B.c){
		return A.d<B.d;
	}
	return A.c<B.c;
}
int t[50009];
int lowbit(int p){return p&(-p);}
void add(int p,int delta){
	for(int i=p;i<=n;i+=lowbit(i)){
		t[i]=max(t[i],delta);
	}
}
int query(int p){
	int res=0;
	for(int i=p;i>=1;i-=lowbit(i)){
		res=max(res,t[i]);
	}
	return res;
}
void del(int p){
	for(int i=p;i<=n;i+=lowbit(i))
		t[i]=0;
}
int ans[50009];
void CDQ2(int l,int r){
	//a轴乱序,b轴排序
    if(l==r) return;
	int mid=(l+r)>>1;
	CDQ2(l,mid);
	sort(pt+l,pt+mid+1,cmp3);
	sort(pt+mid+1,pt+r+1,cmp3);
	int L=l;
	for(int i=mid+1;i<=r;i++){
		while(L<=mid && pt[i].c>=pt[L].c){
			if(pt[L].grd==0){add(pt[L].d,pt[L].ans);}
			L++;
		}
		if(pt[i].grd==1)
			pt[i].ans=max(pt[i].ans,pt[i].w+query(pt[i].d));
	}
	for(int i=l;i<=L;i++){
		if(pt[i].grd==0) del(pt[i].d);
	}
	for(int i=l;i<=r;i++)
		ls[pos2[pt[i].id]]=pt[i];
	for(int i=l;i<=r;i++)
		pt[i]=ls[i];
    CDQ2(mid+1,r);
}
void CDQ1(int l,int r){
    if(l==r) return;
	int mid=(l+r)>>1;
	CDQ1(l,mid);
	for(int i=l;i<=mid;i++)
		pt[i].grd=0;
	for(int i=mid+1;i<=r;i++)
		pt[i].grd=1;
	sort(pt+l,pt+r+1,cmp2);
    for(int i=l;i<=r;i++)
		pos2[pt[i].id]=i;
	CDQ2(l,r);
	for(int i=l;i<=r;i++){
		ls[pos1[pt[i].id]]=pt[i];
	}
	for(int i=l;i<=r;i++)
		pt[i]=ls[i];
    CDQ1(mid+1,r);
	return;
} 
signed main(){
	cin >> n;
	for(int i=1;i<=n;i++){
		cin >> s[i].a >> s[i].b >> s[i].c >> s[i].d;	
		l[i]=s[i].d;
		s[i].id=i;
        s[i].w=1;
	}
	sort(l+1,l+n+1);
	int len=unique(l+1,l+n+1)-l-1;
	for(int i=1;i<=n;i++){
		s[i].d=lower_bound(l+1,l+len+1,s[i].d)-l; 
	}
	sort(s+1,s+n+1,cmp);
	for(int i=1;i<=n;i++){
		if(s[i].a==s[i-1].a && s[i].b==s[i-1].b && s[i].c==s[i-1].c && s[i].d==s[i-1].d)
			pt[tp].w+=s[i].w;
		else{
			pt[++tp]=s[i];
		}	 
	}
	sort(pt+1,pt+tp+1,cmp);
	for(int i=1;i<=tp;i++){
        pt[i].id=i;
        pt[i].ans=pt[i].w;
		pos1[pt[i].id]=i;
	}
	CDQ1(1,tp);
    int maxx=0;
    for(int i=1;i<=n;i++){
        maxx=max(maxx,pt[i].ans);
    }
    cout << maxx << endl;
    system("pause");
	return 0;
}

CF1045G

首先按照\(r_i\)降序排序,这样只用右边能看到左边就可以了。

再来考虑排完序后的 \(CDQ\) ,乍一看可以左右分别按照 \(x_i\) 排序,然后归并,然后每一次贡献都是一个前缀。

其实这样是不正确的,因为此时 \(r_i\) 是乱序,可能在往右扫的过程中前缀是反复横跳状态,而归并只能从左往右,就寄了。

那么只好考虑以 \(q_i\) 排序了,此时的状态是:\(r_i\)乱序,但所有左边的 \(r_i\) 一定大于右边的 \(r_i\),然后左半边和右半边的 \(q_i\) 是分别排好序的。

那么对于右边的点,左边的可能可以贡献给他的点集就是一段连续的区间,这里可以发现,这个区间的端点一定是单调递增的,那么就可以愉快的双指针了。

还差一维,就是刚刚的\(x_i\)这一维度,可以考虑树状数组,就解决了。

注意要离散化。

CF848C

绷不住了,竟然有二维分块做法,万万没想到。

如果用\(CDQ\)分治,那么首先这个贡献是需要差分的,考虑转换一下,变成求:

\[∑_{i=1}^R i−pre_{A_i}\ [pre_{A_i}\ >=L] \]

那么我们可以把每一个 \(i\) 看作是一个横坐标是 \(i\) 纵坐标是 \(pre_{A_i}\) 的点,那么查询就是查询一个左上角矩形了。当然还要加上时间轴。

具体来说也就是:

  • \(i<=R\)

  • \(pre_{A_i}>=L\)

  • \(T[q[lr]]>T[pt]\)

  • 每个点的贡献是 \(i-pre[a_i]\)

三位偏序了,但是修改有点麻烦,可以考虑抵消上一次的点再加入新点。

然后再来考虑怎么三位偏序,首先按照时间维度排序,再把左右分别按照 \(i\) 排序,然后再按照 \(i\) 归并一下,如果当前是左半边的 \(i\) ,就在树状数组的 \(pre_{A_i}\) 位置上加 \(i-pre_{A_i}\)。对于右半部分的一个询问,当前已经加入的节点的 \(i\)值比他小满足条件 \(1\),第二维就是一段树状数组的后缀查询求和。

#include <bits/stdc++.h>
using namespace std;
const int N=100009;
int n,m;
int a[N],pre[N],nxt[N];
set<int> S[N];
struct node{
	int x,prex,op,w;
	int b,c;
	int id;
}e[N*2];
int tp;
int bit[N];
int lowbit(int p){return p&(-p);}
void add(int p,int delta){
	for(int i=p;i<=n;i+=lowbit(i))
		bit[i]+=delta;
}
void clean(int p){
	for(int i=p;i<=n;i+=lowbit(i))
		bit[i]=0;
}
int query(int l,int r){
	int resr=0;
	for(int i=r;i<=n;i+=lowbit(i))
		resr+=bit[i];
	int resl=0;
	l--;
	for(int i=l;i<=n;i+=lowbit(i))
		resl+=bit[i];
	return resr-resl;
}
bool cmp1(node A,node B){
	if(A.op!=B.op) return A.op<B.op;
	if(A.op==1) return A.x<B.x;
	else return A.c<B.c;
	//return A.x<B.x;
}
int ans[N];
void change(int pla,int v){
	e[++tp].op=1;
	e[tp].x=pla;
	e[tp].prex=pre[pla];
	e[tp].w=pre[pla]-pla;
	pre[pla]=v;
	e[++tp].op=1;
	e[tp].x=pla;
	e[tp].prex=pre[pla];
	e[tp].w=pla-pre[pla];
}
void CDQ(int l,int r){
	if(l==r) return;
	int mid=(l+r)>>1;
	CDQ(l,mid);
	CDQ(mid+1,r);
	//sort(e+l,e+mid+1,cmp1);
	//sort(e+mid+1,e+r,cmp1);
	int L=l;
	for(int i=mid+1;i<=r;i++){
		if(e[i].op!=2) continue;
		while(L<=mid && (e[i].x>=e[L].x || e[i].op==2)){
			if(e[i].op==1){
				add(e[i].prex,e[i].w);
			}
			L++;
		}
		ans[e[i].id]+=query(e[i].b,n);
	}
	for(int i=l;i<=L;i++)
		add(e[i].prex,-e[i].w);
	sort(e+l,e+r+1,cmp1);
}
int main(){
	cin >> n >> m;
	for(int i=1;i<=n;i++){
		cin >> a[i];
		set<int>::iterator it=S[a[i]].lower_bound(i);
		if(it!=S[a[i]].end()) pre[i]=*it;
		set<int>::iterator it2=S[a[i]].upper_bound(i);
		if(it2!=S[a[i]].end()) nxt[i]=*it2;
		S[a[i]].insert(i);
		e[++tp].x=i,e[tp].prex=pre[i],e[tp].w=i-pre[i];
		e[tp].op=1;
		//e[tp].id=tp;不需要还原
	}
	for(int i=1;i<=m;i++){
		cin >> e[++tp].op >> e[tp].b >> e[tp].c;
		//e[tp].id=tp;不需要还原
		if(e[tp].op==1){
			if(e[tp].c==a[e[tp].b]) continue;
			int PP,NN;
			set<int>::iterator it=S[a[i]].lower_bound(i);
			if(it!=S[a[i]].end()) PP=*it;
			set<int>::iterator it2=S[a[i]].upper_bound(i);
			if(it!=S[a[i]].end()) NN=*it2;
			S[a[e[tp].b]].erase(e[tp].b);
			S[e[tp].c].insert(e[tp].b);
			nxt[pre[e[tp].b]]=nxt[e[tp].b];
			nxt[PP]=e[tp].b;
			change(e[tp].b,PP);
			nxt[e[tp].b]=NN;
		}
	}
	CDQ(1,tp);
	for(int i=1;i<=tp;i++){
		if(ans[i]){
			cout << ans[i] << endl;
		}
	}
	system("pause");
	return 0;
}

P4207 [NOI2007] 货币兑换

首先有一个理论上应该一眼的性质:要么不买,要么花光积蓄。

证明考虑如果我们买入部分,那么卖出的时候赚的钱就少了。

这也提示我们类似这种每一次可以选择部分物品操作的题目,要思考思考有没有必要部分!

那么我们每天的状态就是两个:

  • 身无分文,全是金券。
  • 一夜暴富,没有金券。

我们设 \(f_i\) 为假如在 \(i\) 这一天没有金权最多能得到的钱。

那么我们转移可以从 \(f_{i-1}\) (什么也不干),或者 \(f_j\) (我在\(j\)这一天买然后在 \(i\)这一天卖出)而来。

\(x_i=f_i\times \frac{R_i}{A_iR_i+B_i}\)

\(y_i=f_i\times \frac{1}{A_iR_i+B_i}\)

\(f_i=max\) {\(f_{i-1},max\) { \(x_jA_i+y_jB_i\) } }

后面的一堆似乎很斜率优化,于是考虑 \(f_j\) 什么时候优于 \(f_k\)

\(\frac{y_j-y_k}{x_j-x_k} \gt -\frac{A_i}{B_i}\)

由于正负号问题,所以不能普通的斜率优化,所以考虑斜率优化,考虑用 \(CDQ\)分治,这样就能根据 \(x\)轴排序了,就能在左半边建立上凸壳再转移到右边了。

BZOJ2961 共点园

咕咕。

posted @ 2023-03-24 10:31  starslight  阅读(54)  评论(0)    收藏  举报