【贪心例题专题】&贪心~越多越好哟~&

贪心算法-例题


@前言@

Ok又是我长期不更博客的怠惰之人~
那么这次给大家带来的是贪心算法的系列例题以及作者-我的感想
好的废话不多说,直接进入正题=>=>


@概念@

@介绍@

【以下是官方介绍】

贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解。
贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。
引用自百度百科

嗯,这解释很官方
但是如果只会死记硬背的话就没意思了~

【以下来自作者的理解】
贪心算法,就像是要到达一个地方,有很多岔路横生的、曲折的道路。你不需要理会横生的支路,因为只是朝着终点的方向一直走,不管路有没有断。
嗯,也就是说明,贪心算法不一定得到正确的答案(关键)(快做笔记)。

与其说贪心是一种算法,不妨说贪心是一种思想、策略——鼠目寸光,眼光短浅,没有长远之见【啊喂喂为什么都是贬义词】。所以在判断一个问题是否能贪心的方法是很 多变 的【表示甚至有时候你用了贪心自己都不知道【可能是作者自己的错QAQ】】。

【以下摘自老师课件】
贪心算法的步骤:
(1)把求解的问题分成若干个子问题。
(2)对每一子问题求解,得到子问题的局部最优解。
(3)把子问题的解局部最优解合成原来解问题的一个解。

@适用范围@

-(1)判断该题是否适合于用贪心策略求解
这个应该很容易理解,毕竟每个算法都有自己的适用范围
你应该不可能拿贪心解决【A+B Problem】,是吧
【注意!】虽然我说着适用贪心的判断好似很简单,其实不然。贪心的应用千万要严格证明出来!不然你怎么得到的0分都不知道

-(2)决定如何选择贪心标准,以得到问题的最优解
举个栗子:【部分背包问题】

现有一个口袋,口袋至多只能装重量为w的金属。有s个种类的金属, 每种金属重量不同,分别为n1, n2, … , ns,同时每个种类的金属总的价值也不同,分别为v1,v2, …, vs。问最多能带走价值多少的金属。注意到金属是可以被任意分割的,并且金属的价值和其重量成正比。

一个简单的贪心标准:总价值的大小
但是这对不对呢?以我们生活的小常识就可知:单价才可以决定物品真正的价值。所以显然贪心标准选择错误。
【再次注意!】依然的,我们选择了一个通俗易懂的栗子。但是你永远不知道 这个错误的“总价值”会在哪个困难的题目把你迷得团团转,所以不要轻视!

@一些典型的结构@

贪心和DP相似点之一就是它也有着特殊的问题结构,可以在你迷茫时点中你的思路。那么:
【以下摘自老师课件】
1.贪心选择性质:算法中每一步选择都是当前看似最佳的选择,这种选择依赖于已做出的选择,但不依赖于未做的选择。
2.最优子结构性质:算法中每一次都取得了最优解(即局部最优解),要保证最后的结果最优,则必须满足全局最优解包含局部最优解。
【不知道你们看懂没有但是……还算是有用的】
但并不是所有具有最优子结构的问题都可以用贪心策略求解。因为贪心往往是盲目的,需要使用更理性的方法——动态规划【那就不是我们讨论的内容了~】


@例题解析@

<1>装箱问题-【唉?这里还可以装!】

此处可测试你的代码
描述
一个工厂制造的产品形状都是长方体,它们的高度都是h,长和宽都相等,一共有六个型号,他们的长宽分别为1*1, 2*2, 3*3, 4*4, 5*5, 6*6。这些产品通常使用一个 6*6*h 的长方体包裹包装然后邮寄给客户。因为邮费很贵,所以工厂要想方设法的减小每个订单运送时的包裹数量。他们很需要有一个好的程序帮他们解决这个问题从而节省费用。现在这个程序由你来设计。
输入
输入文件包括几行,每一行代表一个订单。每个订单里的一行包括六个整数,中间用空格隔开,分别为1*1至6*6这六种产品的数量。输入文件将以6个0组成的一行结尾。
输出
除了输入的最后一行6个0以外,输入文件里每一行对应着输出文件的一行,每一行输出一个整数代表对应的订单所需的最小包裹数。
样例输入
0 0 4 0 0 1
7 5 1 0 0 0
0 0 0 0 0 0
样例输出
2
1

@解析@

抓住两点:
<1>包裹空间装满,才可以最优√
<2>先装大箱后装小箱,用小箱填补大箱的空位,才可以装满√

@深度解析+代码

OK咱先把框架弄出来

#include<cstdio>
#include<cmath>
int a[10];
int main()
{
    while(true)
    {
        int ans=0,S=0;
        for(int i=1;i<=6;i++) 
        {
            scanf("%d",&a[i]);
            S+=a[i];
        }
        if(S==0) return 0;//用来简便判断是否已结束程序
                          //显然,当且仅当6个都为0时,满足if
        /*

此
处
是
重
点
代
码
        */
        printf("%d\n",ans);
    }
}

重点的代码,我们按照解析的思路来:
<1>装6*6的箱子:这一步很显然,因为6*6的箱子直接将空间占满了。
代码:

int a[10];
...
{
    ans+=a[6];
}//6*6

<2>装5*5的箱子:此时剩下6*6-5*5的空间即11个空间,鉴于装完5*5的箱子之后只能装1*1,所以用1*1的箱子尽可能地去装。
代码:

int a[10];
...
{
    ans+=a[5];
    a[1]-=min(11*a[5],a[1]);//这就是尽可能的意思
    /*稍稍解释一下,这条语句的意思是:
    <1>当11*a[5]<=a[1]时,即1*1能够装满
    a[1]就会剩下a[1]-11*a[5]
    <2>当11*a[5]>a[1]时,即1*1装不满
    尽量装后剩下a[1]-a[1]即0个
    下面类似的语句同理*/
}//5*5

<3>装4*4的箱子:这个稍稍有点麻烦咯。我们先考虑用2*2的填空,共需要(6*6-4*4)/4即5个。然后依然尽可能地装。如果装完了呢?这时再用1*1的去尽可能地装,即实现了目的。
代码:

int a[10];
...
{
    int a[10];
    ...
    ans+=a[4];
    if(5*a[4]>a[2])//如果2*2会装完
        a[1]-=min((a[4]*5-a[2])*4,a[1]);
        //剩下a[4]*5-a[2]个本应该是2*2的空间
        //于是剩下((a[4]*5-a[2])*41*1的空间
        //“尽可能”地装吧
    a[2]-=min(5*a[4],a[2]);
}//4*4

<4>装3*3的箱子:好的我们来到最困难的一部分。之所以说最困难,因为装3*3要考虑3*3+2*2+1*1,想想就头大。
没事的,我们一步步稳重地慢慢解
根据我们的解析,我们要先装大箱子。很幸运地,我们发现4个3*3可以不留缝的合成6*6的空间!也就是说我们的混装部分只有一个包裹,这为我们减轻了许多困难。
下一步,装2*2的箱子。不同的装法如图所示

鉴于2*2箱子的数量实在太麻烦了,我们可以写一个映射函数【或者数组】来表明不同数量的3*3箱子应该对应多少2*2箱子的数量,然后尽可能地装
下一步就较简单咯,用1*1尽可能地去装剩余的空间
代码:

int a[10];
int f(int x)
{
    if(x==0) return 0;
    if(x==1) return 5;
    if(x==2) return 3;
    if(x==3) return 1;
}
...
{
    ans+=ceil(a[3]/4.0);//每4个一包裹,不足4个向上取整
    a[3]=a[3]%4;//混装的3*3箱子数量
    if(a[3]!=0)//如果等于0就没有剩余的空间~
    {
        a[1]-=min(36-a[3]*9-min(f(a[3]),a[2])*4,a[1]);
        //36-a[3]*9-min(f(a[3]),a[2])*4这一串表示剩下的空间
        //36-总空间,a[3]*9-3*3箱子占的空间
        //min(f(a[3]),a[2])-所需2*2箱子的数量
        a[2]-=min(f(a[3]),a[2]);
    }
}//3*3

<5>装2*2的箱子:简单啦,直接上代码好了:

int a[10];
...
{
    ans+=ceil(a[2]/9.0);
    a[2]=a[2]%9;
    if(a[2]!=0)
        a[1]-=min(36-a[2]*4,a[1]);
}//2*2

<6>装1*1的箱子:

int a[10];
...
{
    ans+=ceil(a[1]/36.0);
}//2*2

然后,再把这些残片组合起来就是——
AC代码!

#include<cstdio>
#include<cmath>
int a[10];
int min(int x,int y)
{
    if(x<y) return x;
    else return y;
}
int f(int x)
{
    if(x==0) return 0;
    if(x==1) return 5;
    if(x==2) return 3;
    if(x==3) return 1;
}
int main()
{
    while(true)
    {
        int ans=0,S=0;
        for(int i=1;i<=6;i++) 
        {
            scanf("%d",&a[i]);
            S+=a[i];
        }
        if(S==0) return 0;
        {
            ans+=a[6];
        }//6*6
        {
            ans+=a[5];
            a[1]-=min(11*a[5],a[1]);
        }//5*5
        {
            ans+=a[4];
            if(5*a[4]>a[2]) 
                a[1]-=min((a[4]*5-a[2])*4,a[1]);
            a[2]-=min(5*a[4],a[2]);
        }//4*4
        {
            ans+=ceil(a[3]/4.0);
            a[3]=a[3]%4;
            if(a[3]!=0)
            {
                a[1]-=min(36-a[3]*9-min(f(a[3]),a[2])*4,a[1]);
                a[2]-=min(f(a[3]),a[2]);
            }
        }//3*3
        {
            ans+=ceil(a[2]/9.0);
            a[2]=a[2]%9;
            if(a[2]!=0)
                a[1]-=min(36-a[2]*4,a[1]);
        }//2*2
        {
            ans+=ceil(a[1]/36.0);
        }//1*1
        printf("%d\n",ans);
    }
}

@小结@

此题在贪心中算是简单的一类。思路本身不复杂,只是说代码量很大,容易将一些细节弄错。总体来说,明确贪心标准,问题就迎刃而解了!


<2>区间系列问题-【交叉、插点、覆盖、后而贪心】

贪心算法->区间:总共有3类问题,都是区间相关+贪心策略
1-选择不相交区间问题
2-区间选点问题
3-区间覆盖问题
我们一个一个来介绍,不必着急

1.选择不相交区间问题

首先看一下问题的模型:

给定n个半开区间[ai, bi),选择尽量多个区间,使得这些区间两两没有公共点。

描述得很简单,也很明了,甚至明了到无从下手。
先从贪心的结构——最优子结构开始讨论较为简便。在部分数轴[S,T]之间,假设区间A是最优解中的最左区间,则A的右端点R到T之间即[R,T]之间又形成了一个新的子问题。我们既然要最多——就要贪心,让[R,T]尽可能的长
贪心标准:循环选择区间[L,R)使得在[S,T]中[R,T]最长
然后再来看看实现方法。
区间[R,T]中,T是固定右端点,则当R是所有区间的最左边时可以保证整个区间最长。即:对区间以【右端点】为关键字排序,使左边【小】的在前,然后再依次扫描。
关键部分代码:

struct Node{ 
    int Begin,End; 
}a[MN+5]; 
int Tot=0,Start=0;
...
sort(a+1,a+n+1,cmp); 
for(int i=1;i<=n;i++) 
    if(a[i].Begin>=Start) 
    { 
        Start=a[i].End; 
        Tot++;
    } 

【正确性证明】我们按照b1<=b2<=b3…的方式排序【bi是右端点哦~题目中的描述不知道大家忘没忘】,假设我们不选最左边的b1,而是选择了bi。如果区间1与区间i相交,则将bi->b1,总数不变,且不影响后面的结果;否则,加入b1后总数+1,且不影响后面的结果。

2.区间选点问题

依然地,先上模型:

给定n个闭区间[ai, bi],在数轴上选尽量少的点,使得每个区间内都至少有一个点(不同区间内含的点可以是同一个)。

通过读题我们可以直观地给出一个思路:尽量在区间的相交部分上选点
【为了以防大家的思路跑偏,先稍稍提醒一下,我们的操作对象应该是区间而不是整条数轴】
先简化为两个区间相交的问题:若[a1,b1]与[a2,b2]有相交(b1<=b2),则相交部分的k可以使得a1<=k<=b1且a2<=k<=b2。合起来就是(a1&&a2)<=k<=b1<=b2。观察发现b1一定是满足的【即b1一定在相交部分】,也可以证明出。
证明:从(a1&&a2)<=k<=b1<=b2,若k=b1,得证;若k

struct Node{ 
    int Begin,End; 
}a[MN+5]; 
bool b[MX+5]
...
sort(a+1,a+n+1,cmp); 
for(int i=1;i<=n;i++)
{
    bool flag=false;
    for(int j=a[i].Begin;j<=a[i].End;j++)
        if(b[j])
        {
            flag=true;
            break;
        }//如果区间里已经存在了一个点
    if(flag) concinue;
    b[a[i].End]=true;
}

2.*区间选点问题-应用

【NOIP2010模拟】种树
题目描述
一条街的一边有几座房子。因为环保原因居民想要在路边种些树。路边的地区被分割成块,并被编号成1..N。每个部分为一个单位尺寸大小并最多可种一棵树。每个居民想在门前种些树并指定了三个号码B,E,T。这三个数表示该居民想在B和E之间最少种T棵树。当然,B≤E,居民必须记住在指定区不能种多于区域地块数的树,所以T≤E-B+l。居民们想种树的各自区域可以交叉。你的任务是求出能满足所有要求的最少的树的数量。 写一个程序计算最少要种树的数量
输入
一行包含数据N,区域的个数(0<N≤30000); 第二行包含H,房子的数目(0<H≤5000); 下面的H行描述居民们的需要:B E T,0<B≤E≤30000,T≤E-B+1。
输出
输出为满足所有要求的最少树的数量。
样例输入
9 4
1 4 2
4 6 2
8 9 2
3 5 2
样例输出
5
看上去的确和区间选点很相似——但请注意!题目中需要树的数量是不定的,而不是原型中的【1】。
那怎么呢?难道就不能借用区间选点思想了吗?
当然要借用!只不过需要小改一番
贪心标准:从区间的右端点起从右往左选点,直到数量足够
证明也很容易,将上面的正面稍微改改即可。
代码【Duang~】:

#include<cstdio> 
#include<algorithm> 
using namespace std; 
struct Node{ 
    int Begin,End,WM; 
}a[5005]; 
bool Vis[3005]; 
bool cmp(Node a,Node b) 
{ 
    if(a.End<b.End) return 1; 
    else return 0; 
} 
int main() 
{ 
    int ans=0,n,h; 
    scanf("%d %d",&n,&h); 
    for(int i=1;i<=h;i++) 
        scanf("%d %d %d",&a[i].Begin,&a[i].End,&a[i].WM); 
    sort(a+1,a+h+1,cmp); 
    for(int i=1;i<=h;i++) 
    { 
        for(int j=a[i].Begin;j<=a[i].End;j++) 
        { 
            if(a[i].WM<=0) break; 
            if(Vis[j]) a[i].WM--; 
        } 
        for(int j=a[i].End;j>=a[i].Begin;j--) 
        { 
            if(a[i].WM<=0) break; 
            if(!Vis[j])  
            { 
                Vis[j]=true; 
                a[i].WM--; 
            } 
        } 
    } 
    for(int i=1;i<=n;i++) 
        if(Vis[i]) ans++; 
    printf("%d\n",ans); 
} 

3.区间覆盖问题

先看原型:

给n个闭区间[ai,bi],选择尽量少的区间覆盖一条指定线段[s,t]。

嗯。
从思考的角度来说,这道题与例<1>较为相近。即:我们选择一个包含左端点s的区间[L,R],剩下[R,t]这一条等待覆盖,所以拥有最优子结构。那么我们要贪心的就是:
贪心标准:循环选择包含线段左端点s的区间[L,R]使得[R,t]最短
虽然思路很清晰,但是真正实现起来不易,尤其是当某些题数据大到必须O(n)才能过,这个时候就需要我们好好去想想代码怎么写了。首先考虑包含s,则我们需要以【左端点】为关键字排序。然后考虑最短,可以用Max变量记录最大的R。如果区间i已经不包含s,则它以后的也都不包含,这时就可以更新s为Max。然后从i再开始【i以前的都不可能是最优解】,依次直到s>=t,覆盖完毕。
还有就是要考虑无解的情况。如果没有一个区间能覆盖s,则无解。

struct Node{  
    int Begin,End;  
}a[MN+5];
bool flag=false;
int ans=0;
...
for(int i=1;i<=n;i++)  
{  
    if(a[i].Begin>S&&flag==true)  
    {  
        S=Max;  
        ans++;  
        flag=false;  
        if(S>=T) break; 
    }  
    if(a[i].Begin>S&&flag==false)  
    {  
        printf("No Solution\n");  
        return 0;  
    }  
    Max=max(Max,a[i].End);  
    flag=true;  //有区间可以覆盖s
}

@小结@

区间问题也算是贪心的一个经典应用。对于一开始接触的人可能是一脸懵,但是如果经过冷静地分析,从贪心的结构入手,依靠点小小的直觉,也就发现解法并非很困难。区间问题不只这最简单的三种,其的变式很多,但都逃不过贪心的范畴:排序,子问题求解。


<3>背包0-1-【不是所有问题都叫贪心】

@题目描述@

有 n 件物品, 每件物品有一个价值和一个重量,分别记为: v1,v2, …vn与w1,w2, …wn 其中所有的 重量wi 均为整数。 现有一个背包,其最大载重量为W,要求从这n件物品中任取若干件(这些物品要么被装入要么被留下)。问背包中装入哪些物品可使得所装物品的价值和最大?

这道题与另一道贪心可以做的背包问题——【部分背包】都同属于背包问题。但,究竟这道题我们是否能故技重施呢?

@贪心标准-错误@

如果能贪心,这里的贪心标准应该是什么呢?重量?价值?还是单位价值?
显然重量是不靠谱的,假设N=2,背包最大载重量是100,两种物品A、B的重量wi分别为1,100,其对应的价值vi分别为1,100。一个反例就被构造出来。【毕竟重量连部分背包都过不了】
其他两个标准也可构造反例。这里我们假设N=3,背包最大载重量是100,三种物品A、B、C的重量分别是45,50,70,其对应的总价值分别是90、100、150。情况A是贪心,情况B是动规,显然情况B是正解。
0-1背包
那么,这道背包问题真的就不能贪心了吗?
是的。
不管怎么选择贪心标准,总会被举出反例,这就是这道题。

@想说明的东西@

为什么我要在一个贪心的例题中加入一道不是贪心的东西?因为,我想说:尽管证明出问题能使用贪心是个很方便的途径,但如果在一道不是贪心的题目上耗费时间用贪心,便会得不偿失。理智地使用更全面的动态规划,才能稳扎稳打。
贪,而不得,反失。


<4>Huffman编码-【合并,压缩,再压缩】

@编码简介@

编码是信息从一种形式或格式转换为另一种形式的过程

我们通常用的文字【包括汉字与字母】都不能直接在电脑中存储,电脑当中只能存储一系列的0-1串。那么,我们就需要将阅读的【信息-文字】->电脑的【信息-01串】,这一过程就叫编码。
举个栗子:我们平常所用到的ASCII码就属于已经将【字母】这种信息编为了0-1串的码。我们输入法中的【汉字】对应的信息也是一种被编为0-1串的码。
这里就会涉及一个问题:如果我所传达的信息过长,那么系统会消耗过多的空间。这个时候,我们极其希望有一种能够压缩数据的方法,减少系统空间的消耗。
若是针对某一段信息,我选择给频率高的字母编短码,给频率低的字母编长码,就可以大大减少我们所消耗的空间。像这样给一组信息的每一个单位信息编长度不一的码,我们称为变长码。
相对的,有定长码一称。
举个栗子:
变长码例子
定长码所需空间=( 453+133+123+163+93+53 )=300千位
变长码所需空间=( 451+133+123+163+92+52 )=227千位
少了近1/4的空间。
当数据量大到爆炸时,这1/4的空间可不只一点两点。所以,将编码方式改成变长码是很重要。
那么,我们将注意转到变长码上来吧。看看所谓的最优编码是否真正存在。

@二叉树编码@

如果你有想到0-1串与二叉树的联系,那么恭喜你,离成功又近了一步!
二叉树编码
我们将根结点向左子树的路径记为0,向右子树的路径记为1。那么,当从根结点走到某一结点时就会产生一个0-1串——若将要编码的信息放在结点上,不正就对应一种编码规则吗?
这里我们在引入前缀码的概念:

前缀:设a=b1b2…bn,bi∈{0,1}是一个0-1序列(符号串)。序列b= b1b2…bi >(1 i n)称为a的前缀。
.例如,设a=010, 则, 0, 01 ,010都是a的前缀.
前缀码: 设Q ={a1, a2, …, am}是一个0~1序列集合 . 如果Q中没有一个序列是另一个序列的前缀 , 则称Q为前缀码.
例如,{0,10,110}就是一个前缀码,而{0,10,101}就不是前缀码。

举个栗子:假设一种编码规则中a=101,b=10,c=100,d=00【注意到b是a的前缀】。那么【10100】到底意味着”ad”还是”bc”呢?出现了矛盾。
可证:前缀码不会出现矛盾。
将前缀码对应到二叉树上,会发现前缀码的意义就是将信息放到叶结点上。我们再将叶结点加上权值【看到上图了吗】,使权值=叶结点上信息的出现频率。那么,让我们再向前,大大地迈一步吧!

@最优二叉树-问题解决@

一个简单的引入:我们定义一个函数 B(T)=cCc=1frep(c)L(c) 来计算我们编码规则所需的空间量。其中 T 是要传达的整体信息,集合 C 是所有单位信息的集合,函数 frep(c) 定义为 c 的出现频率, L(c) 定义为 c 的编码长度。
相应地,在二叉树编码中, T 是二叉树本身,集合 C 是所有叶结点的集合,函数 frep(c) 定义为 c 的权, L(c) 定义为 c 的深度。
让我们直接走向历史的先哲为我们锻造好的路途,省去弯曲的歧路,直接引出答案吧:

给定 n 个权值作为 n 个叶子结点,构造一棵二叉树 T ,若 B(T) 达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。

假设有n个权值,则构造出的哈夫曼树有n个叶子结点。 n个权值分别设为 w1、w2、…、wn,则哈夫曼树的构造规则为:
(1) 将w1、w2、…,wn看成是有n 棵树的森林(每棵树仅有一个结点);
(2) 在森林中选出两个根结点的权值最小的树合并,作为一棵新树的左、右子树,且新树的根结点权值为其左、右子树根结点权值之和;
(3)从森林中删除选取的两棵树,并将新树加入森林;
(4)重复(2)、(3)步,直到森林中只剩一棵树为止,该树即为所求得的哈夫曼树。

利用哈夫曼树来设计二进制的前缀编码,既满足前缀编码的条件,又保证编码总长最短。
点我!查看百科原址

也就是说,我们每次只【鼠目寸光】地选择最小的频率,把它放在最深的一层。
符合我们变长码的目的:给频率高的字母编短码,给频率低的字母编长码
但是,这么草率的贪心选择,足够正确吗?

@贪心标准-正确性证明@

【注意!以下内容可能过于枯燥且无味!请小心食用!】
【以下证明来自《算法导论》】:

引理1:若 x,y 是权最小的两个叶结点,那么存在一个最优生成树,使得 x,y 是兄弟结点,且它们深度最深。
引理-1-例图
【证明】:令 T 是任意一种最优生成树【如图】,且 a,b 为其深度最深的兄弟结点。不失一般性,我们不妨假设 frep(y)>=frep(x) frep(b)>=frep(a) 。因为我们已经假定x,y权最小,所以必有 frep(b)>=frep(a)>=frep(y)>=frep(x)
现在我们通过交换结点 a 与结点 x ,生成新树 T 。那么

B(T)B(T)=(frep(a)L(a)+frep(x)L(x))(frep(a)L(a)+frep(x)L(x))=(frep(a)L(a)+frep(x)L(x))(frep(a)L(x)+frep(x)L(a))=(frep(a)frep(x))(L(a)L(x)>=0

所以 B(T)>=B(T) ,也就是说 T 只会更好不会更差。
同理,交换结点 b 与结点 y 所形成的新树只会更好不会更差。
原命题得证。
引理2:设 T T 去掉权值最小叶结点 x,y ,将它们父亲的权设为 x 的权与 y 的权之和的这样一棵树。假设 T 是最优生成树,则 T 也是最优生成树。
【证明】:先用 T 去表示原树 T 。可以得到:
B(T)B(T)=frep(z)L(z)(frep(x)+frep(y))L(x)

因为 L(z)=L(x)1 frep(z)=frep(x)+frep(y) ,代入原式可得 B(T)=B(T)(frep(x)+frep(y))
用反证法。假设最优生成树为 F 而不是 T ,不失一般性的,由 引理1可知权值最小叶结点 x,y 是兄弟结点。令 F F 去掉 x,y ,将它们父亲的权设为 x 的权与 y 的权之和的这样一棵树。于是:
B(F)=B(F)(frep(x)+frep(y))<B(T)(frep(x)+frep(y))=B(T)

与命题中的 T 是最优生成树矛盾。
原命题得证。

于是最优生成树的构造一定成立。
【没错我就是复制隔壁我自己的博客233】

@代码@

#include<cstdio>
#include<cstring>
struct Node{
    int value,num;
}heap[55];
int left[105],right[105],pre[105];
int heap_len,ans[55];
char ch[55];
void swap(Node &a,Node &b)
{
    Node t=a;a=b;b=t;
}
void insert(Node p)
{
    heap[++heap_len]=p;
    int k=heap_len;
    while(k!=1&&heap[k/2].value>heap[k].value)
    {
        swap(heap[k/2],heap[k]);
        k/=2;
    }
}
Node top()
{
    Node p=heap[1];
    swap(heap[1],heap[heap_len]);
    heap[heap_len].value=heap[heap_len].num=0;
    heap_len--;
    int k=1;
    while(k*2<=heap_len)
    {
        if(heap[k].value>heap[k*2].value)
        {
            if(k*2+1<=heap_len&&heap[k*2].value>heap[k*2+1].value)
            {
                swap(heap[k],heap[k*2+1]);
                k=k*2+1;
            }
            else
            {
                swap(heap[k],heap[k*2]);
                k=k*2;
            }
        }
        else if(k*2+1<=heap_len&&heap[k].value>heap[k*2+1].value)
        {
            swap(heap[k],heap[k*2+1]);
            k=k*2+1;
        }
        else break;
    }
    return p;
}
void Print(int root,int len)
{
    if(left[root]==0&&right[root]==0)
    {
        printf("%c->",ch[root]);
        for(int i=1;i<=len;i++)
            printf("%d",ans[i]);
        printf("\n");
        return ;
    }
    Print(left[root],len+1);
    ans[len+1]=1;
    Print(right[root],len+1);
}
int main()
{
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        int x;char c;Node p;
        scanf("\n%c %d",&c,&x);
        p.num=i;p.value=x;
        ch[i]=c;
        insert(p);
    }
    for(int i=1;i<n;i++)
    {
        Node a=top();
        Node b=top();
        Node c;
        c.value=a.value+b.value;
        c.num=n+i;
        pre[a.num]=pre[b.num]=c.num;
        left[c.num]=a.num;right[c.num]=b.num;
        insert(c);
    }
    int root=0;
    for(int i=1;i<2*n;i++)
        if(pre[i]==0)   root=i;
    Print(root,0);
}

读入n,再读入n行,每行【需要编码的字母 频率】。
输出n行,每行【需要编码的字母->编码结果】。


@结束语@

恩。
贪心是一种常用的思想,用于求解最优化问题,它总是考虑眼前的最优化,而不是全局的最优化。但对于某些问题,贪心却总能找到正确的途径。贪心和动规是分不开的,可以这么说:动规走完了所有可能的途径,而贪心只走了一种途径走到底。
对于竞赛来讲,如果一个最优化问题不符合动规的【无后效性】,或者数据量过大不宜用动规,那么贪心是一种不错的考虑方向。
我所选取的4道题,各有各的道理。<1>是一个贪心的简单应用;<2>是用贪心思想解题的一个模型/类;<3>深刻说明了贪心的代价——如果不宜贪心,那么就会:贪,而不得,反失;<4>则是选取了贪心的一个漂亮的理论:哈夫曼树,许多的题都可以使用这个理论解题。
其实吧,这里应该还有一个<5>用来揭示贪心与拟阵的联系,可惜我学识疏浅,无法向大家交代一个我自己都无法完全理解的东西。如果大家有兴趣,可以另搜搜“拟阵 贪心”的博客,我只能告退了。
有的时候,贪心不是用来解题而是用来优化其他的算法。这时候的贪心可就真是一种【思想】而不是一种【算法】。大家也不要被【贪心->算法】这一想法禁锢着。
毕竟,在现实中,【贪婪】的思想也会在脑中残存。

End

就是这样,新的一天里,也请多多关照哦(ノω<。)ノ))☆.。~

猜你喜欢

转载自blog.csdn.net/tiw_air_op1721/article/details/77387949
今日推荐