整体二分<QAQ> && CDQ分治

来源:互联网 发布:软件项目质量保证 编辑:程序博客网 时间:2024/05/18 04:01

昨天学习了一下整体二分,写了一下。
真的好快……
0.3s怒过bzoj1901QAQ
我们先来总结一下什么是整体二分:
拿带修改区间第k大作为例子:
1.如果单独得到一个答案,也就是一次询问,那么我们可以考虑二分答案,然后统计区间[l,r]中小于等于k的数字的个数。
2.考虑这样做的复杂度,一次是nlogn,即每次看一下区间[l,r]之间小于答案的有多少个。
它的效果等同于排序,都是nlogn。
3.但是这个东西,如果多加几组询问好像也无所谓,理由如下:
1)区间[l,r],假设我们有一组询问的答案是[l,mid]之间,另外一组在[mid + 1,r]之间。
2)这时候按照传统意义下,我们本来应该分开做这两组询问,把它们独立开,也就是说,考虑这两组询问:
做第一组的时候,要二分到[l,r]
做第二组的时候,要二分到[l,r]
所以我们考虑,在做第一组的时候,我们如果发现和第二组的区间重合了,那么这个贡献也加给第二组,这样的话,我们的整体二分就可以进行了。
这篇博客写的很详细:
http://www.cnblogs.com/zig-zag/archive/2013/04/18/3027707.html
那么我们考虑处理每一组询问,显然每次都需要计算每个询问的[l,r]之间小于等于当前答案的值有多少个
所以扫一遍显然是不行的,但我们可以开一个树状数组,这样就可以做了。
对于修改,我们可以考虑先把原序列的那个值在树状数组里减掉,然后加上新的值。
然后另外就是:
整体二分要开的数组大小:n + 2 * m
这个一定要注意呀QAQ
具体就是看代码了……

代码如下:

#include <cstdio>#include <cstring>#include <algorithm>#define Rep(i,n) for(int i = 1;i <= n;i ++)#define RD(i,l,r) for(int i = l;i <= r;i ++)#define u t[x]#define o q[i]using namespace std;const int N = 200005;const int inf = 1 << 30;struct Query{    int x,y,tp,s,k,cur;    void save(int a,int b,int c = 0,int d = 0,int e = 0){tp = a,x = b,y = c,k = d,s = e,cur = 0;};}q[N],q1[N],q2[N];int t[N],n,m,a[N],ans[N],tmp[N];void Add(int x,int s){for(;x <= n;x += x & -x)u += s;}int Qry(int x){int s = 0;for(;x;x -= x & -x)s += u;return s;}void solve(int l,int r,int ql,int qr){    if(ql > qr)return;    if(l == r)    {        RD(i,ql,qr)if(o.tp == 3)ans[o.s] = l;        return ;    }    int mid = l + r >> 1;    RD(i,ql,qr)        if(o.tp == 1 && o.y <= mid)Add(o.x,1);        else if(o.tp == 2 && o.y <= mid)Add(o.x,-1);        else if(o.tp == 3)tmp[i] = Qry(o.y) - Qry(o.x - 1);    RD(i,ql,qr)        if(o.tp == 1 && o.y <= mid)Add(o.x,-1);        else if(o.tp == 2 && o.y <= mid)Add(o.x,1);    int l1 = 0,l2 = 0;    RD(i,ql,qr)        if(o.tp == 3 && o.cur + tmp[i] >= o.k)            q1[++ l1] = q[i];        else if((o.tp == 1 || o.tp == 2) && o.y <= mid)            q1[++ l1] = q[i];        else if(o.tp == 3)            o.cur += tmp[i],q2[++ l2] = q[i];        else q2[++ l2] = q[i];    Rep(i,l1)q[ql + i - 1] = q1[i];    Rep(i,l2)q[ql + l1 + i - 1] = q2[i];    solve(l,mid,ql,ql + l1 - 1);    solve(mid + 1,r,ql + l1,qr);}int main (){    int cnt = 0,cnq = 0;    scanf("%d%d",&n,&m);    Rep(i,n){scanf("%d",&a[i]);q[++ cnt].save(1,i,a[i]);}    Rep(i,m)    {        char op[3];int l,r,k;        scanf("%s%d%d",op,&l,&r);        if(op[0] == 'Q')        {            scanf("%d",&k);            q[++ cnt].save(3,l,r,k,++ cnq);        }        else         {            q[++ cnt].save(2,l,a[l]);            a[l] = r;            q[++ cnt].save(1,l,a[l]);        }    }    solve(0,inf,1,cnt);    Rep(i,cnq)printf("%d\n",ans[i]);    return 0;}

CDQ分治的坑晚上再填QAQ
我昨天水了一晚上QQ之后看了看CDQ分治的两道<水>题
大概有了一点理解。
1.hdu5618
三维空间上有n个点,对于每个点i, 找出有多少个点j使得xi>=xj&&yi>=yj&&zi>=zj
统计这n个点的答案。
这道题可以离线对不对<废话当然要离线统计
先考虑二维平面内的情况:


然后我们考虑先将x坐标排序,然后扫描整个x坐标,用树状数组维护y坐标即可。
然后我们考虑一个不需要树状数组的做法。
分析:
对于x坐标有序的点,点i可以对点j产生贡献,当且仅当yi<=yj
我们考虑先把x坐标排序,那么对于某个区间[l,r],其x坐标必然有序。
我们再对[l,mid]和[mid + 1,r]进行y坐标的重新排序。
然后考虑计算[l,mid]之间所有点对于[mid + 1,r]的区间的贡献,这样计算显然是O(n)的。
我们考虑是先递归下去,然后排序比较对还是先排序计算贡献,然后递归下去比较对。
嗯……显然,先递归下去之后,会产生两个y有序的区间[l,mid],[mid + 1,r]。
并且我们知道对于左区间的所有点的x值是小于右区间的。
然后我们就可以直接计算左区间对于右区间的贡献。
考虑合并两个区间,显然归并排序一下就可以做到O(n)。我懒,就用了快排
然后我们就可以继续回溯上去。
如果先计算贡献,那么需要把左右区间对y进行排序,然后计算完左边对右边的贡献之后,再按照x坐标排回去。<常数大了很多2333>
时间复杂度O(nlog2n)


考虑三维偏序怎么维护。
显然考虑这样一个事实,就是在你先排序x坐标,然后在分治的过程中排序y坐标,然后不会搞z坐标。
怎么办呢,显然左右区间现在的y坐标已经有序了,就差一个z坐标没办法解决了。
没错,把左区间的所有z坐标都插入到树状数组里<实时插入<一次都插入就狗带了>>
然后对于右边的区间每次询问z坐标即可。
时间复杂度O(nlog2n)
坐标有重复的,所以到时候再乱搞一下,找找那个贡献最多的一起更新就成了。

#include <cstdio>#include <cstring>#include <algorithm>#define Rep(i,n) for(int i = 1;i <= n;i ++)#define u t[x]using namespace std;const int N = 100005;int n,ans[N],t[N];struct Point {int x,y,z,id,sum;}p[N];bool cx(Point a,Point b){return a.x == b.x ? (a.y == b.y ? a.z < b.z : a.y < b.y) : a.x < b.x;}bool cy(Point a,Point b){    if(a.y != b.y)return a.y < b.y;    return a.x == b.x ? a.z < b.z : a.x < b.x;}void add(int x,int val){for(;x <= 100000;x += x & -x)u += val;}int Qry(int x){int s = 0;for(;x;x -= x & -x)s += u;return s;}void solve(int l,int r){    if(l == r)return;    int mid = l + r >> 1;    solve(l,mid),solve(mid + 1,r);    sort(p + l,p + mid + 1,cy);    sort(p + mid + 1,p + r + 1,cy);    int j = l;    for(int i = mid + 1;i <= r;i ++)    {        for(;j <= mid && p[j].y <= p[i].y;j ++)            add(p[j].z,1);        p[i].sum += Qry(p[i].z);    }    for(j --;j >= l;j --)add(p[j].z,-1);}int main (){    int _;scanf("%d",&_);    while(_--)    {        scanf("%d",&n);        Rep(i,n)scanf("%d%d%d",&p[i].x,&p[i].y,&p[i].z),p[i].id = i,p[i].sum = 0;        sort(p + 1,p + 1 + n,cx);        solve(1,n);        for(int i = 1;i <= n;)        {            int j = i + 1;            int tmp = p[i].sum;            for(;j <= n  && p[i].x == p[j].x && p[i].y == p[j].y && p[i].z == p[j].z ;++ j)tmp = max(tmp,p[j].sum);            for(int k = i;k < j;k ++)ans[p[k].id] = tmp;            i = j;        }        Rep(i,n)            printf("%d\n",ans[i]);    }    return 0;}

2.bzoj1176 [Balkan2007]Mokia
维护一个W*W的矩阵,初始值均为0.每次操作可以增加某格子的权值,或询问某子矩阵的总权值.修改操作数M<=160000,询问数Q<=10000,W<=2000000.
第一行两个整数,S,W;其中S为矩阵初始值;W为矩阵大小
接下来每行为一下三种输入之一(不包含引号):
“1 x y a”
“2 x1 y1 x2 y2”
“3”
输入1:你需要把(x,y)(第x行第y列)的格子权值增加a
输入2:你需要求出以左上角为(x1,y1),右下角为(x2,y2)的矩阵内所有格子的权值和,并输出
输入3:表示输入结束


考虑树套树,然后TLE + MLE。
先来想一想子问题:
如果说,这个矩阵已经有了初始值,只需要询问几个矩形的和,这时候我们该怎么办呢?
<排序 + 树状数组>即可。
用树状数组维护y的坐标。
按照x坐标排序,我们每次询问,拆成四个即可,扫描x坐标的同时把那些点权加在树状数组里。
然后这样就可以做出子问题了。


想一下原问题,发现可以这样:
首先按照操作序列来搞的话,我们只需要考虑左区间的操作序列对于右区间的操作序列的影响。
我们可以这样来做:
1.先不管,先分治下去。
2.分治下去按照x坐标对整个区间排序。
3.找到右区间的值并处理询问。
4.然后按照id重新排回去。
你会发现你智障了
所以我们要这样做:
1.直接对原序列进行x坐标的排序。
2.直接处理左边对右边的贡献。
3.按照id和mid的关系分给左右两个区间,并且此时左右两个区间的x坐标也必然是有序的。
<其实就是按照原来顺序把id<= mid的放在左边,剩下的放在右边>
4.分治下去。


每次处理一个询问区间的时候,都会计算由于左边的修改而产生对右边询问的贡献,现在问题在于怎么计算这个贡献。
我们考虑左边修改之后,就是一个带权矩阵。
右边询问也没啥特别的。
而且我们将x坐标排序了。
实际上就是我们的子问题,这个可以直接用树状数组维护y坐标进行解决。


开始写HEOID1T3了QAQ
写完了QAQ
1.首先考虑是三个偏序关系{i > j}{vi > Rj}{Li > vj}
2.考虑分治的过程,[l,r]处理的时候,每次按照第一个关键字排序整个区间(也就是左边按照Rj,右边按照vi)。
3.然后对于属于左区间的元素,用它们去更新右区间的元素。
4.怎么更新右区间的元素呢?考虑到[l,r]区间的x坐标是有序的,且[l,mid]的id始终小于[mid + 1,r],那么我们可以知道,现在已经满足了两个约束条件,只需要满足Li>vj(即y值)就可以进行转移了。
5.怎么维护y值呢,我们可以每次把y值插入到树状数组里,每次查询的时候扫一遍小于当前y值的最大的dp值即可。
注意在更新完右区间之后一定要撤销之前的操作
注意在更新完右区间之后一定要撤销之前的操作
注意在更新完右区间之后一定要撤销之前的操作
按理来说写不挂,然而好像就是写挂了好多次QAQ

真·伪代码void Upd(int l,int r){    sort(l - > r);    for i in [l,r]    {        if q[i].id in [l,mid] then add(q[i].y,f[q[i].id]);        else f[q[i].id] = max(Qry(q[i].y) + 1,f[q[i].id]);    }}

#include <cstdio>#include <cstring>#include <algorithm>#define Rep(i,n) for(int i = 1;i <= n;i ++)#define RD(i,l,r) for(int i = l;i <= r;i ++)#define u t[x]using namespace std;const int N = 300005;struct Query{    int x,y,id;    void save(int x_,int y_,int id_){x = x_,y = y_,id = id_;};}q[N];int L[N],R[N],t[N],val[N],f[N],n,m;bool cmpval(Query a,Query b){return a.x == b.x ? a.id < b.id : a.x < b.x;}void add(int x,int s){if(~s)for(;x <= n;x += x & -x)u = max(u,s);else for(;x <= n;x += x & -x)u = 0;}int Qry(int x){int s = 0;for(;x;x -= x & -x)s = max(u,s);return s;}void solve(int l,int r){    if(l == r){f[l] = max(f[l],1);return;}    int mid = l + r >> 1;    solve(l,mid);    RD(i,l,mid)        q[i].save(R[i],val[i],i);    RD(i,mid + 1,r)        q[i].save(val[i],L[i],i);    sort(q + l,q + r + 1,cmpval);    RD(i,l,r)    {        if(q[i].id <= mid)add(q[i].y,f[q[i].id]);        else f[q[i].id] = max(f[q[i].id],Qry(q[i].y) + 1);    }    RD(i,l,r)if(q[i].id <= mid)add(q[i].y,-1);    solve(mid + 1,r);}int main (){    scanf("%d%d",&n,&m);    Rep(i,n)scanf("%d",&val[i]),L[i] = R[i] = val[i];    Rep(i,m)    {        int x,cg;        scanf("%d%d",&x,&cg);        L[x] = min(L[x],cg);        R[x] = max(R[x],cg);    }    solve(1,n);    int ans = 0;    Rep(i,n)ans = max(ans,f[i]);    printf("%d\n",ans);    return 0;}

嗯……来说一下可以用分治的理由QAQ
首先,分治的时候,我们可以在保证操作序顺不乱的础基上,调整第一个限制的顺序,从而使得三维偏序成功降到二维偏序。
理由:左边区间的操作永远比右边区间的操作优先,所以用打乱的<其实是按照x排好序的>操作来更新右边打乱的询问也是没有问题的。
那么这样问题就能完美地解决了。
也就是说可以花费总体时间*log的基础上降掉一维?不是很懂
以后应该还是得多练题QAQ


我学习完CDQ分治了之后,还是感觉不太理解,忽然脑子里蹦出一个想法:
直接用CDQ可不可以解决LIS问题?
我就跑去问了fsf
估计fsf想的是如何nlogn,他表示能做,但是很麻烦。
于是今天原本想复习地理
然后就跑去写了写:
思路:
1.首先CDQ分治可以保证在一维有序的情况下,对另一维进行排序。
理由如下:
我们先保证一维有序,然后分治到[l,r]
然后我们按照另一维排序左边再排序右边。
因为左边所有的第一维度的顺序都严格小于右边所有的第一维德顺序,所以左边和右边分别按照其它关键字排序都无所谓。
反正是用左边更新右边嘛QAQ
这样的话,我们复杂度肯定就带个log.不过无所谓
然后我想了想,可以这样做:
先把原序列不动,先计算[l,mid]里的dp值,然后对左右区间进行权值排序,再计算[l,mid]对于[mid+1,r]的影响,这样的话能保证每个f值在计算的时候,满足第一维顺序的f值(也就是下标小于它的所有的f值)都已经被计算出来了。
这样能保证计算贡献的时候不丢失。
这个是严格递增的LIS。

#include <cstdio>#include <cstring>#include <algorithm>#define Rep(i,n) for(int i = 1;i <= n;i ++)using namespace std;const int N = 100005;int n,f[N];struct Seq{int val,id;}a[N];bool cmp(Seq aa,Seq b){return aa.val == b.val ? aa.id < b.id : aa.val < b.val;}bool cp(Seq aa,Seq b){return aa.id < b.id;}void CDQ(int l,int r){    if(l == r){f[l] = max(f[l],1);return;}    int mid = l + r >> 1;    CDQ(l,mid);    sort(a + l,a + mid + 1,cmp);    sort(a + mid + 1,a + r + 1,cmp);    int j = l,tmp = 0;    for(int i = mid + 1;i <= r;i ++)    {        while(a[j].val < a[i].val && j <= mid)tmp = max(tmp,f[a[j].id]),j ++;        f[a[i].id] = max(f[a[i].id],tmp + 1);    }    sort(a + l,a + r + 1,cp);    CDQ(mid + 1,r);}int main (){    scanf("%d",&n);    Rep(i,n)scanf("%d",&a[i].val),a[i].id = i;    CDQ(1,n);    int tmp = 1;    Rep(i,n)tmp = max(f[i],tmp);    printf("%d\n",tmp);    return 0;}
0 0
原创粉丝点击