天天看点

0x06算法设计与分析复习(二):算法设计策略-贪心法1算法设计策略-贪心法

参考书籍:算法设计与分析——C++语言描述(第二版)

算法设计策略-贪心法

贪心法

  • 贪心法是一种求解最优化问题的算法设计策略。
    • 贪心法是通过分步决策(stepwise decision)的方法来求解问题的。贪心法在求解问题的每一步上做出某种决策,产生n-元组解的一个分量。贪心法要求根据题意,选定一种最优量度标准(optimization criterion)或贪心准则(greedy criterion),也称贪心选择性质(greedy choice property)。这种量度标准通常只考虑局部最优行。
  • 在初始状态下,解向量 solution=∅ ,其中不包含任何分量。使用最优量度标准,一次选择一个分量,逐步形成解向量 (x0,x1,⋯,xn−1) 。算法执行过程中生成的向量 (x0,x1,⋯,xk),k<n ,称为部分解向量,也称部分向量(partial vector)。
  • 在根据最优量度标准选择分量的过程中,还需要使用一个可行解判定函数。设 (x0,x1,⋯,xk−1) 是贪心算法已经生成的部分解,根据最优量度标准,算法当前选取解向量的第 k(k<n) 个分量为 xk ,此时需要使用可行解判定函数来判断,在添加新的分量 xk 后所形成部分解 (x0,x1,⋯,xk) 是否违反可行解约束条件。
  • 贪心法之所以被称为是贪心的,是因为它希望每一步决策都是正确的,即要求在算法的每一步上,仅根据最有量度标准选择分量,并只需要保证形成的部分解不违反约束条件,最终得到n-元组不仅是可行解,而且必定是最优解。
  • 事实上,最优量度标准一般并不从整体考虑,它只是在某种意义上的局部最优选择,每一步决策只是在当前看来是最优的。因此,贪心法不能保证对所有问题都得到整体最优解。
//贪心法框架
SolutionType Greedy(SType a[], int n)
{
  //初始时,解向量不包含任何分量
  SolutionType solution=EmptySet;
  //多步决策,每次选择解向量的一个分量
  for(int i = ; i<n;i++){
    //遵循最优量度标准选择一个分量
    SType x= Select(a);
    //判定加入分量x后的部分解是否可行
    if(Feasible(solution, x))
      //形成新的部分解
      solution=Union(solution, x);
  }
  return solution;//返回生成的最优解
}
           
对于一个贪心算法,必须进一步证明该算法的每一步上所做出的选择,都必然最终导致问题的一个整体最优解。

背包问题

问题描述

背包问题:已知一个载重为M的背包和n件物品,第i件物品的重量为 wi ,如果将第i件物品全部放进背包,将有收益 pi 这里, wi>0,pi>0,0≤i<n 。所谓背包问题,是指求一种最佳装载方案,使得收益最大。

  • 0/1背包问题:每一件物品不能分割,只能作为整体装入背包,或者不能装入。
  • 一般背包问题:物品是可以分割的,允许将其中的一部分装入背包。

用贪心法求解 0/1背包问题只能求得近似解。

一般背包问题的贪心法求解

一般背包问题:

s.t.max∑i=0n−1pixi∑i=0n−1wixi≤Mwi>0pi>00≤xi≤10≤i<n

最优量度标准:选择使 单位重量收益最大的物品装入背包,即按 pi/wi 的非增次序选取物品。

基本步骤:

  1. 首先计算每种物品单位重量的收益,并按非增次序进行排序;
  2. 然后进行贪心选择策略,选择单位重量收益最高的物品装入背包,依次策略一直进行下去,将尽可能多的物品全部装入背包,直至背包装满;
  3. 若装入某件物品时,不能全部装下,而背包内的物品总量仍未达到M,则根据背包的剩余重量,选择单位重量价值次高的物品尽可能多装入背包。
//背包问题的贪心算法
template<class T>
  class Knapsack
  {
    public:
    //创建一个一维数组w和p,并赋初值
    Knapsack(int mSize, float cap, float *wei, T *prof);
    //数组x为背包问题的最优解
    void GreedyKnapsack(float* x);
    ...
      private:
    float m, *w;//m为背包载重量,w指示存储n个物品重量的数组
    T* p;//p指示存储n个物品收益的数组;
    int n;//n为物品数量
  };
template<class T>
  vois Knapsack<T>::GreedyKnapsack(float* x)
  {
    //前置条件:w[i]已按p[i]/w[i]的非增次序排列;
    float u = m;//u为背包剩余载重量,初始时为m
    for(int i = ;i<n;i++)
      x[i]=;//对解向量初始化
    for(i = ;i<n;i++){
      //按最优量度标准选择解的分量
      if(w[i]>u)
        break;
      x[i]=;
      u=u-w[i];
    }
    if(i<n)
      x[i]=u/w[i];
  }
           

定理:如果 p0/w0≥p1/w1≥⋯≥pn−1/wn−1 ,则以上程序求得的背包问题的解是最优解。

C语言实验:设有载重 M=20 的背包,3件物品的重量为 (w0,w1,w2)=(18,15,10) ,物品装入的收益为 (p0,p1,p2)=(25,24,15) 。

#include <stdio.h>
#include <stdlib.h>

void GreedyKnapsack(float p[], float w[], float x[], int n, float m);
void SortW(float* p, float* w, int n);
int main()
{
    float p[] = { ,, }, w[] = { ,, };
    float x[] = { ,, };
    float M = ;
    //对w[i]进行排序,按p[i]/w[i]的非增次序排列
    SortW(p, w, );
    printf("after sorting:\n");
    for (int i = ; i < ; i++) {
        printf("p[%d] = %f ", i, p[i]);
    }
    printf("\n");
    for (int i = ; i < ; i++) {
        printf("w[%d] = %f ", i, w[i]);
    }
    printf("\n");

    GreedyKnapsack(p, w, x, , M);

    printf("The solution is:\n");
    for (int i = ; i < ; i++) {
        printf("x[%d] = %f ", i, x[i]);
    }
    printf("\n");
    system("pause");
    return ;
}

void SortW(float* p, float* w, int n)
{
    int i = , j = n;
    float tmp = ;
    for (i = ; i < n; i++) {
        for (j = ; j < n - i-; j++) {
            if (p[j] / w[j] < p[j + ] / w[j + ]) {
                tmp = p[j];
                p[j] = p[j + ];
                p[j + ] = tmp;
                tmp = w[j];
                w[j] = w[j + ];
                w[j + ] = tmp;
            }
        }
    }
}

void GreedyKnapsack(float p[], float w[], float x[], int n, float m)
{
    //前置条件:w[i]已按p[i]/w[i]的非增次序排列;
    float u = m;//u为背包剩余载重量,初始时为m
    int i = ;
    for (i = ; i<n; i++)
        x[i] = ;//对解向量初始化
    for (i = ; i<n; i++) {
        //按最优量度标准选择解的分量
        if (w[i]>u)
            break;
        x[i] = ;
        u = u - w[i];
    }
    if (i<n)
        x[i] = u / w[i];
}
           

实验结果:

after sorting:
p[] =  p[] =  p[] = 
w[] =  w[] =  w[] = 
The solution is:
x[] =  x[] =  x[] = 
请按任意键继续. . .
           

贪心法的基本要素

一般来说,适用于贪心法求解的问题大都具有下面两个特征:最优量度标准和最优子结构。

最优量度标准

最优量度标准(optimization criterion)或贪心准则(greedy criterion),也称贪心选择性质(greedy choice property)。是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。

所谓贪心法的最优量度标准,是指可以根据该量度标准,实行多步决策进行求解,虽然在该量度意义下所做的这些选择都是局部最优的,但最终得到的解却是全局最优的。选择最优量度标准是使用贪心法求解问题的核心问题。值得注意的是,贪心算法每步做出的选择可以依赖以前做出的选择,但决不依赖将来的选择,也不依赖于子问题的解。虽然贪心算法的每一步选择也将问题简化为一个规模更小的子问题,但由于贪心算法每一步选择不依赖子问题的解,每步选择只按最优量度标准进行,因此,对于一个贪心算法,必须证明所采用的量度标准能够导致一个整体最优解。

贪心法的当前选择可能会依赖于已经做出的选择,但不依赖于尚未做出的选择和子问题,因此它的特征是自顶向下,一步一步地做出贪心决策。

最优子结构

所谓最优子结构特征是 关于问题最优解的特征。当一个问题的最优解中包含了子问题的最优解时,则称该问题具有最优子结构特性(optimal substructure)。一个可用贪心法求解的问题往往呈现最优子结构特性。

一般而言,如果一个最优化问题的解结构具有元组形式,并具有最优子结构特性,我们可以尝试选择量度标准。如果经过证明(一般是归纳法),确认该量度标准能够导致最优解,便可容易地按贪心法的算法框架设计出求解该问题的具体的贪心算法。问题的最优子结构性质是该问题可用动态规划算法或贪心算法求解的关键特征。

并非所有的具有最优子结构特性的最优化问题,都能够找到最优量度标准,此时,可以考虑采用动态规划法来求解。

小结

一个问题能够用贪心策略的条件是该问题的解是向量结构的,具有最优子结构特性,还要求能够通过分析问题获取最优量度标准。但是,按照该量度标准一次生成解的分量所形成的解是否确实是最优解仍需证明。

一个问题能够使用贪心策略的条件如下:

  • 问题的解是向量结构
  • 具有最优子结构特性
  • 能够获取最优量度标准
  • 能证明是最优解

继续阅读