GREEDY ALGORITHM

贪心算法原理

  • 贪心算法以动态规划方法为基础,区别于贪心算法在每一次做出贪心选择后,子问题之一为空,下一步只需继续分解非空子问题。

  • 贪心算法的两个关键要素:

  1. 贪心选择性质:可以通过做出局部最优选择来构造全局最优选择。这也是与动态规划不同的地方,动态规划中每一个步骤都要进行选择,选择依赖于子问题的解。贪心算法总是选择当前最佳的选择,然后求解剩下的唯一的子问题。因此,贪心算法通常是自顶向下的,进行一次又一次选择,将给定的问题实例变得更小。
  1. 最优子结构:最优子结构的定义是,一个问题的最优解包含了其子问题的最优解。这个性质也是能否运用贪心算法和动态规划的关键。因此一个问题是否能应用贪心算法,需要论证每个步骤的贪心选择是否会生成原问题的最优解。(数学归纳)

数学归纳

  • undo

具体问题

1. 活动选择问题

  • 问题描述:使用同一资源的n个任务的集合S={(a_1,....,a_n)},因为该资源每次只能被一个任务使用,给出每个任务a_i使用资源的开始及结束时间,要求找出使用该资源的最大任务子集(尽可能多的任务被使用),又因为任务不重叠,因此称这些集合(解不止一个,贪心算法给出一个)是最大兼容任务子集。

其中开始时间s_i,结束时间为f_i,具体实例如:

image.png

这个例子里有两个最大兼容任务活动子集,{(a_1,a_4,a_8,a_{11})}还有{(a_2,a_4,a_9,a_{11})}的数目最多且元素不重叠。
为了后面应用贪心算法,先按结束时间对f_i做了排序。

  • 先照书上说的用DP先分析:
    S_{i,j}表示在a_i结束之后,a_j开始之前的活动集合。在S_{i,j}中需求一个最大兼容任务活动子集A_{i,j}。假设这个最优解A_{i,j}包含有任务a_k,按照DP的思想便可以用子问题定义原问题,有:
    (1) A_{i,k}=A_{i,j}\bigwedge S_{i,j}A_{k,j}=A_{i,j}\bigwedge S_{k,j}
    (2) A_{i,j}=A_{i,k}\bigvee\{a_k\} \bigvee A_{k,j}

因此S_{i,j}中最大兼容任务活动子集A_{i,j}的个数为:(3)|A_{i,k}| + |A_{k,j}| + 1

在式子(2)里我们假设了A_{i,j}里包含了两个子问题S_{i,k}S_{k,j}的最优解。可以用书上说的验证方法,设S_{k,j}的一个兼容活动子集为\hat A_{k,j},其大小满足|\hat A_{k,j}| \ge | A_{k,j}|,将\hat A_{k,j}作为S_{i,j}的最优解一部分,构造出一个兼容活动子集,其大小大于(3),这与A_{i,j}为最优解矛盾,因此,证明A_{i,j}里包含了两个子问题S_{i,k}S_{k,j}的最优解。

  • 形式化公式(3),用c_{i,j}表示集合S_{i,j}的最优解大小,可得递归式:
    c_{i,j} = c_{i,k} + c_{k,j} +1

上面假设最优解包含了a_k,实际并不知道,需要寻找k
c_{i,j} = 0 , \ if : S_{i,j} = \emptyset\\ c_{i,j} = \max_{a_{k} \in S_{i,j}}{(c_{i,k} + c_{k,j} +1)}, \ if : S_{i,j} \neq\emptyset

c++代码:

#include <iostream>
#include<vector>
#include<climits>
using namespace std;

int DP_Activity(const vector<int> &start, const vector<int> &end)
{
  const int n = start.size();
  vector<vector<int>> table(n, vector<int>(n, 0));
  vector<int> path;
  //第一个位置为虚拟活动a_0
  for(int i=0; i<n; ++i)
  {
    for (int j=i+1; j<n; ++j)
    {
      for (int k=i+1;k<j;++k)
      {  
        if(start[k]>end[i] && end[k] < start[j])
        {
          int tmp = table[i][k] + table[k][j] + 1;
          if (tmp > table[i][j])
          {
            table[i][j] = tmp;
            path.push_back(k);
          }
        }
      } 
    }
  }
  for (int i=0; i<table.size(); ++i)
  {
    for(int j=0; j<table.size(); ++j)
    {
      cout << table[i][j] << " ";
    }
  cout<< endl;
  }
  return table[0][n-1];
}
int main() {
  //第一个位置和最后一个位置是虚拟位
  vector<int> s={0,1,3,0,5,3,5,6,8,8,2,12,INT_MAX};
  vector<int> f={0,4,5,6,7,9,9,10,11,12,14,16, INT_MAX};
  int ans = DP_Activity(s,f);
  cout << "Answer is : "<< ans << endl;
}


 ./main
0 0 0 0 1 0 1 1 2 2 0 3 4
0 0 0 0 0 0 0 0 1 1 0 2 3
0 0 0 0 0 0 0 0 0 0 0 1 2
0 0 0 0 0 0 0 0 0 0 0 1 2
0 0 0 0 0 0 0 0 0 0 0 1 2
0 0 0 0 0 0 0 0 0 0 0 0 1
0 0 0 0 0 0 0 0 0 0 0 0 1
0 0 0 0 0 0 0 0 0 0 0 0 1
0 0 0 0 0 0 0 0 0 0 0 0 1
0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0
Answer is : 4

参考下别人的,多个角度看一个问题收获更多:https://www.cnblogs.com/hapjin/p/5573419.html

  • 贪心算法

先确定所做的贪心选择方法:直觉上,先选最早结束的活动,那就可以剩下更多的资源供其他活动选择,即选择某一最早结束的活动a_k之后,剩下的问题规模变小了,活动集合为S_k=\{s_i \ge f_k\}

书上证明了这样一种思路可以得到最优解。主要思路是a_mS_k中最早结束的活动,假设有a_jA_k的最早结束活动,替换a_ja_k,得到结论a_k也是属于一个最大兼容活动集合。

c++代码,运行时间为\Theta(n)

#include <iostream>
#include<vector>
#include<climits>
using namespace std;

void Greedy_Recursive_Activity(const vector<int> &start, const vector<int> &end, const int k, const int n, vector<int> &ans)
/*
Params: 
  start: 活动开始时间
  end: 活动结束时间(已经升序排列)
  k: S活动集合区间的左边界
  n:S活动集合区间的右边界
Return:
  void: 使用ans返回S[k,n]内的最大的兼容区间大小
*/
{
  int m=k+1;
  while(m<=n && start[m]<end[k]) //找到最早结束的活动
  {
    m += 1;
  }
  if(m<=n)
  {
    ans.push_back(m);
    Greedy_Recursive_Activity(start,end,m,n,ans);
  }
  else
    return;
}
int main() {
  //第一个位置和最后一个位置是虚拟位
  vector<int> s={0,1,3,0,5,3,5,6,8,8,2,12,INT_MAX};
  vector<int> f={0,4,5,6,7,9,9,10,11,12,14,16, INT_MAX};
  vector<int> ans;
  int n = s.size();
  Greedy_Recursive_Activity(s,f,0,n-2,ans);
  for (auto i:ans)
    cout << i << " ";
  
  cout << endl << "Answer is : "<< ans.size() << endl;
}

1 4 8 11
Answer is : 4

迭代版本:

#include <iostream>
#include<vector>
#include<climits>
using namespace std;

vector<int> Greedy_Iter_Activity(const vector<int> &start, const vector<int> &end)
/*
Params: 
  start: 活动开始时间
  end: 活动结束时间(已经升序排列)
Return:
  vector<int>: 使用ans返回S[k,n]内的最大的兼容区间大小
*/
{
  vector<int> ans;
  int n = start.size();
  ans.push_back(1);
  int k=1;
  for (int m=2; m<n-1; ++m)
  {
    if(start[m] > end[k])
    {

      ans.push_back(m);
      k = m;
    }
  }
  return ans;
}
int main() {
  //第一个位置和最后一个位置是虚拟位
  vector<int> s={0,1,3,0,5,3,5,6,8,8,2,12,INT_MAX};
  vector<int> f={0,4,5,6,7,9,9,10,11,12,14,16, INT_MAX};
  vector<int> ans;
  int n = s.size();
  ans = Greedy_Iter_Activity(s,f);
  for (auto i:ans)
    cout << i << " ";
  
  cout << endl << "Answer is : "<< ans.size() << endl;
}

背包问题9讲学习记录

1. 01背包
  • 定义:给定N个物品,没种物品都有自己的重量w_i和价值v_i,在限定容量为C内,选择若干个物品(每种物品可以选0个或者1个),设计选择方案使得物品的总价值最高。

  • 0-1背包问题的定性 : 贪婪算法无法得到最优解。(参见算法导论)

  • 0-1背包问题的递推关系
    设问题为取前i个物品,放入容量为W的背包,获得的总价值为F(i,W),由取不取第i个物品,可以用子问题定义原问题:
    F(i, W)= \begin{cases} 0, & \ if: i = 0 \\ 0, & \ if: W = 0 \\ F(i-1,W), & \ if: w_i > W \\ \max\{F(i-1,W), v_i + F(i-1, W-w_i)\}, & \ otherwise \end{cases}

  • 最优解回溯
    • V(i,j)=V(i-1,j) 时,说明没有选择第i个商品,则回到V(i-1,j)
    • V(i,j)=V(i-1,j-w(i))+v(i) 时,说明装了第i个商品,该商品是最优解组成的一部分,随后我们得回到装该商品之前,即回到V(i-1,j-w(i))
    • 一直遍历到i=0结束为止,所有解的组成都会找到。
  • 例子:


    image.png
  • c++代码:
#include <iostream>
#include<vector>
#include<climits>
using namespace std;

void Zero_One_knapsack(const vector<int> &weight, const vector<int> &value, const int capacity, vector<vector<int>> &ans, vector<vector<int>> &path)
/*
Params:
  weight: weight list of objects.
  value: corresponding valus of each object.
  capacity: total size of knapsack.
  ans: return answer table.
  path: return backtracking table.
Return:
  None.
*/
{
  const int n = weight.size();
  vector< vector<int>> _ans(n, vector<int>(capacity+1, 0));
  vector< vector<int>> _path(n, vector<int>(capacity+1, 0));
  for(int i=1; i<n;++i)
  {
    for(int j=1; j<=capacity; ++j)
    {
      if(j < weight[i])  // whether backpack size enough for weight
      {
        _ans[i][j] = _ans[i-1][j]; 
      }else{
        if (_ans[i-1][j] > _ans[i-1][j-weight[i]] + value[i])
        {
          _ans[i][j] = _ans[i-1][j];
        }
        else{
          _ans[i][j] = _ans[i-1][j-weight[i]] + value[i];
          _path[i][j] = i;
        }
      }
    }
  }

  ans = _ans;
  path = _path; 
}

void Backtracking(const vector<vector<int>> &ans, const vector<int> &weight, const vector<int> &value, const int i, const int j)
/*
Params:
  weight: weight list of objects.
  value: corresponding valus of each object.
  capacity: total size of knapsack.
  i: size of objects (without zero visual item).
  j: size of backpack.
Return:
  None.
*/
{
  if (i > 0)
  {
    if (ans[i][j] == ans[i-1][j])
    {
      Backtracking(ans, weight, value, i-1, j);
    }
    else
    {
      if (j-weight[i]>=0 && ans[i][j] == ans[i-1][j-weight[i]] + value[i])
      {
        cout<< i << " ";
        Backtracking(ans, weight, value, i-1, j-weight[i]);
      }

    }
  }
}

int main() {
  int mark = 0;
  vector<int> weight={mark,1,2,5,6,7};
  vector<int> value={mark,1,6,18,22,28};
  const int capacity = 11;
  vector<vector<int>> ans;
  vector<vector<int>> path;
  Zero_One_knapsack(weight, value, capacity, ans, path);

  for (int i=0; i< ans.size(); ++i)
  {
    for(int j=0; j < ans[0].size(); ++j)
        cout << ans[i][j] << " ";
    cout << endl;
  }  
  cout << endl << "Answer is : "<< ans[ans.size()-1][capacity] << endl;
  Backtracking(ans,weight, value, 5, 11);
}

0 0 0 0 0 0 0 0 0 0 0 0
0 1 1 1 1 1 1 1 1 1 1 1
0 1 6 7 7 7 7 7 7 7 7 7
0 1 6 7 7 18 19 24 25 25 25 25
0 1 6 7 7 18 22 24 28 29 29 40
0 1 6 7 7 18 22 28 29 34 35 40

Answer is : 40
4 3
2.分数背包
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,366评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,521评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,689评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,925评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,942评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,727评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,447评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,349评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,820评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,990评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,127评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,812评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,471评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,017评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,142评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,388评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,066评论 2 355