浅谈分治思想与倍增思想及其应用

这篇文章屯了很久了,一直想把内容总结到最好。

但由于时间关系,就先把现在能总结出来的写一下。

关于分治

基础分治

分治,意为:分而治之。

通常来说,将问题划分为若干个子问题,通过子问题的合并从而得到最优解。

一般分治都是讲问题分为左区间与右区间,划分为其他区间的分治 少之又少。

怎么通过问题的合并来得到最优解呢?

举个例子来看:

例子1:有N个数,你的任务是求出异或为0的区间有多少个?

这道题在之前写过的博客关于异或问题的总结中提到过,用map记录一下前缀即可。

但是现在任务是用分治来写(小题大做):

首先分治要分而治之,所以要分块l=1,r=n并写好分治函数

ll work(ll l,ll r){
    if(r<l) return 0;
    if(r==l) return num[l]==0?1:0;
    ll mid=(l+r)/2;
    ll sum=0;
    sum+=work(l,mid);
    sum+=work(mid+1,r);
}

上述代码应该都可以看懂:

首先计算左区间与右区间的贡献。

接下来将两个区间合并为一个新的区间,那么合并过程中必然又会产生新的贡献所以:

    ll res=0;
    map<int,int>mp;
    for(int i=mid;i>=l;i--){
        res^=num[i];
        mp[res]++;
    }
    res=0;
    for(int i=mid+1;i<=r;i++){
        res^=num[i];
        sum+=mp[res];
    }
    return sum;

这样一来就是整个区间[l,r]的贡献,所以主函数中输出 work(1,n)即为答案 ,复杂度 (n*lgn*c) c这个常数还蛮大的,因为用了map

例子2:平面最近点对:给出1e5个点,求最近的两点间的距离

这样枚举的复杂度会爆,所以我们考虑分治(按x坐标分治)。

我们把区间二等分,所以合并区间的时候就会有三种情况:

最小点对在左区间,最小点对在右区间,最小点对合并后产生

对于前两种没什么可说的,对于第三种:

如果我们不加优化的话,会增加[l,mid]*[mid+1,r]的复杂度*nlgn ,这个常数无疑来说太大了。

由于我们求的是最近距离,那么完全可以加一个启发式优化:

如果对mid.x的横坐标来说,如果大于当前最小距离,那么肯定不可以使得其成为最小值。

double solve(int l,int r)
{
    if(l==r) return INF;//一个点距离 返回INF
    if(l+1==r) return cal(save[l],save[r]);//两个点直接计算
    int mid=(l+r)/2;
    double d=min(solve(l,mid),solve(mid+1,r));
    double minl=d;
    int cnt=0;//储存两边的数
    for(int i=l;i<=r;i++)//[l,r]区间内在两边 横坐标之差不能超过在两边的最小距离
        if(abs((save[i].x-save[mid].x))<d)
            index[++cnt]=i;//更新一下
    sort(index+1,index+1+cnt,cmp);
    for(int i=1;i<=cnt;i++)
        for(int k=i+1;k<=cnt&&abs(save[index[i]].y-save[index[k]].y)<d;k++)//优化
            minl=min(minl,cal(save[index[i]],save[index[k]]));
    return minl;
}

例子3:归并排序

这个应该不需要过多的讲解了吧,分治的基础题目,没有掌握的话参见算法导论等算法书籍,基本都会有。

cdq分治

cdq分治,是陈丹琪(国际集训队队员,贼nb)发表论文里面提到的一种分治,与整体二分类似。

cdq分治的应用在于处理偏序问题。

算法核心在于,只考虑左区间对右区间的贡献。

那么对于

一维偏序问题(树状数组裸题)

二维偏序问题(排序后裸题)

三维偏序问题呢?还是裸题?

这就需要cdq分治

首先对于数组整体x坐标排序,保证了右边的不可能会对左边产生贡献

之后对于区间[l,r]进行cdq分治。

所以现在cdq分治核心就来了,左区间对右区间的贡献如何计算?

我们可以把左区间按照y坐标排序,右区间也按照y坐标排序

然后使用归并排序的思想:如果当前左边的这个y小于当前右区间的y,那么当前的左边的这个z就可以对右边指针的所有大于左边z的产生贡献,所以这一部分用树状数组去维护。

所以要求树状数组维护的区间为z的大小。

不要忘记减去贡献!!!!

void Divide(int l,int r){
    if(l>=r) return;
    int mid=(l+r)/2;
    Divide(l,mid);Divide(mid+1,r);
    sort(cop+l,cop+mid+1,cmpb);
    sort(cop+mid+1,cop+r+1,cmpb);
    int j=l;
    for(int i=mid+1;i<=r;i++){
        while(cop[i].b>=cop[j].b&&j<=mid){///当前左边的可以对之后的大于左边c的产生贡献
            update(cop[j].c,cop[j].w);///树状数组加上贡献 cop[j].w 为1
            j++;
        }
        cop[i].ans+=GetSum(cop[i].c);///然后计算一下 左边区间对当前值的贡献为多少
    }
    for(int i=l;i<j;i++) update(cop[i].c,-cop[i].w);
}

关于倍增

基础倍增

倍增的思想与分治如出一格,但实质又不同。

倍增思想的原理是 ,任何数字都可以用二进制数表示,那么处理距离问题,就可以使用倍增或者区间问题就可以使用倍增。

例子1:说到基础倍增不说RMQ问题就白说了~

关于RMQ问题——静态区间询问最大值与最小值

根据dp的思想,可以将静态区间的rmq数组处理出来。

dp[i][j]表示以i开始的区间长度为2^j次方的区间内的最大值或最小值。

那么很显然dp[i][j] = max(dp[i][j-1],dp[i+(1<<(j-1)][j-1])

之后询问就更为简单了,由于最大值和最小值具有区间可合并性,所以完全可以将区间拆为两个二进制的区间O1复杂度就可以得到静态区间最大值与最小值,假设区间长度为len,取log(len)即可

例子2:区间连续和问题:询问区间[x,y]内最少可以划分多少个区间使得每个区间的和都小于等于k

首先,如果单次询问的话:方法为贪心——从区间端点开始寻找这个区间最大的右边界,然后移动过去,继续找右边界。

这样贪心一定是正确的?可以通俗的想,如果跳过左边界,那么区间就会+1,而不这么做的话最多区间会-1,所以正确。

或者当作结论记住。

那么多次询问怎么处理?

首先倍增思想预处理出dp[i][j]表示,以i开始划分为2^j段的结束点+1的位置

基础上述的贪心思想我们便可以从l开始倍增判断dp[i][j]是否小于等于r,如果小于等于r,那么i=dp[i][j],同时ans=ans+(1<<j)

最后的ans要+1?为什么呢?因为剩下的要自己划分为一段而且这一段和一定小于等于k

        ll ans=1;
        int x,y;scanf("%d%d",&x,&y);
        for(int i=21;i>=0;i--){
            if(st[x][i]&&st[x][i]<=y){
                x=st[x][i];
                ans+=1ll<<i;
            }
        }

树上倍增问题

关于树上倍增问题首先想到——LCA

树上两点最近公共祖先:

首先,预处理整个树的deep数组——深度数组与f i,j 数组——i点向上2^j祖先是谁

下面分三步:

第一步:考虑什么时候两者可以同时倍增——深度相同的时,距离公共祖先的距离,就可以用二进制表示这个距离,从而使两个点一起倍增上移,所以第一步确定谁的深度大

第二步:让深度大的利用倍增,跳到与深度小的相同的高度,显然是可以的——任何数都可以用二进制表示。

第三步:两点同时向上跳,注意此时的判断条件是祖先不相同就向上跳,因为公共祖先上面的所有祖先两者一定相同。

这样LCA就解决了。

另外树上倍增还可以维护树链的最大值与树链的最小值,甚至可以维护树上路径的贸易 从u->v可以赚的最大差值。

这些题目以后总结再说吧

这次就先到这里了

完结~撒花!

发布了157 篇原创文章 · 获赞 146 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_43857314/article/details/105250821