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加以解决。

原文地址:https://www.cnblogs.com/rrsb/p/8313064.html

时间: 2024-11-09 03:45:44

CQH分治与整体二分的相关文章

CDQ分治与整体二分总结

Cdq分治和整体二分是两个很奇妙的东西.他们都是通过离线的思想来进行优化,从而更快的求出解. 整体二分通俗的讲就是二分答案,但是它了不起的地方是一下子把所有的答案都二分出来了,从而可以一下子得出所有查询. CDQ分治通俗的讲就是二分查询.通常的做法是把所有的查询分成两半,然后通过递归先计算出左边一半的所有的查询,然后通过这些已知的左半边的值来更新右半边的值.这里,最最重要的思想是通过左半边来更新右半边.更具体一点,就是用左半边的修改来更新右半边的查询. 重要的事情说话三遍: CDQ分治就是通过左

【cdq分治】cdq分治与整体二分学习笔记Part1.整体二分

之所以把cdq分治和整体二分放在一起学习,是因为他们两个实在太像了-不管是做法还是代码- 感觉整体二分可能会比cdq分治稍微简单那么一点点?所以先学整体二分.(感觉他们的区别在于整体二分是对每个操作二分答案,cdq是分治了操作序列) 整体二分是对答案进行二分,其具体操作如下: (比如以ZJOJ2013K大数查询为例) 具体过程 Step1.从(L,R)二分答案.mid=(L+R)>>1,用线段树维护原序列中(a,b)位置比mid大的数有多少个,同时记录对序列的操作分别是什么操作. Step2.

CDQ分治与整体二分小结

前言 这是一波强行总结. 下面是一波瞎比比. 这几天做了几道CDQ/整体二分,感觉自己做题速度好慢啊. 很多很显然的东西都看不出来 分治分不出来 打不出来 调不对 上午下午晚上的效率完全不一样啊. 完蛋.jpg 绝望.jpg. 关于CDQ分治 CDQ分治,求的是三维偏序问题都知道的. 求法呢,就是在分治外面先把一维变成有序 然后分治下去,左边(l,mid)关于右边(mid+1,r)就不存在某一维的逆序了,所以只有两维偏序了. 这个时候来一波"树状数组求逆序对"的操作搞一下二维偏序 就可

时间分治和整体二分总结

时间分治(又叫cdq分治),是解决一类“贡献独立”.“支持离线”的数据结构问题的算法. 假设有一个操作序列:ABAABAABBAAAB,其中每个A对其后面的B有一定贡献,要求输出每个B对应的答案. “贡献独立”是指:每个A对其后面的B的影响是不受其他A影响的,即是要我们用B前面的所有A更新过B,那么B的答案就是正确的. 贡献独立的例子:max,min,sum,count(极值,和,满足某种条件的A的个数). “支持离线”是指每个AB必须开始时就给出,有些问题(如维护凸壳优化DP),可能A和B是合

[整体二分]【学习笔记】【更新中】

先小结一下吧 主要为个人理解 整体二分 理解 $zyz:$整体二分是在权值上进行$CDQ$分治 我觉得更像是说$:$整体二分是在答案上进行$CDQ$分治 整体二分是二分答案在数据结构题上的扩展 因为数据结构题二分的答案通常是第几个操作之后,需要进行一些操作(预处理)之后才能判断,所以每次询问二分还不如从前往后暴力找 整体二分可以解决这样的问题 核心就是维护一个$cur$数组保存当前的影响,分治到$[l,r]$时只需要计算$[l,mid]$的影响再与$cur$里的合并就好了 这样分治里的操作就只与

4538: [Hnoi2016]网络 链剖 + 堆(优先队列) / 整体二分

GDOI之后写的第一道题.看到之后没什么感觉(是我太弱,中途一度想用kpm之前在某道题上用过的链表的方法.想了想应该不可能.) 好!让我们来分析这道题吧!首先简化模型,它是要求维护树上的一些路径,支持添加和修改,要求不经过某个点的路径的最大权值(不经过某个点,我一度想到了动点分,虽然我还不会). 我们可以先考虑在链上(其实仔细一想,如果链上的你会做,那么树上的大多数情况下便是多个了链剖而已吧!)的情况.在链上,有一些区间覆盖,要求没有覆盖某个点的区间的最大权值.那么我们接着想如果询问2询问了一个

K-th Number POJ - 2104 (整体二分)

K-th Number POJ - 2104 之前学主席树写了一遍 最近再看CDQ分治和整体二分,一直不是很理解,看着别人代码稍微理解了一些 1 //比主席树慢了挺多 2 #include <iostream> 3 #include <cstring> 4 #include <cstdio> 5 6 using namespace std; 7 8 const int maxn = 1e5 + 10; 9 const int maxq = 5010; 10 const

【cdq分治】【整体二分】bzoj 3110: [Zjoi2013] HYSBZ - 3110 K大数查询

题目链接:https://www.lydsy.com/JudgeOnline/problem.php?id=3110 题意:有N个位置,M个操作.操作有两种,每次操作如果是1 a b c的形式表示在第a个位置到第b个位置,每个位置加入一个数c.如果是2 a b c形式,表示询问从第a个位置到第b个位置,第C大的数是多少.注意是加入一个数,不是让这个数去求和. 题解:虽然是看cdq找到这题,但是感觉这个和平时做的三维偏序不大一样.这题其实是整体二分.就是首先,每次询问的答案应该是1,n之间的,然后

CF E. Till I Collapse 整体二分+根号分治

本来模拟赛想出这个来的,但是感觉不太友好啊~ 主席树可以直接屎过去,但是空间消耗很大. 考虑用整体二分: code: #include <bits/stdc++.h> #define N 100003 #define ll long long #define setIO(s) freopen(s".in","r",stdin) using namespace std; int A[N],C[N],ans[N],n; int solve(int x) {