CQH分治与整体二分
CDH分治,核心思想就是对操作进行二分。感觉和我以前对操作分块的思想很像啊,fhb分块 ……(⊙o⊙)…
日常懒得写模板的题解,转载一篇(本家)
-----------------------------------------------------------分割线----------------------------------------------------------------------
在线/离线:首要考虑
在线算法: 可以以序列化的方式一个一个的处理输入,不必事先知道所有输入数据
离线算法: 必须事先知道所有的输入数据
(例如选择排序就是一个离线算法,而插入排序则不是)
众所周知,现在遍地毒瘤高级数据结构题(以及在一些算法之中需要用高级数据结构来加速的题),各种树(套树)*,代码量->INF,调试难度->INF,烦躁程度->INF,所幸在一些问题中我们可以利用分治的思想来解决之,最具有代表性的就是CDQ分治以及整体二分
如果题目强制要求在线的话(比如操作参数依赖于之前答案),只能乖乖地码数据结构了(不过似乎有一种二进制分组的做法能化一些在线问题为离线),而如果题目没有要求(或者你设计的算法不需要)在线的话,离线算法常常成为我们首要考虑的对象,CDQ分治和整体二分就是离线算法条件下可以运用的有力武器
CDQ分治
查询的限制——序
对于一个数据结构题而言(或者需要运用数据结构的地方),我们无非就是做两件操作,一是修改,二是查询
对于修改而言,有插入,删除,变更(其实等价于删除再插入)这几种方式
那么查询的本质是什么呢
我们思考所遇到过的数据结构题,可以发现查询实际上就在做一件事情:
把符合本次查询的限制的修改对答案产生的效果合并起来
满足这种限制通常表现为一种序的要求,并且这种序是广义的,符合限制的操作往往是按某种序(或多种序)排序后的操作的前缀
通常来说,查询一定有时间上的限制,也就是要求考虑发生在某个时刻之前的所有查询,对于一个问题而言,假如所有查询要求的发生时刻相同,那这就是一个静态查询问题,如果要求发生的时刻随着查询而变,那这就是一个动态修改问题,动态修改问题较静态查询而言复杂很多,往往需要高级数据结构,可持久化等手段,而静态查询简单很多,例如时间倒流,twopointers之类的方法都是很好的选择
动态修改->静态查询
CDQ分治算法的核心就在于:去掉时间的限制,将所有查询要求发生的时刻同化,化动态修改为静态查询
(其实对于有些问题来说可以把某一维的限制通过排序看作时间限制然后运用CDQ分治)
我们记过程DivideConquer(l,r)表示处理完[l,r]内的修改对查询的影响
此时我们引入分治思想,将操作序列划分为[l,mid],[mid+1,r]两个区间
这两个区间内部的修改对区间内部的查询的影响是完全相同的子问题,我们递归处理
处理完之后剩下来只要考虑[l,mid]中的修改对[mid+1,r]中的查询的影响
这时我们发现这其实已经变成了一个静态查询问题,因为所有的查询都发生在修改之后,我们只需要考虑静态查询的问题如何处理即可
时间复杂度分析
假设我们处理前面部分的修改对后面部分的复杂度为O(f(n))
CDQ分治的复杂度就为O(f(n)logn)
也就是说CDQ分治用一个log的代价完成了动态到静态
在处理静态查询的时候,我们往往需要对操作进行重新排序,如果直接做最后会多一个log,这时候我们有两种手段,一是在CDQ分治开始之前就先将这一维有序化,通过从左往右扫分两边来保证时刻操作序列都这一维有序,另一种方法时每次分治都得到一个有序表,通过合并两边的有序表来得到新的有序表
整体二分
二分答案——整体二分的前身
首先对于一类查询而言,我们要找的答案满足二分性,例如区间第k大(统计权值然后二分答案),这时候我们就可以采用二分答案的方法来解决,二分答案是把计算问题转化为判定问题的有效手段
二分答案的做法是不断维护一个可能的答案区间[l,r],每次二分,我们先求出当前的判定答案mid=(l+r)/2,然后我们统计在当前标准下会对查询产生贡献的修改(例如参数≤mid)的贡献和,我们再比较现在的贡献和与我们想要的贡献和的大小,如果贡献和已经超过我们想要的贡献和了,说明符合标准的修改太多了,我们需要紧缩标准(将答案区间变为l,mid),否则我们需要放宽标准(将答案区间变为mid+1,r),
所有操作的二分——从单个到整体
对于单个查询而言,我们可以采用预处理+二分答案的方法解决,但往往我们要回答的是一系列的查询,对于每个查询而言我们都要重新预处理然后二分,时间复杂度无法承受,但是我们仍然希望通过二分答案的思想来解决,整体二分就是基于这样一种想法——我们将所有操作(包括修改和查询)一起二分,进行分治
整体二分具体的做法比较难理解,我先把伪代码给出来
Divide_Conquer(Q, AL, AR) //Q是当前处理的操作序列 //WANT是要求的贡献,CURRENT为已经累计的贡献(记录的是1~AL-1内所有修改的贡献) //[AL, AR]是询问的答案范围区间 if AL = AR then 将Q中所有是询问操作的答案设为AL end if //我们二分答案,AM为当前的判定答案 AM = (AL+AR) / 2 //Solve是主处理函数,只考虑参数满足判定标准[AL, AM]的修改的贡献,因为CURRENT域中已经记录了[1,AL-1]的修改的贡献了,这一步是保证时间复杂度的关键,因为SOLVE只于当前Q的长度有关,而不与整个操作序列的长度有线性关系,这保证了主定理解出来只多一个log Solve(Q, AL, AM) //Solve之后Q中各个参数满足判定标准的修改对询问的贡献被存储在ANS数组 //Q1,Q2为了两个临时数组,用于划分操作序列 for i = 1 to Length(Q) do if (Q[i].WANT <= Q[i].CURRENT + ANS[i]) then //当前已有贡献不小于要求贡献,说明最终答案应当不大于判定答案 向数组Q1末尾添加Q[i] else //当前已有贡献小于要求贡献,说明最终答案应当大于判定答案 //这里是整体二分的关键,把当前贡献累计入总贡献,以后不再重复统计! Q[i].CURRENT = Q[i].CURRENT + ANS[i] 向数组Q2末尾添加Q[i] end if end for //分治,递归处理 Divide_Conquer(Q1, AL, AM) Divide_Conquer(Q2, AM+1, AR)
我们时刻维护一个操作序列和对应的可能答案区间[AL,AR]
我们先求得一个判定答案AM=(AL+AR)/2
然后我们考虑操作序列的修改操作,将其中符合标准(例如参数<=AM)的修改对各个询问的贡献统计出来
然后我们对操作序列进行划分
第一类操作是查询
如果当前查询累计贡献比要求贡献大,说明AM过大,满足标准的修改过多,我们需要给这中查询设置更小的答案区间来紧缩标准,于是将它划分到答案区间[AL,AM]中(这种情况我们不改变查询的CURRENT域,保证了继续下一次分治时这些查询的CURRENT域还是累计的[1,AL−1]的修改的贡献)
否则我们将当前已经统计到的贡献更新,将它划分到答案区间[AM+1,AR](这种情况下我们将[AL,AM]内的修改的贡献更新了CURRENT域,保证了下次继续分治时这些查询的CURRENT域已经保留的是[1,AM]的贡献了)
第二类操作是修改
假如它符合当前的标准,已经被统计入了贡献,那么它对于答案区间是[AM+1,AR]的查询来说已经没有意义了(因为我们知道它一定会对这些查询产生贡献,并且我们已经累计了这种贡献到CURRENT域中),我们就把它划分到[AL,AM]的区间里,
对于不符合当前的标准,未被统计入贡献的修改来说,如果我们放宽标准,它仍然可能起贡献,然而我们并未统计这种贡献,因此对于[AM+1,AR]的区间来说它仍具有考虑的意义,我们把它划分到[AM+1,AR]中
划分好了操作序列之后就继续分治递归下去就可以了
至此整体二分结束
时间复杂度分析
和CDQ分治一样,整体二分的代价也是O(f(n)logn)
--------------------------------------------------------分割线-----------------------------------------------------------------------------
1.三维偏序
我们通过排序得到第一维,CDQ分治第二维,树状数组统计第三维就好了。
#include<bits/stdc++.h> #define sight(c) ('0'<=c&&c<='9') #define N 200007 inline void read(int &x){ static char c; for (c=getchar();!sight(c);c=getchar()); for (x=0;sight(c);c=getchar())x=x*10+c-48; } void write(int x){if (x<10) {putchar('0'+x); return;} write(x/10); putchar('0'+x%10);} inline void writeln(int x){if (x<0) x*=-1,putchar('-'); write(x); putchar('\n'); } using namespace std; struct Node{ int a,b,c,id; inline bool operator <(const Node& A)const{ if (a==A.a) { if (b==A.b) return c<A.c; return b<A.b; } return a<A.a; } inline bool operator ==(const Node& A)const{ return A.a==a&&A.b==b&&A.c==c; } }p[N>>1],a[N>>1]; struct Tre{ #define L(x) x&-x int s[N]; void in(int x,int dla) {for (;x<N;x+=L(x)) s[x]+=dla;} int ask(int x) {static int L;for (L=0;x;x-=L(x)) L+=s[x]; return L;} void clear() {memset(s,0,sizeof s);} #undef L }Tree; int ans[N>>1],n,k,tot,id[N>>1]; inline bool cmp(const Node &x,const Node &y){ if (x.b==y.b) return x.id<y.id; return x.b<y.b; } #define Mid ((l+r)>>1) void cqh(int l,int r){ if (l==r) return; for (int i=l;i<=r;i++) p[i]=a[i],p[i].id=i; sort(p+l,p+r+1,cmp); for (int i=l;i<=r;i++) if (p[i].id<=Mid) Tree.in(p[i].c,1); else ans[a[p[i].id].id]+=Tree.ask(p[i].c); for (int i=l;i<=r;i++) if (p[i].id<=Mid) Tree.in(p[i].c,-1); cqh(l,Mid); cqh(Mid+1,r); } int main () { read(n); read(k); for (int i=1;i<=n;i++) read(a[i].a),read(a[i].b),read(a[i].c),a[i].id=i; sort(a+1,a+n+1); for (int i=n-1;i;i--) { if (a[i]==a[i+1]) tot++; else tot=0; ans[a[i].id]=tot; } cqh(1,n); for (int i=1;i<=n;i++) id[ans[i]]++; for (int i=0;i<n;i++) writeln(id[i]); return 0; }
2.三维偏序最长链
我们把时间当做第一维,我们考虑如何维护最长链。我们使用树状数组,用max取代+操作。重置操作在拓展过的节点遍历置0。(不要用memset)。
我们还要注意先分治(l,mid)再合并再分治(mid+1,r)。
#include<bits/stdc++.h> #define sight(c) ('0'<=c&&c<='9') #define N 307007 inline void read(int &x){ static char c;static int b; for (b=1,c=getchar();!sight(c);c=getchar())if (c=='-') b=-1; for (x=0;sight(c);c=getchar())x=x*10+c-48; x*=b; } void write(int x){if (x<10) {putchar('0'+x); return;} write(x/10); putchar('0'+x%10);} inline void writeln(int x){if (x<0) x*=-1,putchar('-'); write(x); putchar('\n'); } using namespace std; struct Node{ int a,b,c; }p[N>>1],a[N>>1]; struct Tre{ #define max(a,b) (a>b?a:b) #define L(x) x&-x int s[N]; void in(int x,int dla) {for (;x<N;x+=L(x)) s[x]=max(dla,s[x]);} int ask(int x) {static int L;for (L=0;x;x-=L(x)) L=max(s[x],L); return L;} void clear(int x) {for (;x<N;x+=L(x)) s[x]=0;} #undef L #undef max }Tree; int ans[N>>1],n,k,tot,T; vector<int> Q; void Li() { for(int i=1;i<=n;i++) Q.push_back(a[i].c); sort(Q.begin(),Q.end()); for(int i=1;i<=n;i++) a[i].c=lower_bound(Q.begin(),Q.end(),a[i].c)-Q.begin()+1; } inline bool cmp(const Node &x,const Node &y){ if (x.b^y.b) return x.b<y.b; return x.a>y.a; } #define Mid (l+r>>1) void cqh(int l,int r){ if (l==r) return; cqh(l,Mid); for (int i=l;i<=r;i++) p[i]=a[i]; sort(p+l,p+r+1,cmp); for (int i=l;i<=r;i++) if (p[i].a<=Mid) Tree.in(p[i].c,ans[p[i].a]); else ans[p[i].a]=max(Tree.ask(p[i].c-1)+1,ans[p[i].a]); for (int i=l;i<=r;i++) if (p[i].a<=Mid) Tree.clear(p[i].c); // for (int i=l;i<=Mid;i++) Tree.clear(a[i].c); cqh(Mid+1,r); } //void cqh(int l,int r){ // if (l==r) return; // cqh(l,Mid); // for (int i=l;i<=r;i++) p[i]=a[i]; // sort(p+l,p+Mid+1,cmp); sort(p+Mid+1,p+r+1,cmp); // for (int i=Mid+1,j=l;i<=r;i++) { // for (;j<=Mid&&p[j].b<p[i].b;j++) Tree.in(p[j].c,ans[p[j].a]); // ans[p[i].a]=max(ans[p[i].a],Tree.ask(p[i].c-1)+1); // } // for (int i=l;i<=Mid;i++) Tree.clear(p[i].c); // cqh(Mid+1,r); //} int main () { read(n); for (int i=1;i<=n;i++) read(a[i].b),read(a[i].c),a[i].a=i,ans[i]=1; Li(); cqh(1,n); int Ans=0; for (int i=1;i<=n;i++) Ans=max(Ans,ans[i]); writeln(Ans); return 0; } //两个cqh函数都是对的,只是不同的实现而已。
其实CDQ是可以拓展的。即使某些操作之间的贡献会互相影响,只要其满足可加性,我们也可以用CDQ加以解决。