贪心算法(英语:greedy algorithm),又称贪婪算法,是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是最好或最优的算法。比如在旅行推销员问题中,如果旅行员每次都选择最近的城市,那这就是一种贪心算法。
贪心算法在有最优子结构的问题中尤为有效。最优子结构的意思是局部最优解能决定全局最优解。简单地说,问题能够分解成子问题来解决,子问题的最优解能递推到最终问题的最优解。也就是说,不从整体最优上加以考虑,他所做出的仅是在某种意义上的局部最优解。
贪心算法与动态规划的不同在于它对每个子问题的解决方案都做出选择,不能回退。动态规划则会保存以前的运算结果,并根据以前的结果对当前进行选择,有回退功能。
贪心法可以解决一些最优化问题,如:求图中的最小生成树、求哈夫曼编码……对于其他问题,贪心法一般不能得到我们所要求的答案。一旦一个问题可以通过贪心法来解决,那么贪心法一般是解决这个问题的最好办法。由于贪心法的高效性以及其所求得的答案比较接近最优结果,贪心法也可以用作辅助算法或者直接解决一些要求结果不特别精确的问题。
细节
- 创建数学模型来描述问题。
- 把求解的问题分成若干个子问题。
- 对每一子问题求解,得到子问题的局部最优解。
- 把子问题的解局部最优解合成原来解问题的一个解。
实现该算法的过程:
从问题的某一初始解出发;while 能朝给定总目标前进一步 do,求出可行解的一个解元素;
最后,由所有解元素组合成问题的一个可行解。
贪心解决问题的三个步骤为:
a、确定贪心策略
b、根据贪心策略,一步一步得到局部最优解
c、将局部最优解合并起来就得到全局最优解
利用贪心求解的经典算法实例有哪些?
a、最优装载问题
有一天,海盗们截获了一艘装满各种各样古董的货船,每一件古董都价值连城,一旦打碎就失去了它的价值。虽然海盗船足够大,但载重量为C,每件古董的重量为wi,海盗们该如何把尽可能多数量的宝贝装上海盗船呢?
贪心策略:依次取重量最小的古董
b、背包问题
假设山洞中有 n 种宝物,每种宝物有一定重量w 和相应的价值v,毛驴运载能力有限,只能运走m 重量的宝物,一种宝物只能拿一样,宝物可以分割。那么怎么才能使毛驴运走宝物的价值最大呢?
我们可以尝试贪心策略:
(1)每次挑选价值最大的宝物装入背包,得到的结果是否最优?
(2)每次挑选重量最小的宝物装入,能否得到最优解?
(3)每次选取单位重量价值最大的宝物,能否使价值最高?
贪心策略:第三种
c、会议安排问题
有多个会议,每个会议i 都有起始时间bi 和结束时间ei,且bi<ei,即一个会议进行的时间为半开区间[bi,ei)。任务就是实现召开更多的满足在这个“有限的时间内”等待安排的会议。
我们可以尝试贪心策略:
(1)每次从剩下未安排的会议中选择会议具有最早开始时间且与已安排的会议相容的会议安排,以增大时间资源的利用率。
(2)每次从剩下未安排的会议中选择持续时间最短且与已安排的会议相容的会议安排,这样可以安排更多一些的会议。
(3)每次从剩下未安排的会议中选择具有最早结束时间且与已安排的会议相容的会议安排,这样可以尽快安排下一个会议。
贪心策略:第三种
d、最短路径(Dijkstra)
这是一个求单源最短路径的问题。给定有向带权图 G =(V,E),其中每条边的权是非负实数。此外,给定V 中的一个顶点,称为源点。现在要计算从源到所有其他各顶点的最短路径长度,这里路径长度指路上各边的权之和。
贪心策略:每次选取距离当前节点最短的路径
最短路子问题:
e、哈夫曼编码
不等长编码方法需要解决两个关键问题:
(1)编码尽可能短
我们可以让使用频率高的字符编码较短,使用频率低的编码较长,这种方法可以提高压缩率,节省空间,也能提高运算和通信速度。即频率越高,编码越短。
(2)不能有二义性
例如,ABCD 四个字符如果编码如下。
A:0。B:1。C:01。D:10
贪心策略:概率越小,放的越远
f、最小生成树(prim算法)
贪心策略:选择距离当前网络节点最短的点
贪心和动态规划的区别是什么?
动态规划算法通常以自底向上的方式解各子问题,是递归过程。
贪心算法则通常以自顶向下的方式进行,以迭代的方式作出相继的贪心选择,每作一次贪心选择就将所求问题简化为规模更小的子问题。
以二叉树遍历为例:
贪心法是从上到下仅仅进行深度搜索。也就是说它从根节点一口气走到黑的,它的代价取决于子问题的数目,也就是树的高度,每次在当前问题的状态上作出的选择都是1。不进行广度搜索。所以终于它得出的解不一定是最优解。非常有可能是近似最优解。
而动态规划法在最优子结构的前提下,从树的叶子节点開始向上进行搜索,而且在每一步都依据叶子节点的当前问题的状况作出选择,从而作出最优决策。所以她的代价是子问题的个数和可选择的数目。它求出的解一定是最优解。
背包问题和0/1背包的区别是什么?
物品可分割的装载问题我们称为背包问题,物品不可分割的装载问题我们称之为0-1 背包问题。
0-1背包问题和背包问题
1).两个问题的描述
- 0-1背包问题:
给定n种物品和一个背包。物品i的重量是Wi,其价值为Vi,背包的容量为C。应如何选择装入背包的物品,使得装入背包中物品的总价值最大?
在选择装入背包的物品时,对每种物品i只有2种选择,即装入背包或不装入背包。不能将物品i装入背包多次,也不能只装入部分的物品i。
- 背包问题:
与0-1背包问题类似,所不同的是在选择物品i装入背包时,可以选择物品i的一部分,而不一定要全部装入背包,1≤i≤n。
这2类问题都具有最优子结构性质,极为相似,但背包问题可以用贪心算法求解,而0-1背包问题却不能用贪心算法求解。
2).用贪心算法解背包问题的基本步骤:
首先计算每种物品单位重量的价值Vi/Wi,然后,依贪心选择策略,将尽可能多的单位重量价值最高的物品装入背包。若将这种物品全部装入背包后,背包内的物品总重量未超过C,则选择单位重量价值次高的物品并尽可能多地装入背包。依此策略一直地进行下去,直到背包装满为止。
代码实现:
float knapsack(float c,float w[MAXNUM], float v[MAXNUM],float con[MAXNUM])
{
int n=MAXNUM;
float d[n],hascon=0,remain=c;
int i;
for (i = 0; i < n; i++)
d[i] = v[i]/w[i]; //算出每个物品的平均价值
sort(d,w,v,n); //按照平均价值对w,v进行排列,详细代码略
for (i=0;i<n;i++)
con[i]=0; //con记录第i个物品完整度,如果为0,未装入,如果为1整体装入 0,1之间装入部分
for (i=0;i<n;i++)
{
if (w[i]>remain ) //如果第i个物品无法整体装进保 则跳出循环
break;
con[i]=1;
hascon+=v[i] //累加装进包的物品总价值
remain-=w[i];
}
if (i<n)
{
con[i]=remain/w[i]; //计算包中剩余部分还能容纳多少价值
hascon+=con[i]*v[i];
}
return hascon;
}
算法knapsack的主要计算时间在于将各种物品依其单位重量的价值从大到小排序。因此,算法的计算时间上界为O(nlogn)。当然,为了证明算法的正确性,还必须证明背包问题具有贪心选择性质。
对于0-1背包问题,贪心选择之所以不能得到最优解是因为在这种情况下,它无法保证最终能将背包装满,部分闲置的背包空间使每公斤背包空间的价值降低了。事实上,在考虑0-1背包问题时,应比较选择该物品和不选择该物品所导致的最终方案,然后再作出最好选择。由此就导出许多互相重叠的子问题。这正是该问题可用动态规划算法求解的另一重要特征。
实际上也是如此,动态规划算法的确可以有效地解0-1背包问题。
一般求解过程:
使用贪心法求解能够依据下面几个方面进行(终于也相应着每步代码的实现),以找零钱为例:
1、候选集合(C)
通过一个候选集合C作为问题的可能解。(终于解均取自于候选集合C)
比如。在找零钱问题中,各种面值的货币构成候选集合。
2、解集合(S)
每完毕一次贪心选择,将一个解放入S。终于获得一个完整解S
3、解决函数(solution)
检查解集合S是否构成问题的完整解。
比如,在找零钱问题中。解决函数是已付出的货币金额恰好等于应付款。
4、选择函数(select)
即贪心策略。这是贪心法的关键,选择出最有希望构成问题的解的对象。
(这个选择函数通常和目标函数有关)
比如,在找零钱问题中,贪心策略就是在候选集合中选择面值最大的货币。
5、可行函数(feasible)
检查解集合中增加一个候选对象是否可行。(增加下一个对象后是不是满足约束条件)
比如。在找零钱问题中,可行函数是每一步选择的货币和已付出的货币相加不超过应付款。
一般过程:
Greedy(C) //C是问题的输入集合即候选集合
{
S={ }; //初始解集合为空集
while (not solution(S)) { //集合S没有构成问题的一个解
x=select(C); //在候选集合C中做贪心选择
if feasible(S, x) { //判断集合S中加入x后的解是否可行
S=S+{x};
C=C-{x};
}
}
return S;
例子:数组的最小乘积子集
给定一个数组a,我们必须找到数组中存在的元素子集的最小乘积。最小乘积也可以是单个元素。
例子:
输入: a [] = {-1,-1,-2,4,3} 输出: -24 说明:最小乘积将为(-2 * -1 * -1 * 4 * 3)= -24 输入: a [] = {-1,0} 输出: -1 说明: -1(单个元素)是最小乘积 输入: a [] = {0,0,0} 输出: 0
一个简单的解决方案是生成所有子集,找到每个子集的乘积并返回最大乘积。
更好的解决方案是使用以下方法:
- 如果负数为偶数且不为零,则结果为除最大负数之外的所有结果的乘积。
- 如果有奇数个负数而没有零个,那么结果就是所有乘积。
- 如果有零和正数,没有负数,则结果为0。例外情况是,当没有负数且所有其他元素为正数时,我们的结果应为第一个最小正数。
// Java program to find maximum product of
// a subset.
class GFG {
static int minProductSubset(int a[], int n)
{
if (n == 1)
return a[0];
// Find count of negative numbers,
// count of zeros, maximum valued
// negative number, minimum valued
// positive number and product of
// non-zero numbers
int negmax = Integer.MIN_VALUE;
int posmin = Integer.MAX_VALUE;
int count_neg = 0, count_zero = 0;
int product = 1;
for (int i = 0; i < n; i++)
{
// if number is zero,count it
// but dont multiply
if(a[i] == 0){
count_zero++;
continue;
}
// count the negetive numbers
// and find the max negetive number
if(a[i] < 0)
{
count_neg++;
negmax = Math.max(negmax, a[i]);
}
// find the minimum positive number
if(a[i] > 0 && a[i] < posmin)
posmin = a[i];
product *= a[i];
}
// if there are all zeroes
// or zero is present but no
// negetive number is present
if (count_zero == n ||
(count_neg == 0 && count_zero > 0))
return 0;
// If there are all positive
if (count_neg == 0)
return posmin;
// If there are even number except
// zero of negative numbers
if (count_neg % 2 == 0 && count_neg != 0)
{
// Otherwise result is product of
// all non-zeros divided by maximum
// valued negative.
product = product / negmax;
}
return product;
}
// main function
public static void main(String[] args)
{
int a[] = { -1, -1, -2, 4, 3 };
int n = 5;
System.out.println(minProductSubset(a, n));
}
}
// This code is contributed by Arnab Kundu.