贪心

· · 算法·理论

贪心

引言

贪婪是一种人类本能的东西,贪心算法也是最接此人类平常思维的一种解题策略。其实,在解决一些日常问题时,我们本能就怀着对目标最直观、 最简单、最高效的思路,其中往往就带有贪心思想的影子。虽然它不能保证求得的最后解一定是最佳的,但是它可以为某些问题确定一个可行性范围。在某些范围内,贪心算法是我们的最佳选择。

贪心法的基本思想

贪心法是从问题的某个初始解出发,采用逐步构造最优解的方法,向给定的目标前进。在每一个局部阶段,都做一个在当前“看上去”最优的决策,并期望通过每一次所做的局部最优选择产生出一个全局最优解。做出贪心决策的依据称为“贪心策略”。要注意的是,贪心策略一旦做出,就不可再更改。

与搜索、动态规划不同的是,贪心只是一种策略或方法,而不是算法。推进的每一步不是依据某一个固定的递推式,而是做一个当时“看似最佳”的贪心选择(操作),不断将问题归纳为更小的相似子问题。所以,归纳、分析、选择正确合适的贪心策略,是解决贪心问题的关键。

贪心算法的步骤

\color{skyblue}{【真正意义要求解原问题】}

从问题的某一初始解出发

\color{skyblue}{【将原问题变成更小子问题的步骤】} $do$ 求出可行解的一个解元素 $\color{skyblue}{【整理解】}

由所有解元素组合成问题的一个可行解

贪心法的正确性证明

对于一个问题,如果想用贪心法求解,首先要想到基于某种“序”或者“规则”的贪心策略。

其次还要能证明其正确性。 要严格证明一个贪心算法的正确性是很困难的,目前最有效的一种方法叫“矩阵胚理论”,但是很复杂。信息学竞赛中常用的贪心证明方法,一般有反证法、构造法、调整法。其实,即使一个贪心算法是不完全正确的,也可以努力寻找一些调整方法,或 制定多种贪心策略,通过调整优化、比较择优来争取得到最优解 ,甚至也可以先得到一个“较优”解,然后,在此基础上进行搜索剪枝或分支定界。

构造法

根据描述的算法,用贪心的策略依次构造出一个解,可证明一定是合法的解。即用贪心法找可行解。

反证法

用贪心的策略,依次构造出一个解 S1 ,假设最优解 S2 不同于 S1 ,可以证明是矛盾的,从而得出 S1 就是最优解。

调整法

用贪心的策略,依次构造出一个解 S1 。假设最优解 S2 不同于 S1 ,找出不同之处,在不破坏最优性的前提下,逐步调整 S2 ,最终使其变为 S1 ,从而 S1 也是最优解。

题目

A:修理牛棚

这题可以使用调整法。

考虑如何使总长度最短。因为有些位置没有牛,所以有些牛棚是不需要覆盖木板的。如果能每个牛棚分单独一个木板,一定总长度是最小的。但是由于木板数量有限制,所以我们有时只能用一个比较长的木板的时候,顺便把没有牛的牛棚也覆盖了。就造成了浪费。所以一定是使用最大数目木板的。

第一头牛的位置在 a_1 ,最后一头牛的位置在 a_c 。先考虑把整个木板覆盖在 [1,c] ,这显然是最不优的选择。题目要求最多只能使用 M 块木板,我们可以把它转化为木板只能有 M-1 个空隙。也就是说我们可以先覆盖整个区间,再选择 M-1 处空隙去掉。根据贪心可以得出,去掉最长的 M-1 个空隙可以使总长度尽量短。

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=1100;
int m,c,a[N],b[N],sum;
bool cmp(int a1,int a2)
{
    return a1>a2;
}
int main()
{
    scanf("%d%d",&m,&c);
    if(m>=c)//特判 
    {
        printf("%d",c);//如果可以使用的木板数够用,可以一个牛棚用一块木板 
        return 0;
    }
    for(int i=1;i<=c;i++) scanf("%d",&a[i]);
    sort(a+1,a+c+1);
    for(int i=1;i<c;i++) b[i]=a[i+1]-a[i]-1;//算出每个空隙的长度 
    sort(b+1,b+c,cmp);//把空隙长度从小到大排序 
    for(int i=1;i<m;i++) sum+=b[i];//选择最长的M-1个空隙 
    printf("%d",a[c]-a[1]+1-sum);
    return 0;
}

B:删数问题

这题可以使用构造法

如果采取“找最大的数字删除”这种贪心思想,答案显然是错误的。比如 13542972 ,答案为 13297 ,而不是 13542

为了使数字变大,某个数字删去后一定要比原来大,于是我们得出贪心策略:每一步总是选择一个使剩下的数最小的数字删去,即按高位到低位的顺序搜索,若各位数字递增,则删除最后一个数字;否则,删除第一个递减区间的首字符,这样删一位便形成了一个新数字串。然后回到数字串首,按上述规则再删下一个数字。重复以上过程 s 次为止,剩下的数字串便是问题的解。

这样, 问题就转化为“求一个数字串的递减区间首字符”

还有 两个细节问题

#include<iostream>
#include<cstdio>
using namespace std;
const int N=310;
string s;
int n,a[N],m,lastp;
bool r;
//因为某些数会被删除,所以导致i-1不一定是i的上一个数,可以用-1标记删除的数,使用循环从i-1开始往前遍历,找出第一个不等于-1的数 
int last(int p)//返回上一个(未被删除的)数 
{
    for(int i=p-1;i>=1;i--) 
    {
        if(a[i]!=-1) return i;
    }
    return 0;
}
int main()
{
    cin>>s>>m;
    for(int i=0;i<s.size();i++) a[i+1]=s[i]-'0';//将字符串转化为数字 
    n=s.size();
    for(int i=2;i<=n;i++)
    {
        lastp=last(i);//得到上一个数 
        if(lastp&&a[lastp]>a[i]) 
        {
            a[lastp]=-1;//删除,标记为-1 
            m--;
            i--;
        }
        if(!m) break;
    }
    for(int i=n;i>=1;i--)//如果整个序列是升序的但是还没删完,就从后面删起。
    {
        if(!m) break;
        if(a[i]!=-1)
        {
            a[i]=-1;
            m--;
        }
    }
    for(int i=1;i<=n;i++)
    {
        if(a[i]>0) r=true;//特判前导0 
        if(r&&a[i]!=-1) printf("%d",a[i]);
    }
    return 0;
}

C:蜈蚣

这题要运用 最不利原则 (就是要尽量取多)

贪心过程:

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=110;
int c,f,n,a[N],cnt,sum,num;
int main()
{
    scanf("%d%d%d",&c,&f,&n);
    for(int i=1;i<=n;i++) 
    {
        scanf("%d",&a[i]);
        cnt+=a[i]/f;
        if(a[i]<f) sum+=a[i];
    }
    if(cnt<c) 
    {
        printf("-1");
        return 0; 
    }
    sum+=c*f;//把要穿的取了 
    cnt=0;
    for(int i=1;i<=n;i++)
    {
        if(a[i]>=f) cnt++;
        num+=(a[i]-(f-1))/f;
    }
    if(num>=c) sum+=(cnt-1)*(f-1);//取了f-1只后还能满足 
    else//取了f-1只后不能满足 
    {
        c-=num;
        for(int i=1;i<=n;i++) a[i]-=(a[i]-(f-1))/f*f;//更新数组a 
        sort(a+1,a+n+1);
        sum+=(cnt-c)*(f-1);//有cnt-c只还没满足 
        for(int i=n,j=1;j<c;i--,j++) sum+=a[i]-f;
    }
    printf("%d",sum);
    return 0;
}

D:安装饮水机

这题是一道区间贪心。

我们既然要满足安装的饮水机最少,又要满足每个人都要喝到水。我们可以先求出 lr 来表示每个人可以走到的最左和最右端点。这两个端点连成的线段就是每个人可以走到的区间。如果两个区间有重复,那么 r 较小的那个区间如果将饮水机定在较后的位置,可以使得后面的区间可以走到这个饮水机的位置。根据贪心的思想,我们得出当饮水机定在 r 的位置时,对后面的影响最大。

于是得出贪心策略:求出区间左右端点 lr ,把区间按 r 的大小排序。接着遍历 [l,r] 这整个区间,如果区间内没有饮水机,就在 r 的位置上建立一个饮水机。

证明:

每一个区间都一定包含一个点,所以当前选择方案是一种合法方案。假设最终答案是 ans ,当前答案是 cnt 。由于最优解是当前所有合法方案的最小值,所以 ans \le cnt 。而找到 cnt 个点就相当于找到 cnt 个两两无交集的区间。要想覆盖所有区间,就至少需要 cnt 个点,所以 ans \ge cnt

\because ans \le cnt,$ $ans \ge cnt \therefore$ 得证 $ans=cnt
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=1100,M=150100;
int n,s,w,cnt;
struct node
{
    int l,r;
}a[N];
bool f,r[M]; 
bool cmp(node a1,node a2) 
{
    return a1.r<a2.r;
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++) 
    {
        scanf("%d%d",&s,&w);
        //算出每个人能走到的最左和最右位置 
        a[i].l=s-w;
        a[i].r=s+w;
        if(a[i].l<0) a[i].l=0;
    }
    sort(a+1,a+n+1,cmp);//按右端点大小排序 
    for(int i=1;i<=n;i++)
    {
        f=false;
        for(int j=a[i].l;j<=a[i].r;j++)//找区间内是否已经有饮水机 
        {
            if(r[j])
            {
                f=true;
                break;
            }
        }
        if(!f)//如果没有,则在r上建立一个 
        {
            r[a[i].r]=true;
            cnt++;
        }
    }
    printf("%d",cnt);
    return 0;
}