贪心算法

  • 1. 贪心算法定义及性质
  • 2. 实例1:活动选择问题
  • 3. 实例2:霍夫曼编码
  • 4. 附录(代码)
    • 4.1 活动选择代码
    • 4.2 霍夫曼编码代码

1. 贪心算法定义及性质

贪心算法(Greedy Method),又称为“贪婪算法”。是一种在每一步选择中都采取在当前状态下最好或最优的选择,从而导致结果是最好或最优的算法。

贪心算法的两个要素是:最优子结构、贪心选择性质。

最优子结构:如果一个问题的最优解包含其子问题的最优解,则称此问题具有最优子结构性质。此性质是能够应用动态规划和贪心算法的关键因素。

贪心选择性质:我们可以通过做出局部最优选择来构造全局最优解。换句话说,当我们进行选择时,只需要做出当前问题看起来最优的解,而不必考虑其子问题的解。

一个动态规划算法通常是自底向上计算的,而一个贪心算法通常是自顶向下的。

一般地,可以按如下步骤设计贪心算法:

  1. 将最优化问题转换为如下形式:对其做出一次选择后,只剩下一个子问题需要求解。
  2. 证明做出贪心选择后,原问题总是存在最优解,即贪心选择总是安全的
  3. 证明做出贪心选择后,剩余的子问题满足性质:其最优解与贪心选择组合即可得到原问题的最优解。

2. 实例1:活动选择问题

问题:有一个由多个活动构成的集合,各自有一个开始时间sis_isi​和结束时间fif_ifi​,它们的开始时间和结束时间可能重叠,我们需要选出几个互不重叠的活动,且使得活动的数量达到最多。

:活动已按结束时间排序。

动态规划解法1(书上的解法):令SijS_{ij}Sij​表示在aia_iai​结束之后开始,且在aja_jaj​开始之前结束的那些活动的集合。AijA_{ij}Aij​表示SijS_{ij}Sij​的一个最大的相互兼容的活动子集。例如,S1,11S_{1,11}S1,11​表示在a1a_1a1​结束之后开始,a11a_{11}a11​开始之前结束的活动集合,因此S1,11={a4,a6,a7,a8,a9}S_{1,11}=\{a_4, a_6, a_7, a_8, a_9\}S1,11​={a4​,a6​,a7​,a8​,a9​},且A1,11A_{1,11}A1,11​至少包含S1,11S_{1,11}S1,11​中的一个元素。设AijA_{ij}Aij​包含元素aka_kak​,则可以得到两个子问题:寻找SikS_{ik}Sik​中的兼容活动以及SkjS_{kj}Skj​中的兼容活动

如果用c[i,j]c[i,j]c[i,j]表示集合SijS_{ij}Sij​的最优解的大小,则可得递归式c[i,j]=c[i,k]+c[k,j]+1c[i,j] = c[i,k]+c[k,j] + 1c[i,j]=c[i,k]+c[k,j]+1
由于我们并不知道aka_kak​取SijS_{ij}Sij​中的哪一个活动,因此需要考察其中的所有活动,因此
c[i,j]={0Sij=ϕmax⁡ak∈Sij{c[i,k]+c[k,j]+1}Sij≠ϕc[i,j] = \begin{cases}0&S_{ij}=\phi \\\max_{a_k\in S_{ij}}\{c[i,k]+c[k,j]+1\}&S_{ij}\neq\phi \end{cases}c[i,j]={0maxak​∈Sij​​{c[i,k]+c[k,j]+1}​Sij​=ϕSij​̸​=ϕ​
根据该递归式可以设计出动态规划算法:

void Solution::dp(vector<pair<int, int>> data) {int n = data.size();init(n);for (int l = 2; l < n; l++) // 按长度自底向上for (int i = 0; i < n - l; i++) {vector<int> S;for (int j = i + 1; j < i + l; j++) { // 构造集合Sif (data[j].first >= data[i].second && data[j].second <= data[i + l].first) S.push_back(j);if (!S.empty()){int max = 0;for (int k : S) { // 考察S中的所有元素if (c[i][k] + c[k][i + l] + 1 > max) {max = c[i][k] + c[k][i + l] + 1;r[i][i + l] = k;}}c[i][i + l] = max;}}}restruct(data);
}

动态规划解法2:首先选择一个活动aka_kak​作为第一个活动,则原问题转换为寻找活动aka_kak​结束后开始的所有活动的最大兼容活动子集。由于不知道哪一个活动作为第一个活动时能够构造出最大的活动子集,因此需要对所有的活动进行考察。

自底向上的动态规划算法如下:

void AS::dp(const vector<pair<int, int>> &Activity) {// Dynamic programmingint n = Activity.size();init(n);for (int i = n - 1; i >= 0; i--) { // 按结束时间自底向上for (int j = n - 1; j >= 0; j--) {int max = 0;if (Activity[j].first >= Activity[i].second) {if (c[j] + 1 > max) {max = c[j] + 1;c[i] = max;r[i] = j;}}}}restruct_dp(0, Activity);
}

贪心算法解法:从动态规划解法2中可以看出,如果我们每次都选择结束时间最早的活动(贪心选择),就能够剩下更多的时间来安排后续的活动。

贪心算法的递归解法如下:

vector<pair<int, int>> AS::gd(const vector<pair<int, int>> &Activity, int k) {int m = k + 1;int n = Activity.size();vector < pair<int, int>> buffer = { Activity[0] };while (m < n && Activity[m].first < Activity[k].second) m++;if (m < n) {buffer = select_gd(Activity, m);buffer.push_back(Activity[m]);}return buffer;
}

迭代解法如下:

vector<pair<int, int>> AS::select_gd1(const vector<pair<int, int>> &Activity) {int n = Activity.size();vector < pair<int, int>> buffer = { Activity[0] };int k = 0;for (int m = 1; m < n; m++) {if (Activity[m].first >= Activity[k].second) {k = m;buffer.push_back(Activity[m]);}}return buffer;
}

上述两种算法在进行选择时都采用的贪心策略,减少了时间和空间的损耗。

3. 实例2:霍夫曼编码

一个字符编码问题:对于一个100 000个字符的文件,只包含a−fa-fa−f 6个不同字符,出现频率如下表所示。如果为每个字符指定一个3位的码字,可以将文件编码为300 000位的长度。但如果使用表中的变长编码,可以仅用224 000位长度即可。

注意:变长编码中的每一个编码都不是其它码字的前缀,这样在解码时才不会出现混乱。

用二叉树表示上述两种编码方式如下:

其中,左侧表示定长编码,所有叶子结点的深度相同。右侧表示变长编码,虽然看似树的深度更深,但由于频率越高的字符编码长度越短,因此变长编码具有更高的效率。此外,文件的最优编码方案总是对应一棵满二叉树,如右侧的二叉树所示。

霍夫曼设计了一个贪心算法来构造最优前缀码,被称为霍夫曼编码。下面是构造霍夫曼树的伪代码:

HUFFMAN(C)n = |C|Q = Cfor i = 1 to n - 1allocate a new node zz.left = x = EXTRACT_MIN(Q)z.right = y = EXTRACT_MIN(Q)z.freq = x.freq + y.freqINSERT(Q, z)return EXTRACT_MIN(Q)

其构造过程如下:

这里霍夫曼树的构造借助了优先队列,首先将所有的结点加入优先队列中,然后取出其中最小的两个结点并将它们合并为一个新的结点,将新结点加入优先队列后再次重复上过程,直到队列中只剩下一个元素为止。

4. 附录(代码)

4.1 活动选择代码

#include <iostream>
#include <vector>
#include <utility>
using namespace std;
class AS {private:int *r, *c;void init(int n);
public:void dp(const vector<pair<int, int>> &Activity);void restruct_dp(int start, const vector<pair<int, int>> &Activity);vector<pair<int, int>> select_gd(const vector<pair<int, int>> &Activity, int k);vector<pair<int, int>> select_gd1(const vector<pair<int, int>> &Activity);};
void AS::init(int n) {r = new int[n];c = new int[n];for (int i = 0; i < n; i++) c[i] = 1;
}
void AS::dp(const vector<pair<int, int>> &Activity) {// Dynamic programmingint n = Activity.size();init(n);for (int i = n - 1; i >= 0; i--) { // 按结束时间自底向上for (int j = n - 1; j >= 0; j--) {int max = 0;if (Activity[j].first >= Activity[i].second) {if (c[j] + 1 > max) {max = c[j] + 1;c[i] = max;r[i] = j;}}}}restruct_dp(0, Activity);
}
void AS::restruct_dp(int start, const vector<pair<int, int>> &Activity) {int max = 0;int index = -1;int n = Activity.size();for (int i = 0; i < n; i++) {if (c[i] > max && Activity[i].first >= start) {max = c[i];index = i;}}cout << Activity[index].first << "," << Activity[index].second << "\t";if (index >= n - 1) return;restruct_dp(Activity[index].second, Activity);
}
vector<pair<int, int>> AS::select_gd(const vector<pair<int, int>> &Activity, int k) {// greedy methodint m = k + 1;int n = Activity.size();vector < pair<int, int>> buffer = { Activity[0] };while (m < n && Activity[m].first < Activity[k].second) m++;if (m < n) {buffer = select_gd(Activity, m);buffer.push_back(Activity[m]);}return buffer;
}
vector<pair<int, int>> AS::select_gd1(const vector<pair<int, int>> &Activity) {// greedy methodint n = Activity.size();vector < pair<int, int>> buffer = { Activity[0] };int k = 0;for (int m = 1; m < n; m++) {if (Activity[m].first >= Activity[k].second) {k = m;buffer.push_back(Activity[m]);}}return buffer;
}int main(int argc, char* argv[]) {vector<pair<int, int>> Activity = {{1, 4}, {3, 5}, {0, 6}, {5, 7}, {3, 9}, {5, 9}, {6, 10},{8, 11}, {8, 12}, {2, 14}, {12, 16} };AS as;as.dp(Activity);vector<pair<int, int>> ret = as.select_gd1(Activity);for (int i = 0; i < ret.size(); i++) {cout << ret[i].first << "," << ret[i].second << "\t";}cout << endl;return 0;
}

4.2 霍夫曼编码代码

#include <iostream>
#include <queue>
#include <utility>
#include <string>
#include <map>
using namespace std;
struct Node {Node* left = nullptr;Node* right = nullptr;char ch = '\0';int freq = 0;Node(Node* l, Node* r, char c, int f):left(l), right(r), ch(c), freq(f) {};
};
bool operator<(Node A, Node B) {return B.freq < A.freq;
}
class Huffman{private:map<char, string> Map;priority_queue<Node> Q;
public:Node Tree_Build(vector<pair<char, int>> &data) {int n = data.size();for (int i = 0; i < n; i++) {Q.push(Node(nullptr, nullptr, data[i].first, data[i].second));}for (int i = 1; i < n; i++) {Node *node1 = new Node{ Q.top() }; Q.pop();Node *node2 = new Node{ Q.top() }; Q.pop();Node node(node1,node2, '\0', node1->freq + node2->freq);Q.push(node);}return Q.top();}void Map_Build(Node T, string str) {string s = str;if (T.ch != '\0') {Map.emplace(pair<char, string>{T.ch, str});}else {if (T.left != nullptr) {s.append("0");Map_Build(*(T.left), s);s.pop_back();}if (T.right != nullptr) {s.append("1");Map_Build(*(T.right), s);}}}void Init(vector<pair<char, int>> &data) {Node node = Tree_Build(data);Map_Build(node, "");}string Encoder(string str) {string s;for (auto ch : str) {s.append(Map.at(ch));}return s;}string Decoder(string str) {Node node = Q.top();Node* p = &node;string s;for (char ch : str) {if (p->ch != '\0') {s.push_back(p->ch);p = &node;}if (ch == '0') p = p->left;else p = p->right;}if (p->ch != '\0') s.push_back(p->ch);return s;}
};int main(int argc, char* argv[]) {Huffman H;vector<pair<char, int>> data = { {'f', 5}, {'e', 9}, {'c', 12},{'b',13}, {'d', 16}, {'a', 45} };H.Init(data);string str = "abcdefabcdef";cout << "Encode: " << str << "->" << H.Encoder(str) << endl;string code = "001011011001001111111101110111001100";cout << "Decode: " << code << "->" << H.Decoder(code) << endl;return 0;
}

算法导论学习笔记13_贪心算法相关推荐

  1. 算法导论-上课笔记7:贪心算法

    文章目录 0 前言 1 活动选择问题 1.1 活动选择问题的最优子结构 1.2 贪心选择 1.3 递归贪心算法 1.4 迭代贪心算法 2 贪心算法原理 2.1 贪心选择性质 2.2 最优子结构 2.3 ...

  2. 【转】算法导论学习笔记 一 分治算法

    分治策略是一种常见的算法.在分治策略中,我们递归的求解一个问题,在每层递归中应用如下三个步骤: 1. 分解,将问题分解成规模更小但解决方案相同的子问题 2. 解决,递归的求解子问题,如果子问题足够小则 ...

  3. 算法导论中C语言代码,算法导论-学习笔记与进度

    算法导论 阅读进度 第一部分 基础知识 第一章 计算中算法的角色 Done 1.1 算法 输入与输出 算法可以解决哪些问题 数据结构 技术 一些比较难的问题 1.2 作为一种技术的算法 效率 算法和其 ...

  4. 算法导论学习笔记 第6章 堆排序

    在本章中介绍了另一种排序算法:堆排序(heapsort).与归排序一样,但不同于插入排序的是,堆排序的时间复杂度式(Onlgn).而与插入排序相同,但不同于归并排序的是,堆排序同样具有空间原址性(我理 ...

  5. 【算法导论学习笔记】第3章:函数的增长

    原创博客,转载请注明: http://www.cnblogs.com/wuwenyan/p/4982713.html  当算法的输入n非常大的时候,对于算法复杂度的分析就显得尤为重要,虽然有时我们能通 ...

  6. 算法导论学习笔记 第7章 快速排序

    对于包含n个数的输入数组来说,快速排序是一种时间复杂度为O(n^2)的排序算法.虽然最环情况的复杂度高,但是快速排序通常是实际应用排序中最好的选择,因为快排的平均性能非常好:它的期望复杂度是O(nlg ...

  7. 算法导论学习笔记 第2章 算法基础

    本章介绍了一个贯穿本书的框架,后续的算法设计都是在这个框架中进行的. 本章通过插入排序和归并排序两种常见的算法来说明算法的过程及算法分析,在分析插入排序算法时,书中是用了循环不变式证明了算法的正确性, ...

  8. 算法导论学习笔记1_循环不变式

    循环不变式 1. 循环不变式和数学归纳法 2. 循环不变式的三条性质 3. 利用循环不变式分析插入排序 4. 练习题 2.1.3 1. 循环不变式和数学归纳法 在数学中,数学归纳法常用于证明给定命题在 ...

  9. 算法导论学习笔记 6.5 优先队列

    优先队列(priority queue)是一种用来维护由一组元素构成的集合S的数据结构,其中的每一个元素都有一个相关的值,称为关键字(key).一个最大优先队列支持一下操作: INSERT(S, x) ...

  10. 生日悖论问题——《算法导论学习笔记》

    1      生日悖论问题 1.1    原始问题 一个房间里的人数必须达到多少,才能使两个人生日相同的机会达到50%?不考虑闰年情况,也就是一年按照365天来计算. 解答: 假设房间里的人数是k,我 ...

最新文章

  1. 大小端字节序介绍以及判断当前环境字节序的程序【C语言】
  2. 阿里云混合云Apsara Stack 2.0发布,加速政企数智创新
  3. Android设计模式之——解释器模式
  4. linux java amr转mp3_本工具用于将微信语音 amr 格式转换为 mp3 格式以便在 html5 的 audio 标签中进行播放...
  5. 0基础怎么做可视化大屏?2种可以节省95%时间的方法教给你
  6. “创新”,我们应该如何去做?
  7. leetcode53. 最大子序和详解——pygo
  8. 我的内核学习笔记15:海思Hi3516平台GPIO使用记录
  9. query string parameter前端怎么传参_Substrate 前端开发-1: 用 Polkadot-JS API 轻松搭建前端
  10. vue使用contenteditable 实现光标处插入自定义图片
  11. Android中跳转应用市场
  12. 运营商推无限流量卡:这是不是一记昏招?
  13. 《敏捷教练-如何打造优秀的敏捷团队》读书笔记
  14. “下一代智能硬件Web应用防火墙”——创宇盾X完成统信互认
  15. 利用谷歌镜像网站编辑Latex的参考文献与doi链接
  16. 《破窑赋》 《命运赋》 《时运赋》
  17. Spring Boot WebSocket实时监控
  18. 芯盾时代: 开启“智慧身份认证”新时代
  19. 设备远程监控及智能化系统在工程机械中的应用
  20. 阿里云服务器企业用户最新配置表!

热门文章

  1. 123D画一个产品外壳3d图给3d打印机打印完整流程
  2. 关于NSIS脚本操作静默安装第三方程序+判断电脑位数
  3. 如何查找计算机主机地址,ip地址查询 怎么查询电脑IP地址?
  4. Excel中不复制隐藏行
  5. python画画excel_用Python在Excel里画出蒙娜丽莎的方法示例
  6. 基因组测序 转录组测序
  7. 13个免费下载 SVG 图标网站
  8. 用Python模拟QQ界面之QQ登录界面的奥秘
  9. 想用好低代码这把“双刃剑”,先搞清楚这三个问题|低代码系列(四)
  10. C++ Reference: Standard C++ Library reference: C Library: cfenv: FE_INEXACT