贪心算法详解
贪心算法思想:
顾名思义,贪心算法总是作出在当前看来最好的选择。也就是说贪心算法并不从整体最优考虑,它所作出的选择只是在某种意义上的局部最优选择。当然,希望贪心算法得到的最终结果也是整体最优的。虽然贪心算法不能对所有问题都得到整体最优解,但对许多问题它能产生整体最优解。如单源最短路经问题,最小生成树问题等。在一些情况下,即使贪心算法不能得到整体最优解,其最终结果却是最优解的很好近似。
贪心算法的基本要素:
1.贪心选择性质。所谓贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。
动态规划算法通常以自底向上的方式解各子问题,而贪心算法则通常以自顶向下的方式进行,以迭代的方式作出相继的贪心选择,每作一次贪心选择就将所求问题简化为规模更小的子问题。
对于一个具体问题,要确定它是否具有贪心选择性质,必须证明每一步所作的贪心选择最终导致问题的整体最优解。
2. 当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。问题的最优子结构性质是该问题可用动态规划算法或贪心算法求解的关键特征。
贪心算法的基本思路:
从问题的某一个初始解出发逐步逼近给定的目标,以尽可能快的地求得更好的解。当达到算法中的某一步不能再继续前进时,算法停止。
该算法存在问题:
1. 不能保证求得的最后解是最佳的;
2. 不能用来求最大或最小解问题;
3. 只能求满足某些约束条件的可行解的范围。
实现该算法的过程:
从问题的某一初始解出发;
while 能朝给定总目标前进一步 do
求出可行解的一个解元素;
由所有解元素组合成问题的一个可行解;
用背包问题来介绍贪心算法:
背包问题:有一个背包,背包容量是M=150。有7个物品,物品可以分割成任意大小。要求尽可能让装入背包中的物品总价值最大,但不能超过总容量。
物品 A B C D E F G
重量 35 30 60 50 40 10 25
价值 10 40 30 50 35 40 30
分析如下
目标函数: ∑pi最大
约束条件是装入的物品总重量不超过背包容量:∑wi<=M( M=150)。
(1)根据贪心的策略,每次挑选价值最大的物品装入背包,得到的结果是否最优?
(2)每次挑选所占重量最小的物品装入是否能得到最优解?
(3)每次选取单位重量价值最大的物品,成为解本题的策略。
值得注意的是,贪心算法并不是完全不可以使用,贪心策略一旦经过证明成立后,它就是一种高效的算法。
贪心算法还是很常见的算法之一,这是由于它简单易行,构造贪心策略不是很困难。
可惜的是,它需要证明后才能真正运用到题目的算法中。
一般来说,贪心算法的证明围绕着:整个问题的最优解一定由在贪心策略中存在的子问题的最优解得来的。
对于背包问题中的3种贪心策略,都是无法成立(无法被证明)的,解释如下:
贪心策略:选取价值最大者。反例:
W=30
物品:A B C
重量:28 12 12
价值:30 20 20
根据策略,首先选取物品A,接下来就无法再选取了,可是,选取B、C则更好。
(2)贪心策略:选取重量最小。它的反例与第一种策略的反例差不多。
(3)贪心策略:选取单位重量价值最大的物品。反例:
W=30
物品:A B C
重量:28 20 10
价值:28 20 10
根据策略,三种物品单位重量价值一样,程序无法依据现有策略作出判断,如果选择A,则答案错误。但是果在条件中加一句当遇见单位价值相同的时候,优先装重量小的,这样的问题就可以解决.
所以需要说明的是,贪心算法可以与随机化算法一起使用,具体的例子就不再多举了。(因为这一类算法普及性不高,而且技术含量是非常高的,需要通过一些反例确定随机的对象是什么,随机程度如何,但也是不能保证完全正确,只能是极大的几率正确)。
网上对于这个装包问题的描述就就只有这些,但是在这里我还是要写一下,假设条件是什么?假设条件是上述几种反例的情况不存在的时候该如何求解:
#include <iostream>
using namespace std;
struct Node
{
float weight;
float value;
bool mark;
char char_mark;
float pre_weight_value;
};
int main(int argc, char* argv[])
{
float Weight[7] = {35,30,60,50,40,15,20};
float Value [7] = {10,40,30,50,35,40,30};
Node array[7];
for(int i=0; i<7; i++)
{
array[i].value = Value[i];
array[i].weight = Weight[i];
array[i].char_mark = 65 + i;
array[i].mark = false;
array[i].pre_weight_value = Value[i] / Weight[i];
}
for(i=0;i<7;i++)
cout<<array[i].pre_weight_value<<" ";
cout<<endl;
float weight_all=0.0;
float value_all = 0.0;
float max = 0.0;
char charArray[7];
int flag,n = 0;
while(weight_all <= 150)
{
for(int index=0;index < 7; ++index)
{
if(array[index].pre_weight_value > max && array[index].mark == false)
{
max = array[index].pre_weight_value ;
flag = index;
}
}
charArray[n++] = array[flag].char_mark;
array[flag].mark = true;
weight_all += array[flag].weight;
value_all += array[flag].value;
max = 0.0;
}
for(i=0;i<n-1;i++)
cout<<charArray[i]<<" ";
cout<<endl;
cout<<"weight_all:"<<weight_all- array[n-1].weight<<endl;
cout<<"value_all:"<<value_all<<endl;
system("pause");
return 0;
}
下面我要说的是,这个算法里面就是采用的贪心第三方案,一般这个方案是成功率最大的,其他两个方案我在这里没有考虑,在这里得到的结果是利用了115容量装了价值195的东西,但是这明显不是最优结果,分明还可以装一个A进去!刚好满足150重量,由于在算法中我单纯的利用第三种贪心方法求解,当剩余的包裹中最优的再加进来的时候已经超过了,所以这个时候可以选择剩余包裹中次优的(如这里选择A),再不行就次次优的,尽量把包裹装满,这样得到的结果就很接近了(不保证一定为最优),但是我们一般不这样来求解,下一文章会介绍动态规划算法来解决这个问题,动态规划很好的弥补了贪心算法的不足!
经自己总结的贪心算法几大经典问题:
1:活动时间安排的问题
设有N个活动时间集合,每个活动都要使用同一个资源,比如说会议场,而且同一时间内只能有一个活动使用,每个活动都有一个使用活动的开始si和结束时间fi,即他的使用区间为(si,fi),现在要求你分配活动占用时间表,即哪些活动占用该会议室,哪些不占用,使得他们不冲突,要求是尽可能多的使参加的活动最大化,即所占时间区间最大化!
上图为每个活动的开始和结束时间,我们的任务就是设计程序输出哪些活动可以占用会议室!
#include <iostream>
using namespace std;
void GreedyChoose(int len,int *s,int *f,bool *flag);
int main(int argc, char* argv[])
{
int s[11] ={1,3,0,5,3,5,6,8,8,2,12};
int f[11] ={4,5,6,7,8,9,10,11,12,13,14};
bool mark[11] = {0};
GreedyChoose(11,s,f,mark);
for(int i=0;i<11;i++)
if(mark[i])
cout<<i<<" ";
system("pause");
return 0;
}
void GreedyChoose(int len,int *s,int *f,bool *flag)
{
flag[0] = true;
int j = 0;
for(int i=1;i<len;++i)
if(s[i] >= f[j])
{
flag[i] = true;
j = i;
}
}
得出结果是 0 3 7 10,也就是对应的时间段
值得说明一下,虽然贪心算法不是一定可以得到最好的解 ,但是对于这种活动时间的问题,他却得到的总是最优解,这点可以用数学归纳法证明,在这里,体现出来的贪心策略是:每一个活动时间的挑选总是选择最优的,就是刚好匹配的,这样得出的结果也就是最优的了!由于这个算法很简单,在这里就没有注释了!
类似这种题还有个区间覆盖问题,就是说很多个区间,其中有些是相互覆盖着的,要求去除多余的区间,使剩下的区间占用长度最大,实际就是这个题,只是问法变换了而已!接下来让我们看线性覆盖的问题,跟上面的相反!
2.贪心实例之线段覆盖(lines cover)
题目大意:
在一维空间中告诉你N条线段的起始坐标与终止坐标,要求求出这些线段一共覆盖了多大的长度。
为了方便说明,我们采用上述表格中的数据代表10条线段的起始点和终点,注意,这里是用起始点为顺序进行排列,和上面的不一样,知道了这些我们就可以着手开始设计这个程序:
#include <iostream>
using namespace std;
int main(int argc, char* argv[])
{
int s[10] = {2,3,4,5,6,7,8,9,10,11};
int f[10] = {3,5,7,6,9,8,12,10,13,15};
int TotalLength = (3-2);
for(int i=1,int j=0; i<10 ; ++i)
{
if(s[i] >= f[j])
{
TotalLength += (f[i]-s[i]);
j = i;
}
else
{
if(f[i] <= f[j])
continue;
else
{
TotalLength += f[i] - f[j];
j = i;
}
}
}
cout<<TotalLength<<endl;
system("pause");
return 0;
}
运行结果为13,显然这是我们需要的结果,这里注明一下,上面图表中数据有点问题,实际以程序中给出的为主!
3,:数字组合问题!
设有N个正整数,现在需要你设计一个程序,使他们连接在一起成为最大的数字,例3个整数 12,456,342 很明显是45634212为最大,4个整数 342,45,7,98显然为98745342最大
程序要求:输入整数N 接下来一行输入N个数字,最后一行输出最大的那个数字!
题目解析:拿到这题目,看起要来也简单,看起来也难,简单在什么地方,简单在好像就是寻找哪个开头最大,然后连在一起就是了,难在如果N大了,假如几千几万,好像就不是那么回事了,要解答这个题目需要选对合适的贪心策略,并不是把数字由大排到小那么简单,网上的解法是将数字转化为字符串,比如a+b和b+a,用strcmp函数比较一下就知道谁大,也就知道了谁该排在谁前面,不过我觉得这个完全没必要,在这里我采用一种比较巧妙的方法来解答,不知道大家还记得冒泡排序法不,那是排序最早接触的一种方法,我们先看看它的源代码:
#include <iostream>
using namespace std;
int main(int argc, char* argv[])
{
int array[10];
for(int i=0;i<10;i++)
cin>>array[i];
int temp;
for(i=0; i<=9 ; ++i)
for(int j=0;j<10-1-i;j++)
if(array[j] > array[j+1] )
{
temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
for(i=0;i<10;i++)
cout<<array[i]<<" ";
cout<<endl;
system("pause");
return 0;
}
相信这种冒泡已经很熟悉了,注意看程序中最核心的比较规则是什么,是这一句
if
(array[j] > array[j+1] ) 他是以数字大小作为比较准则来返回
true
或者是
false
,那么我们完全可以改变一下这个排序准则,比如23,123,这两个数字,在我们这个题中它可以组成两个数字 23123和12323,分明是前者大些,所以我们可以说23排在123前面,也就是23的优先级比123大,123的优先级比23小,所以不妨写个函数,传递参数a和b,如果ab比ba大,则返回
true
,反之返回
false
,
只需要把这个比较准则加到关键的地方,这个题就算完成了,最终代码如下:
#include <iostream>
#include <cmath>
using namespace std;
bool compare(int Num1,int Num2);
int main(int argc, char* argv[])
{
int N;
cout<<"please enter the number n:"<<endl;
cin>>N;
int *array = new int [N];
for(int i=0;i<N;i++)
cin>>array[i];
int temp;
for(i=0; i<=N-1 ; ++i)
{
for(int j=0;j<N-i-1;j++)
if( compare(array[j],array[j+1]) )
{
temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
}
cout<<"the max number is:";
for( i=N-1 ; i>=0 ; --i)
cout<<array[i];
cout<<endl;
delete [] array;
system("pause");
return 0;
}
bool compare(int Num1,int Num2)
{
int count1=0,count2=0;
int MidNum1 = Num1,MidNum2 = Num2;
while( MidNum1 )
{
++count1;
MidNum1 /= 10;
}
while( MidNum2 )
{
++count2;
MidNum2 /= 10;
}
int a = Num1 * pow(10,count2) + Num2;
int b = Num2 * pow(10,count1) + Num1;
return (a>b)? true:false; }
4.钱币找零问题
这个问题在我们的日常生活中就更加普遍了。假设1元、2元、5元、10元、20元、50元、100元的纸币分别有c0, c1, c2, c3, c4, c5, c6张。现在要用这些钱来支付K元,至少要用多少张纸币?用贪心算法的思想,很显然,每一步尽可能用面值大的纸币即可。在日常生活中我们自然而然也是这么做的。在程序中已经事先将Value按照从小到大的顺序排好。
#include<iostream>
#include<algorithm>
using namespace std;
const int N=7;
int Count[N]={3,0,2,1,0,3,5};
int Value[N]={1,2,5,10,20,50,100};
int solve(int money)
{
int num=0;
for(int i=N-1;i>=0;i--)
{
int c=min(money/Value[i],Count[i]);
money=money-c*Value[i];
num+=c;
}
if(money>0) num=-1;
return num;
}
int main()
{
int money;
cin>>money;
int res=solve(money);
if(res!=-1) cout<<res<<endl;
else cout<<"NO"<<endl;
}
5.多机调度问题
n个作业组成的作业集,可由m台相同机器加工处理。要求给出一种作业调度方案,使所给的n个作业在尽可能短的时间内由m台机器加工处理完成。作业不能拆分成更小的子作业;每个作业均可在任何一台机器上加工处理。这个问题是NP完全问题,还没有有效的解法(求最优解),但是可以用贪心选择策略设计出较好的近似算法(求次优解)。当n<=m时,只要将作业时间区间分配给作业即可;当n>m时,首先将n个作业从大到小排序,然后依此顺序将作业分配给空闲的处理机。也就是说从剩下的作业中,选择需要处理时间最长的,然后依次选择处理时间次长的,直到所有的作业全部处理完毕,或者机器不能再处理其他作业为止。如果我们每次是将需要处理时间最短的作业分配给空闲的机器,那么可能就会出现其它所有作业都处理完了只剩所需时间最长的作业在处理的情况,这样势必效率较低。在下面的代码中没有讨论n和m的大小关系,把这两种情况合二为一了。
#include<iostream>
#include<algorithm>
using namespace std;
int speed[10010];
int mintime[110];
bool cmp( const int &x,const int &y)
{
return x>y;
}
int main()
{
int n,m;
memset(speed,0,sizeof(speed));
memset(mintime,0,sizeof(mintime));
cin>>n>>m;
for(int i=0;i<n;++i) cin>>speed[i];
sort(speed,speed+n,cmp);
for(int i=0;i<n;++i)
{
*min_element(mintime,mintime+m)+=speed[i];
}
cout<<*max_element(mintime,mintime+m)<<endl;
}
6、小船过河问题
POJ1700是一道经典的贪心算法例题。题目大意是只有一艘船,能乘2人,船的运行速度为2人中较慢一人的速度,过去后还需一个人把船划回来,问把n个人运到对岸,最少需要多久。先将所有人过河所需的时间按照升序排序,我们考虑把单独过河所需要时间最多的两个旅行者送到对岸去,有两种方式:
1.最快的和次快的过河,然后最快的将船划回来;次慢的和最慢的过河,然后次快的将船划回来,所需时间为:t[0]+2*t[1]+t[n-1];
2.最快的和最慢的过河,然后最快的将船划回来,最快的和次慢的过河,然后最快的将船划回来,所需时间为:2*t[0]+t[n-2]+t[n-1]。
算一下就知道,除此之外的其它情况用的时间一定更多。每次都运送耗时最长的两人而不影响其它人,问题具有贪心子结构的性质。
#include<iostream>
#include<algorithm>
using namespace std;
int main()
{
int a[1000],t,n,sum;
scanf("%d",&t);
while(t--)
{
scanf("%d",&n);
sum=0;
for(int i=0;i<n;i++) scanf("%d",&a[i]);
while(n>3)
{
sum=min(sum+a[1]+a[0]+a[n-1]+a[1],sum+a[n-1]+a[0]+a[n-2]+a[0]);
n-=2;
}
if(n==3) sum+=a[0]+a[1]+a[2];
else if(n==2) sum+=a[1];
else sum+=a[0];
printf("%d\n",sum);
}
}
6、区间覆盖问题
POJ1328是一道经典的贪心算法例题。题目大意是假设海岸线是一条无限延伸的直线。陆地在海岸线的一侧,而海洋在另一侧。每一个小的岛屿是海洋上的一个点。雷达坐落于海岸线上,只能覆盖d距离,所以如果小岛能够被覆盖到的话,它们之间的距离最多为d。题目要求计算出能够覆盖给出的所有岛屿的最少雷达数目。对于每个小岛,我们可以计算出一个雷达所在位置的区间。
问题转化为如何用尽可能少的点覆盖这些区间。先将所有区间按照左端点大小排序,初始时需要一个点。如果两个区间相交而不重合,我们什么都不需要做;如果一个区间完全包含于另外一个区间,我们需要更新区间的右端点;如果两个区间不相交,我们需要增加点并更新右端点。
#include<cmath>
#include<iostream>
#include<algorithm>
using namespace std;
struct Point
{
double x;
double y;
}point[1000];
int cmp(const void *a, const void *b)
{
return (*(Point *)a).x>(*(Point *)b).x?1:-1;
}
int main()
{
int n,d;
int num=1;
while(cin>>n>>d)
{
int counting=1;
if(n==0&&d==0) break;
for(int i=0;i<n;i++)
{
int x,y;
cin>>x>>y;
if(y>d)
{
counting=-1;
}
double t=sqrt(d*d-y*y);
//转化为最少区间的问题
point[i].x=x-t;
//区间左端点
point[i].y=x+t;
//区间右端点
}
if(counting!=-1)
{
qsort(point,n,sizeof(point[0]),cmp);
//按区间左端点排序
double s=point[0].y;
//区间右端点
for(int i=1;i<n;i++)
{
if(point[i].x>s)
//如果两个区间没有重合,增加雷达数目并更新右端点
{
counting++;
s=point[i].y;
}
else if(point[i].y<s)
//如果第二个区间被完全包含于第一个区间,更新右端点
{
s=point[i].y;
}
}
}
cout<<"Case "<<num<<':'<<' '<<counting<<endl;
num++;
}
}
8.销售比赛
在学校OJ上做的一道比较好的题,这里码一下。假设有偶数天,要求每天必须买一件物品或者卖一件物品,只能选择一种操作并且不能不选,开始手上没有这种物品。现在给你每天的物品价格表,要求计算最大收益。首先要明白,第一天必须买,最后一天必须卖,并且最后手上没有物品。那么除了第一天和最后一天之外我们每次取两天,小的买大的卖,并且把卖的价格放进一个最小堆。如果买的价格比堆顶还大,就交换。这样我们保证了卖的价格总是大于买的价格,一定能取得最大收益。
#include<queue>
#include<vector>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
long long int price[100010],t,n,res;
int main()
{
ios::sync_with_stdio(false);
cin>>t;
while(t--)
{
cin>>n;
priority_queue<long long int, vector<long long int>, greater<long long int> > q;
res=0;
for(int i=1;i<=n;i++)
{
cin>>price[i];
}
res-=price[1];
res+=price[n];
for(int i=2;i<=n-1;i=i+2)
{
long long int buy=min(price[i],price[i+1]);
long long int sell=max(price[i],price[i+1]);
if(!q.empty())
{
if(buy>q.top())
{
res=res-2*q.top()+buy+sell;
q.pop();
q.push(buy);
q.push(sell);
}
else
{
res=res-buy+sell;
q.push(sell);
}
}
else
{
res=res-buy+sell;
q.push(sell);
}
}
cout<<res<<endl;
}
}
9、Huffman编码
这同样是《算法导论》上的例子。Huffman编码是广泛用于数据文件压缩的十分有效的编码方法。我们可以有多种方式表示文件中的信息,如果用01串表示字符,采用定长编码表示,则需要3位表示一个字符,整个文件编码需要300000位;采用变长编码表示,给频率高的字符较短的编码,频率低的字符较长的编码,达到整体编码减少的目的,则整个文件编码需要(45×1+13×3+12×3+16×3+9×4+5×4)×1000=224000位,由此可见,变长码比定长码方案好,总码长减小约25%。
对每一个字符规定一个01串作为其代码,并要求任一字符的代码都不是其他字符代码的前缀,这种编码称为前缀码。可能无前缀码是一个更好的名字,但是前缀码是一致认可的标准术语。编码的前缀性质可以使译码非常简单:例如001011101可以唯一的分解为0,0,101,1101,因而其译码为aabe。译码过程需要方便的取出编码的前缀,为此可以用二叉树作为前缀码的数据结构:树叶表示给定字符;从树根到树叶的路径当作该字符的前缀码;代码中每一位的0或1分别作为指示某节点到左儿子或右儿子的路标。
从上图可以看出,最优前缀编码码的二叉树总是一棵完全二叉树,而定长编码的二叉树不是一棵完全二叉树。 给定编码字符集C及频率分布f,C的一个前缀码编码方案对应于一棵二叉树T。字符c在树T中的深度记为dT(c),dT(c)也是字符c的前缀码长。则平均码长定义为:
使平均码长达到最小的前缀码编码方案称为C的最优前缀码。
Huffman编码的构造方法:先合并最小频率的2个字符对应的子树,计算合并后的子树的频率;重新排序各个子树;对上述排序后的子树序列进行合并;重复上述过程,将全部结点合并成1棵完整的二叉树;对二叉树中的边赋予0、1,得到各字符的变长编码。
POJ3253一道就是利用这一思想的典型例题。题目大意是有把一块无限长的木板锯成几块给定长度的小木板,每次锯都需要一定费用,费用就是当前锯的木板的长度。给定各个要求的小木板的长度以及小木板的个数,求最小的费用。以要求3块长度分别为5,8,5的木板为例:先从无限长的木板上锯下长度为21的木板,花费21;再从长度为21的木板上锯下长度为5的木板,花费5;再从长度为16的木板上锯下长度为8的木板,花费8;总花费=21+5+8=34。利用Huffman思想,要使总费用最小,那么每次只选取最小长度的两块木板相加,再把这些和累加到总费用中即可。为了提高效率,使用优先队列优化,并且还要注意使用long long int保存结果。
#include<queue>
#include<cstdio>
#include<iostream>
using namespace std;
int main()
{
long long int sum;
int i,n,t,a,b;
while(~scanf("%d",&n))
{
priority_queue<int,vector<int>,greater<int> >q;
for(i=0;i<n;i++)
{
scanf("%d",&t);
q.push(t);
}
sum=0;
if(q.size()==1)
{
a=q.top();
sum+=a;
q.pop();
}
while(q.size()>1)
{
a=q.top();
q.pop();
b=q.top();
q.pop();
t=a+b;
sum+=t;
q.push(t);
}
printf("%lld\n",sum);
}
}
10、Dijkstra算法
Dijkstra算法是由E.W.Dijkstra于1959年提出,是目前公认的最好的求解最短路径的方法,使用的条件是图中不能存在负边。算法解决的是单个源点到其他顶点的最短路径问题,其主要特点是每次迭代时选择的下一个顶点是标记点之外距离源点最近的顶点,简单的说就是bfs+贪心算法的思想。
#include<iostream>
#include<algorithm>
#define INF 1000
#define MAX_V 100
using namespace std;
int main()
{
int V,E;
int i,j,m,n;
int cost[MAX_V][MAX_V];
int d[MAX_V];
bool used[MAX_V];
cin>>V>>E;
fill(d,d+V+1,INF);
fill(used,used+V,false);
for(i=0;i<V;i++)
{
for(j=0;j<V;j++)
{
if(i==j) cost[i][j]=0;
else cost[i][j]=INF;
}
}
for(m=0;m<E;m++)
{
cin>>i>>j>>cost[i][j];
cost[j][i]=cost[i][j];
}
cin>>n;
d[n]=0;
//源点
while(true)
{
int v=V;
for(m=0;m<V;m++)
{
if((!used[m])&&(d[m]<d[v])) v=m;
}
if(v==V) break;
used[v]=true;
for(m=0;m<V;m++)
{
d[m]=min(d[m],d[v]+cost[v][m]);
}
}
for(i=0;i<V;i++)
{
cout<<"the shortest distance between "<<n<<" and "<<i<<" is "<<d[i]<<endl;
}
}
11、最小生成树算法
设一个网络表示为无向连通带权图G =(V, E) , E中每条边(v,w)的权为c[v][w]。如果G的子图G’是一棵包含G的所有顶点的树,则称G’为G的生成树。生成树的代价是指生成树上各边权的总和,在G的所有生成树中,耗费最小的生成树称为G的最小生成树。例如在设计通信网络时,用图的顶点表示城市,用边(v,w)的权c[v][w]表示建立城市v和城市w之间的通信线路所需的费用,最小生成树给出建立通信网络的最经济方案。
构造最小生成树的Kruskal算法和Prim算法都利用了MST(最小生成树)性质:设顶点集U是V的真子集(可以任意选取),如果(u,v)∈E为横跨点集U和V—U的边,即u∈U,v∈V- U,并且在所有这样的边中,(u,v)的权c[u][v]最小,则一定存在G的一棵最小生成树,它以(u,v)为其中一条边。
使用反证法可以很简单的证明此性质。假设对G的任意一个最小生成树T,针对点集U和V—U,(u,v)∈E为横跨这2个点集的最小权边,T不包含该最小权边<u, v>,但T包括节点u和v。将<u,v>添加到树T中,树T将变为含回路的子图,并且该回路上有一条不同于<u,v>的边<u’,v’>,u’∈U,v’∈V-U。将<u’,v’>删去,得到另一个树T’,即树T’是通过将T中的边<u’,v’>替换为<u,v>得到的。由于这2条边的耗费满足c[u][v]≤c[u’][v’],故即T’耗费≤T的耗费,这与T是任意最小生成树的假设相矛盾,从而得证。
Prim算法每一步都选择连接U和V-U的权值最小的边加入生成树。
#include<iostream>
#include<algorithm>
#define MAX_V 100
#define INF 1000
using namespace std;
int main()
{
int V,E;
int i,j,m,n;
int cost[MAX_V][MAX_V];
int mincost[MAX_V];
bool used[MAX_V];
cin>>V>>E;
fill(mincost,mincost+V+1,INF);
fill(used,used+V,false);
for(i=0;i<V;i++)
{
for(j=0;j<V;j++)
{
if(i==j) cost[i][j]=0;
else cost[i][j]=INF;
}
}
for(m=0;m<E;m++)
{
cin>>i>>j>>cost[i][j];
cost[j][i]=cost[i][j];
}
mincost[0]=0;
int res=0;
while(true)
{
int v=V;
for(m=0;m<V;m++)
{
if((!used[m])&&(mincost[m]<mincost[v]))
v=m;
}
if(v==V) break;
used[v]=true;
res+=mincost[v];
for(m=0;m<V;m++)
{
mincost[m]=min(mincost[m],cost[v][m]);
}
}
cout<<res<<endl;
}
Kruskal算法每一步直接将权值最小的不成环的边加入生成树,我们借助并查集这一数据结构可以完美实现它。
#include<iostream>
#include<algorithm>
#define MAX_E 100
using namespace std;
struct edge
{
int u,v,cost;
};
int pre[MAX_E];
edge es[MAX_E];
int find(int x);
void initvalue(int x);
bool same(int x,int y);
void unite(int x,int y);
bool comp(const edge& e1,const edge& e2);
int main()
{
int V,E;
int i,j,m,n;
cin>>V>>E;
initvalue(V);
for(i=0;i<E;i++) cin>>es[i].u>>es[i].v>>es[i].cost;
sort(es,es+E,comp);
int res=0;
for(i=0;i<E;i++)
{
edge e=es[i];
if(!same(e.u,e.v))
{
unite(e.u,e.v);
res+=e.cost;
}
}
cout<<res<<endl;
}
bool comp(const edge& e1,const edge& e2)
{
return e1.cost<e2.cost;
}
void initvalue(int x)
{
for(int i=0;i<x;i++) pre[i]=i;
}
int find(int x)
{
int r=x;
while(pre[r]!=r) r=pre[r];
int i=x,j;
while(pre[i]!=r)
{
j=pre[i];
pre[i]=r;
i=j;
}
return r;
}
bool same(int x,int y)
{
if(find(x)==find(y)) return true;
else return false;
}
void unite(int x,int y)
{
int fx=find(x);
int fy=find(y);
if(fx!=fy) pre[fx]=fy;
}
转自:
http://blog.csdn.net/effective_coder/article/details/8736718