<福州集训之旅Day6> | 分治与贪心 |


<更新提示>

<第一次更新>拖了好长时间的博客啦啦啦


<正文>

分治

分治不是一种算法,而是一种思想。分治的思想即为将一个难以解决的大问题分成若干个子问题,并将子问题一一解决,要求子问题的解可以合并为母问题的解,分治的思想具有一下三个特点:
· 容易拆解子问题
· 容易合并子问题
· 最小的子问题好求
这是运用分治思想最基本的特点。

<例一>
递归法求斐波那契数列
解析:我们知道斐波那契数列的递归式是Fn=Fn-1+Fn-2,但是如果直接用这条递归式递归求解斐波那契数列的第n项,会发现在求解Fn-1=Fn-2+Fn-3时又计算了一遍Fn-2,在算了两遍的情况下,时间复杂度就会大大增加,甚至逼近于动态规划的复杂度。我们先不解决这个问题,但是,直接用分治法肯定是不行了。所一我们推导出使用分治法必须的另一个特点:
· 子问题不重复

<例二>
数字的分治:快速幂
求解ab%p,b巨大。
解析:对于暴力方法,我们重复将a相乘即可,mod p用同余定理依次取模p就能得出答案,但是现在b巨大,我们必须采用其他方法。我们尝试使用分治法:根据同底数幂相乘的定理,我们可以使用ab/2平方即可,这样节约了大量的时间,如果b是奇数,那就再次乘一个a就能解决。回顾分治法的特点,使用这种方法刚好符合分治法,我们就得到了解决方案:快速幂
参考代码:

int power(int a,int b,int p)
{
    if(b==0)
    {
        return 1%p; 
    }
    else 
    {
        if(b==1)
        {
            return a%p;
        }
        else
        {
            long long result=power(a,b/2,p);
            result*=result;
            result%=p;
        }
        return result;
    }
}

<例三>
序列的分治:归并排序
解析:对与一个序列的排序,我们有许多种方案,我们尝试用分治法来对一个序列进行排序:归并排序。
归并排序的思想是这样的,我们已经有两个有序的序列:a{},b{}。我们将其所有的元素合并在一个新的有序序列c{},我们这样操作:先比较每一个序列的头元素的大小,选择大的移出序列,加入新序列,再次比较新的头元素,直至构造完c{}为止。对于如何获得有序的序列,我们以此将序列分半,直至序列只有一个元素,只有一个元素的序列必然是有序的,在递归向上返回,依次按照归并排序的思想对序列进行排序即可。这就是分治法排序——归并排序。
参考代码:

#include<bits/stdc++.h>
using namespace std;
int c[10000]={},tmp[100000]={};//对c{}进行排序 
long long ans=0;
void mergesort(int begin,int end)
{
    if(begin==end)return;
    else
    {
        int mid=(begin+end)>>1;
        mergesort(begin,mid);
        mergesort(mid+1,end);
        int h1=begin,h2=mid+1,t1=mid,t2=end,t=begin;
        while(t<=end)
        {
            if(h1<=t1)
            {
                if(h2<=t2)
                {
                    if(c[h1]<=c[h2])tmp[t++]=c[h1++];
                    else{ans+=t1-h1+1;tmp[t++]=c[h2++];}            
                }
                else tmp[t++]=c[h1++];
            }
            else tmp[t++]=c[h2++];          
        }
        for(int i=begin;i<=end;i++)c[i]=tmp[i];
        return;
    }
}
int main()
{
    int n;
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>c[i];
    }
    mergesort(1,n);
    for(int i=1;i<=n;i++)
    {
        cout<<c[i]<<" ";        
    }
    cout<<endl;
    return 0;
}
小结

对于分治,牢记几个特点,思考怎么分解问题是关键。

贪心

贪心也是一种思想,不是算法。贪心的思想:同过每一次决策的最优解来得到全局的最优解。

<例一>
硬币问题
现在你有1元钱,5元钱,10元钱,50元钱,100元钱的纸币各 C1 , C2 , C3 , C4 , C5 枚,当你想要支付A元钱的时候,你最少需要用多少张纸币?
解析:当然,我们首先用大面额的纸币即可,这显然是正确的决策。每一次尽量用大面额,用的纸币就少。这就是由每一次决策的最优解来得到全局的最优解。也是最简单的贪心。

<例二>
字典序
给定长度为N的字符串S ,要构造一个长度为N字符串T。T是一个空串,反复执行下列任意操作:
l 从S的头部删除一个字符,加到T的尾部;
l 从S的尾部删除一个字符,加到T的尾部;
目标是要构造字典序尽可能小的字符串T。
解析:当然,按照贪心的策略,我们直接比较两头字符串的字典序大小,字符一大一小就选择小的移动到前面,如果两头一样大,再看第二个,第二个也一样大,就以此往后,直至比较出大小在选择是否移动即可,这就是本题的贪心策略。

<例三>
删数问题
对于给定的n位正整数a,
编程计算删去k个数字后得到的最小数。
解析:对于这个问题,看似比较难处理,但是我们依然可以找到可行的策略:从头开始扫描,默认视为在扫描一个升序序列,然后删除合法升序序列的最后一个元素,即顺序扫描,删除第一个下降前的那个数。如果整个数是一个合法的升序序列,删除最后一个数即可。举例发现,这个策略是可行的,这就在于如何发现简单的贪心策略了。

<例四>
区间覆盖(线段覆盖)
在一个数轴上有n条线段,现要选取其中k条线段使得这k条线段两两没有重合部分(端点可以重合),问最大的k为多少。
解析:这是一道非常经典的的贪心题,策略就是按线段的右端点排序,在从大往小取。意思就是我们不管这条线段前面有多长,就管他什么时候结束,我们取结束早的点,就能得到最优答案。
证明:证明:假设a[i]表示对于左端点在i右边的线段们的答案,那么显然序列a是递减的。

小结

贪心的重点是策略,有时候我们自己一下无法证明贪心是否正确,但是可以通过对拍来验证,所以试图找到贪心策略是关键。


<后记>
分治与贪心结束啦


<废话>

猜你喜欢

转载自blog.csdn.net/prasnip_/article/details/79387466
今日推荐