算法CDQ分治初探

Posted Sakits

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了算法CDQ分治初探相关的知识,希望对你有一定的参考价值。

  CDQ分治是处理数据结构题的有力武器,通俗的讲,它可以替代一层数据结构,从而达到降低代码难度以及常数的作用,缺点是必须离线。

  CDQ分治一般可以用来处理偏序问题以及斜率优化DP问题。

  与普通分治不同的是,CDQ分治左区间的答案对右区间有贡献,最经典的例子是归并排序求逆序对。

  下面先讲讲偏序问题:

    二维偏序本质上和归并排序求逆序对一样,不多提及。

    三维偏序:第一维直接排序,第二维用CDQ分治,第三维用树状数组。

    具体地讲讲CDQ分治求偏序。因为我们第一维已经排序了,那么绝对不会出现左区间的第一维比右区间的第一维大的情况。

    那么递归完左右区间之后,将左右区间都按第二维排序,这时我们就可以统计左区间对右区间的元素的贡献了,因为右区间的第二维是有序的,于是可以扫一遍右区间,每次找到左区间最大的第二维小于当前枚举到的右区间元素的第二维的元素,用树状数组维护第三维,更新答案。

    四维偏序:实际上可以使用CDQ分治套CDQ分治的方式,只需要将上面的BIT改成CDQ分治就好了,照这么讲,n维偏序都可做了不是吗?

    $\geq$五维偏序:...是可做,但是复杂度上天。五维偏序往上就不如$O(n^2)$了。。。

求偏序问题的CDQ分治基本流程:

  若$l==r$ 则返回。

  分治左区间,分治右区间。

  统计左区间对右区间的贡献。

  消除BIT上的贡献(套CDQ分治就不需要)。

三维偏序代码:

技术分享图片
#include<iostream>
#include<cstring>
#include<cstdlib>
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn=500010;
struct poi{int a, b, c, cnt, ans;}v[maxn], tmp[maxn];
int n, k, tott;
int ans[maxn], cnt[maxn], tree[maxn], q[maxn];
inline void read(int &k)
{
    int f=1; k=0; char c=getchar();
    while(c<0 || c>9) c==- && (f=-1), c=getchar();
    while(c<=9 && c>=0) k=k*10+c-0, c=getchar();
    k*=f;
}
inline bool cmp2(poi a, poi b){return a.b<b.b||(a.b==b.b && a.c<b.c);}
inline bool cmp(poi a, poi b){return a.a<b.a||(a.a==b.a && cmp2(a, b));}
inline void update(int x, int delta){for(;x<=k;x+=x&-x) tree[x]+=delta;};
inline int query(int x){int sum=0; for(;x;x-=x&-x) sum+=tree[x]; return sum;}
void solve(int l, int r)
{
    if(l==r) return;
    int mid=(l+r)>>1;
    solve(l, mid); solve(mid+1, r);
    sort(v+l, v+mid+1, cmp2); 
    sort(v+mid+1, v+r+1, cmp2);
    int L1=l, L2=mid+1;
    while(L2<=r)
    {
        while(L1<=mid && v[L1].b<=v[L2].b) update(v[L1].c, v[L1].cnt), ++L1;
        v[L2].ans+=query(v[L2].c); ++L2;
    }
    for(int i=l;i<L1;i++) update(v[i].c, -v[i].cnt);
}
int main()
{
    read(n); read(k);
    for(int i=1;i<=n;i++) read(tmp[i].a), read(tmp[i].b), read(tmp[i].c);
    sort(tmp+1, tmp+1+n, cmp);
    for(int i=1, j=1;i<=n;i=j)
    {
        v[++tott]=tmp[i];
        while(tmp[i].a==tmp[j].a && tmp[i].b==tmp[j].b && tmp[i].c==tmp[j].c && j<=n)
        j++, v[tott].cnt++;
    }
    solve(1, tott);
    for(int i=1;i<=tott;i++) cnt[v[i].ans+v[i].cnt-1]+=v[i].cnt;
    for(int i=0;i<n;i++) printf("%d\n", cnt[i]);
}
View Code

  斜率优化DP用CDQ也可以去掉一层数据结构,但是写法和偏序略微有些不同,按上面的做法可能会多一个$log$用于排序。

  我们用左区间去更新右区间的时候,显然左右区间需要排序的东西是不一样的,这时候我们就不能简单的按时间排序后分治了。

  这时有个非常喵喵的做法。先按等式右边排序,每次分治的时候,我们可以用$O(n)$的时间把编号$\leq mid$的分到左边,$> mid$的分到右边,但是如果一开始按编号排序,是无法做到$O(n)$时间把斜率分到左右两边的。

  每次我们先递归左区间,每次退出区间的时候把这个区间按照横坐标归并排序,而我们一开始已经按照等式右边排序了,所以递归完左区间后当前区间就是左区间横坐标有序,右区间按等式右边有序的情况,这时候我们就可以$O(n)$用单调队列做到斜率优化DP了。

斜率优化DP用CDQ分治的基本流程:

  若l==r 则返回。

  把左区间按编号把$\leq mid$的分到左边,$>mid$的分到右边。

  分治左区间。

  此时左区间横坐标有序,右区间按等式右边有序,所以用单调队列斜率优化DP。

  分治右区间。

  按等式右边归并排序。

这个的代码可以参考下方例题bzoj1492: [NOI2007]货币兑换Cash。

施工中...

y[j]=f[j]/(a[j]*rate[j]+b[j])*rate[j]

x[j]=f[j]/(a[j]*rate[j]+b[j])

x[j]*a[i]+y[j]*b[i]<x[k]*a[i]+y[k]*b[i]

(x[j]-x[k])*a[i]<(y[k]-y[j])*b[i]

(x[j]-x[k])/(y[k]-y[j])<b[i]/a[i]

技术分享图片
#include<iostream>
#include<cstring>
#include<cstdlib>
#include<cstdio>
#include<algorithm>
#include<cmath>
using namespace std;
const int maxn=500010, inf=1e9;
struct poiq{double a, b, rate, k; int pos;}q[maxn], nq[maxn];
struct poip{double x, y;}p[maxn], np[maxn];
int n, que[maxn], st[maxn];
double f[maxn];
bool operator < (poiq a, poiq b){return b.k-a.k>1e-9;}
inline double xl(int a, int b){return fabs(p[b].x-p[a].x)>1e-9?(p[a].y-p[b].y)/(p[b].x-p[a].x):-inf;}
void solve(int l, int r)
{
    if(l==r)
    {
        f[l]=max(f[l], f[l-1]);
        p[l].x=f[l]/(q[l].a*q[l].rate+q[l].b);
        p[l].y=p[l].x*q[l].rate;
        return;
    }
    int mid=(l+r)>>1, L1=l, L2=mid+1;
    for(int i=l;i<=r;i++)
    if(q[i].pos<=mid) nq[L1++]=q[i]; else nq[L2++]=q[i];
    for(int i=l;i<=r;i++) q[i]=nq[i]; solve(l, mid); 
    int L=1, R=0;
    for(int i=l;i<=mid;i++)
    {
        while(L<R && xl(que[R-1], que[R])-xl(que[R], i)>1e-9) R--;
        que[++R]=i;
    }
    for(int i=mid+1;i<=r;i++) 
    {
        while(L<R && q[i].k-xl(que[L], que[L+1])>1e-9) L++;
        f[q[i].pos]=max(f[q[i].pos], q[i].a*p[que[L]].y+q[i].b*p[que[L]].x);
    }
    solve(mid+1, r); 
    L1=l, L2=mid+1, L=l;
    while(L1<=mid && L2<=r)
    if(p[L2].x-p[L1].x>1e-9) np[L]=p[L1], L1++, L++; else np[L]=p[L2], L2++, L++;
    while(L1<=mid) np[L]=p[L1], L1++, L++;
    while(L2<=r) np[L]=p[L2], L2++, L++;
    for(int i=l;i<=r;i++) p[i]=np[i];
}
int main()
{
    scanf("%d%lf", &n, &f[0]);
    for(int i=1;i<=n;i++)
    {
        scanf("%lf%lf%lf", &q[i].a, &q[i].b, &q[i].rate);
        q[i].k=q[i].b/q[i].a; q[i].pos=i;
    }
    sort(q+1, q+1+n); solve(1, n);
    printf("%.3lf\n", f[n]);
    return 0;
}
View Code

 

以上是关于算法CDQ分治初探的主要内容,如果未能解决你的问题,请参考以下文章

CDQ 分治算法模板

CDQ分治与整体二分

CDQ分治学习笔记

cdq分治浅谈

CDQ分治入门

浅谈CDQ分治