Greedy Algorithm
《數據結構與算法——C語言描述》
圖論涉及的三個貪婪算法
- Dijkstra 算法
- Prim 算法
- Kruskal 算法
Greedy 經典問題:coin change
在每一個階段,可以認為所作決定是好的,而不考慮將來的后果。
如果不要求最對最佳答案,那么有時用簡單的貪婪算法生成近似答案,而不是使用一般說來產生准確答案所需的復雜算法。
所有的調度問題,或者是NP-完全的,或者是貪婪算法可解的。
NP-完全性:
計算復雜性理論中的一個重要概念,它表征某些問題的固有復雜度。一旦確定一類問題具有NP完全性時,就可知道這類問題實際上是具有相當復雜程度的困難問題。
貪心算法是一類算法的統稱
10.1.1 一個簡單的調度問題
將最后完成時間最小化
這個問題是NP-完全的。因此,將++最后完成時間最小化++顯然比++平均完成時間最小++化困難得多。
10.1.2 Huffman 編碼
讓字符代碼的長度從字符到字符是變化不等,同時保證經常出現的字符其代碼更短。
最優編碼的樹將具有的性質:所有結點或者是樹葉,或者有兩個兒子。
字符代碼的長度是否不相同並不要緊,只要沒有字符代碼是別的字符代碼的前綴即可。
基本的問題:
找到總價值(編碼總長度)最小的滿二叉樹。
Huffman 算法
因為每一次合並都不進行全局的考慮,只是選擇兩棵最小的樹,所以該算法是貪心算法。由此可見,貪心算法是一類算法的統稱。
在文件壓縮這樣的應用中,實際上幾乎所有的運行時間都花費在讀入文件和寫文件所需的磁盤 I/O 上。
偽代碼
1. 生成 Huffman 樹
while(minHeap.size() > 1){
tree->left = minHeap.get()
tree->right = minHeap.get()
tree->frequence = tree->left->frequence + tree->right->frequence
minHeap.insert(tree);
}
huffmanTree = minHeap.get();
2. 從 Huffman 樹到 Huffman 編碼(遞歸調用)
字符集一般是常數的數量級,所以這里使用遞歸雖不是最優解,但足矣解此題。
void treeToCode(struct Node* root, Map& codePlan, string& path){
if(isLeaf(root)){
//left node
codePlan[root->character].huffmanCode = path;
}
else {
//internal node
string leftPath = path + "0";
treeToCode(root->left, codePlan, leftPath);
string rightPath = path + "1";
treeToCode(root->right, codePlan, rightPath);
}
}
Huffman編碼的完整代碼實現:
#include <iostream>
#include <string>
#include <queue>
#include <vector>
#include <unordered_map>
#include <utility>
using namespace std;
struct node {
char c;
int freq;
struct node *left;
struct node *right;
bool isLeaf;
};
struct Cmp {
bool operator() (const struct node* lhs, const struct node* rhs){
return lhs->freq > rhs->freq;
}
};
struct node* make_node() {
struct node *p = new node();
p->left = nullptr;
p->right = nullptr;
return p;
}
void treeToCode(struct node* root, unordered_map<char, string>& codePlan, string& path) {
if (root->left == nullptr && root->right == nullptr) {
//left node
codePlan[root->c] = path;
}
else {
//internal node
string leftPath = path + "0";
treeToCode(root->left, codePlan, leftPath);
string rightPath = path + "1";
treeToCode(root->right, codePlan, rightPath);
}
}
void huffman(const string &s) {
unordered_map<char, int> hashmap;
for (auto c : s) {
hashmap[c] += 1;
}
priority_queue<struct node*, vector<struct node*>, Cmp> heap;
for (auto e : hashmap) {
struct node* p = make_node();
p->c = e.first;
p->freq = e.second;
heap.push(p);
}
/*while (heap.size()) {
cout << heap.top()->c << heap.top()->freq << endl;
heap.pop();
}*/
while (heap.size() > 1) {
struct node* p = make_node();
p->left = heap.top();
p->left->freq = heap.top() > 0 ? heap.top() > 0 : heap.top()->left->freq + heap.top()->right->freq;
heap.pop();
p->right = heap.top();
p->right->freq = heap.top() > 0 ? heap.top() > 0 : heap.top()->left->freq + heap.top()->right->freq;
heap.pop();
p->freq = p->left->freq + p->right->freq;
heap.push(p);
}
string path;
unordered_map<char, string> codePlan;
struct node* root = heap.top();
treeToCode(root, codePlan, path);
for (auto e : codePlan) {
cout << e.first << e.second << endl;
}
}
int main() {
string s = "abbcccdddd";
huffman(s);
return 0;
}
必須注意的細節
- 壓縮文件的開頭必須要傳送編碼信息,因為否則將不可能譯碼。
- 該算法是一個兩趟掃描算法。第一趟搜集頻率數據,第二遍進行編碼。顯然這對於處理大型文件的程序來說是不高效的。
10.1.3 近似裝箱問題
聯機裝箱問題:將每一件物品放入一個箱子之后才處理下一件物品。
while (cin >> e) {
foo(e);
}
脫機裝箱問題:做任何事情都需要等到所有的輸入數據全部讀入之后才進行。
while (cin >> e) {
v.push_back(e);
}
foo(v);
聯機算法
1. 下項適合算法
當處理任何一件物品時,我們檢查看它是否能裝進剛剛裝進物品的同一個箱子中去。如果能就放入該箱子,否則放進新的箱子中。
struct Boxes {
int rest;
const int maxQuality;
vector<int> v;
};
void nextFit(vector<Boxes> &boxes, int curr) {
if (curr > boxes[0].maxQuality)
cout << "error : exceed max quality" << endl;
if (boxes[boxes.size() - 1].rest - curr >= 0){
boxes[boxes.size() - 1].rest -= curr;
boxes[boxes.size() - 1].v.push_back(curr);
}
else {
boxes.push_back(Boxes());
boxes[boxes.size() - 1].rest -= curr;
boxes[boxes.size() - 1].v.push_back(curr);
}
}
2.首次適合算法
依次掃描這些箱子,但把新的一項物品放入足夠盛下它的第一個箱子中。
struct Boxes {
int rest;
const int maxQuality;
vector<int> v;
};
void firstFit(vector<Boxes> &boxes, int curr) {
if (curr > boxes[0].maxQuality)
cout << "error : exceed max quality" << endl;
for (auto &box : boxes) {
if (box.rest - curr >= 0) {
box.rest -= curr;
box.v.push_back(curr);
return;
}
}
boxes.push_back(Boxes());
boxes[boxes.size() - 1].rest -= curr;
boxes[boxes.size() - 1].v.push_back(curr);
}
3. 最佳適合算法
把新物品放入所有的箱子當中,能夠容納它並且最滿的箱子中。
struct Boxes {
int rest;
const int maxQuality;
vector<int> v;
};
void bestFit(vector<Boxes> &boxes, int curr) {
if (curr > boxes[0].maxQuality) {
cout << "error : out of max quality" << endl;
return;
}
size_t fitNo = -1;
int fitRest = boxes[0].maxQuality;
for (size_t i = 0; i < boxes.size(); i++) {
int curRest = boxes[i].rest - curr;
if (fitRest > curRest && curRest >= 0) {
fitRest = curRest;
fitNo = i;
}
}
if (fitNo == -1 && fitRest == boxes[0].maxQuality) {
boxes.push_back(Boxes());
boxes[boxes.size() - 1].rest -= curr;
boxes[boxes.size() - 1].v.push_back(curr);
}
else {
boxes[fitNo].v.push_back(curr);
boxes[fitNo].rest -= curr;
}
}
脫機算法
圍繞這個問題的自然方法是將各項物品排序,把最大的物品放在最先。此時應用首次適合算法或最佳適合算法,分別得到首次適合遞減算法(first fit decreasing)和最佳適合遞減算法(best fit decreasing)。
struct Boxes {
int rest;
const int maxQuality;
vector<int> v;
};
void firstFit(vector<Boxes> &boxes, int curr) {
if (curr > boxes[0].maxQuality)
cout << "error : exceed max quality" << endl;
for (auto &box : boxes) {
if (box.rest - curr >= 0) {
box.rest -= curr;
box.v.push_back(curr);
return;
}
}
boxes.push_back(Boxes());
boxes[boxes.size() - 1].rest -= curr;
boxes[boxes.size() - 1].v.push_back(curr);
}
void firstFitNonIncreasing(vector<Boxes>& boxes, vector<int>& v) {
sort(v.begin(), v.end(), greater<int>());
for (auto e : v)
firstFit(boxes, e);
}
int main() {
firstFitNonIncreasing(boxes, input);
}
《算法之美》
7.4.1 哈夫曼編碼
哈夫曼使用自底向上的方法構建二叉樹。
在 JPEG 圖像壓縮方式中,就用到了哈夫曼編碼。
哈夫曼編碼是一種不等長德編碼,其基本原理是頻繁使用的數據用較短的代碼代替。(離散性)
哈夫曼編碼具有即時性和唯一可譯性。
哈夫曼樹就是帶權路徑最小的二叉樹。樹的帶權路徑長度(WPL,Weight Path Length)是樹中所有葉子結點的帶權路徑長度之和。
通過將權值大的外結點調整到離根結點較近的位置來得到最小路徑長度。
7.4.2 構造哈夫曼樹
7.4.3 哈夫曼編碼的實現
哈夫曼編碼是無前綴編碼。
產生哈夫曼編碼需要對原始數據掃描兩遍。第1遍掃描時為了要統計出原始數據中每個值出現的頻率,第2遍是建立哈夫曼樹並進行編碼。
缺點與不足
- 哈夫曼編碼的碼字長度參差不齊,硬件實現不方便
- 碼字在存儲或傳輸過程中,如果出現誤碼時,可能引起誤碼的連續傳播
- 對數據進行解碼時,必須參照哈夫曼編碼表
《算法的樂趣》
算法設計的常用思想
算法是一次智力活動的結果,但並不是毫無章法的爆發,它應該是遵循一定規律的智力活動。
首先,它需要一些基礎知識作為着力點。比如數據結構。
其次,對問題域做高度概括並抽象出問題的精確描述。建立數學模型。
最后,選擇一些常用的模式和原則,有人稱之為算法設計模式或算法設計思想。
3.1 貪婪法
尋找最優解的問題的常用辦法。將求解過程分成若干步驟,並在每個步驟都應用貪心原則——選取當前狀態下,最好或者最優的選擇。
貪婪法、動態規划和分治法一樣,都需要對問題進行分解,定義最優解的子結構。
因為不進行回溯處理,貪婪法只在很少的情況下可以得到真正的最優解,比如最短路徑問題、圖的最小生成樹問題。
通常最為其他算法的輔助算法來使用。
3.1.1 貪婪法的基本思想
三個步驟
- 建立數學模型
- 分解為子問題
- 用子問題的局部最優解迭代出全局最優解
關於“找零問題”,子問題的最優解結構就是在之前的步驟中,給已經選好的硬幣加上當前選擇的一枚硬幣。
但是,同樣是“找零問題”,貪婪法在很多情況下得到的只是近似最優解。
《數據結構、算法與應用(C++語言描述)》
17.1 最優化問題
限制條件、優化函數、可行解、最優解
每個最優化問題都包含一組限制條件和一個優化函數。
符合限制條件的問題求解方案稱為可行解。
使優化函數可能取得最佳值的可行解稱為最優解。
用數學語言來表達問題是精確的,它可以清楚地說明求解問題的程序。
17.2 貪婪算法思想
在貪婪算法中,我們要逐步構造一個最優解。
每一步我們都在一定的標准下,作出一個最優決策。
例17-4[找零錢]
struct Stratrgy{
map<int, int, greater<int>> m;
Stratrgy(initializer_list<int> l) {
for (auto e : l) {
m[e] = 0;
}
}
};
void change(int total, Stratrgy& s) {
for (auto &e : s.m) {
int n = 0;
while ((total - (n+1) * e.first) >= 0){
n++;
}
total -= n*e.first;
e.second = n;
}
}
void printStrategy(const Stratrgy& s) {
for (auto e : s.m) {
cout << e.first << " " << e.second << endl;
}
}
int main() {
Stratrgy s({25, 10, 5, 1});
change(41, s);
printStrategy(s);
printStrategy(s2);
return 0;
}
得到的是近似最優解
例17-5[機器調度]
按照任務起始時間的非遞減順序
采用一個復雜性為 O(N * logN)的排序算法(如堆排序),按 Si 的非遞減次序排列排序,然后使用一個關於“舊”機器可用時刻的最小堆。
struct Task {
Task(char id, int start, int finish) :id(id), s(start), f(finish) {}
char id;
int s;
int f;
};
struct Machine {
Machine(size_t id) : id(id), u(0), v() {}
Machine() {}
size_t id;
int u;
vector<char> v;
};
vector<Machine> schedule(vector<Task>& tasks) {
sort(tasks.begin(), tasks.end(),
[](const Task& lhs, const Task& rhs) {
return lhs.s < rhs.s;
});
auto machineCmp = [](const Machine& lhs, const Machine& rhs) {return lhs.u > rhs.u; };
priority_queue<Machine, vector<Machine>, decltype(machineCmp)> machines(machineCmp);
for (auto e : tasks) {
Machine m;
if (!machines.empty() && e.s >= machines.top().u) {
m = machines.top();
machines.pop();
}
else {
m.id = machines.size() + 1;
}
m.u = e.f;
m.v.push_back(e.id);
machines.push(m);
}
vector<Machine> v;
while (!machines.empty()){
v.push_back(machines.top());
machines.pop();
}
return v;
}
int main() {
vector<Task> v = {
{ 'a', 0, 2},
{ 'b', 3, 7 },
{ 'c', 4, 7 },
{ 'd', 9, 11 },
{ 'e', 7, 10 },
{ 'f', 1, 5 },
{ 'g', 6, 8 }
};
vector<Machine> scheduling = schedule(v);
for (auto machine : scheduling) {
cout << machine.id << endl;
for (auto e : machine.v) {
cout << e << " ";
}
cout << endl;
}
return 0;
}
17-3-2 0/1背包問題
問題的公式描述
約束條件
貨物裝箱與 0/1 背包的對比
貨物裝箱 Wi C
0/1 背包 Wi Pi C
總結
0/1 背包問題實際上是一個一般化的貨箱裝載問題,只是從每個貨箱所獲得的價值不同。
0/1 背包問題是一個 NP-復雜問題
《算法設計與分析基礎》
窮舉查找
對於背包問題,窮舉查找算法對於任何輸入都是非常低效率的。
旅行商問題和背包問題是NP困難問題中最著名的例子。
對於NP困難問題,目前沒有已知的效率可以用多項式來表示的算法。
本書對於“背包問題”的分類與學習順序
窮盡查找 -> DP -> 分支界定法 -> NP困難問題的近似解法