貪心算法(英語:greedy algorithm),又稱貪婪算法,是一種在每一步選擇中都採取在當前狀態下最好或最優(即最有利)的選擇,從而希望導致結果是最好或最優的算法。
—— 維基百科
揹包相關問題
經典揹包問題
求最優裝載,給出n個物體,第i個物體重量爲wi。選擇儘量多的物體,使得總重量不超過C。
- 分析
由於只關心物體的數量,所以裝重的沒有裝輕的划算。只需把所有物體按重量從小到大
排序,依次選擇每個物體,直到裝不下爲止。這是一種典型的貪心算法,它只顧眼前,但卻能得到最優解。
部分揹包問題
有n個物體,第i個物體的重量爲wi,價值爲vi。在總重量不超過C的情況下讓總價值儘量高。每一個物體都可以只取走一部分,價值和重量按比例計算。
- 分析
不能簡單地先拿輕的(輕的可能價值也小),也不能先拿價值大的(可能它特別重),而應該綜合考慮兩個因素。一種直觀的貪心策略是:優先拿價值除以重量的值
最大的,直到重量和正好爲C。
乘船問題
有n個人,第i個人重量爲wi。每艘船的最大載重量均爲C,且最多隻能乘兩個人。用最少的船裝載所有人。
- 分析
考慮最輕的人i,他應該和誰一起坐呢?如果每個人都無法和他一起坐船,則唯一的方案就是每人坐一艘船。否則,他應該選擇能和他一起坐船的人中最重的一個j。這樣的方法是貪心的,因此它只是讓“眼前”的浪費最少。可以分爲以下兩種情況:
- i不和任何一個人坐同一艘船,那麼可以把j拉過來和他一起坐,總船數不會增加(而且可能會減少)。
- i和另外一人k同船。由貪心策略,j是可以和i一起坐船的人中最重的,因此k比j輕。把j和k交換後k所在的船仍然不會超重(因爲k比j輕),而i和j所在的船也不會超重(由貪心法過程),因此所得到的新解不會更差。
程序實現
在以上的分析中,比j更重的人只能每人坐一艘船。這樣,只需用兩個下標i和j分別表示當前考慮的最輕的人和最重的人,每次先將j往左移動
,直到i和j可以共坐一艘船,然後將i加1,j減1
,並重覆上述操作。
例題
題目描述:
n個人,已知每個人體重。獨木舟承重固定,每隻獨木舟最多坐兩個人,可以坐一個人或者兩個人。顯然要求總重量不超過獨木舟承重,假設每個人體重也不超過獨木舟承重,問最少需要幾隻獨木舟?
原題鏈接:51Nod獨木舟
代碼如下:
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <cstdlib>
#include <algorithm>
#include <vector>
#include <string>
using namespace std;
constexpr auto maxn = 10010; // 相當於#define maxn 10010
int n, m, in;
vector<int> vec; // 存儲人的體重
bool vis[maxn]; // 用於標記上船的人
bool cmp(int a, int b) {
return a > b;
}
int main()
{
ios::sync_with_stdio(false);
cin >> n >> m; // 上船人數和船的承重
for (int i = 0; i < n; i++) {
cin >> in;
vec.push_back(in);
}
sort(vec.begin(), vec.end(), cmp);
int start = 0, end = vec.size() - 1, ans = 0;
while (start < end) {
++ans;
int sum = vec[start] + vec[end];
if (sum <= m) {
vis[start] = vis[end] = true;
start++;
end--;
}
else {
vis[start] = true;
start++;
}
}
if (start == end && vis[start] == false) {
ans++;
}
cout << ans;
return 0;
}
區間相關問題
選擇不相交區間
數軸上有n個開區間(ai, bi)。選擇儘量多個區間,使得這些區間兩兩沒有公共點。
- 分析
首先明確一個問題:假設有兩個區間x,y,區間x完全包含y。那麼,選x是不划算的,因爲x和y最多隻能選一個,選x還不如選y,這樣不僅區間數目不會減少,而且給其他區間留出了更多的位置。接下來,按照bi從小到大的順序給區間排序
。貪心策略是:一定要選第一個區間。現在區間已經排序成b1≤b2≤b3…了,考慮a1和a2的大小關係:
- a1>a2,如圖8-7(a)所示,區間2包含區間1。前面已經討論過,這種情況下一定不會選擇區間2。不僅區間2如此,以後所有區間中只要有一個i滿足a1>ai,i都不要選。在今後的討論中,將不考慮這些區間。
- 排除了情況1,一定有a1≤a2≤a3≤…,如圖8-7(b)所示。如果區間2和區間1完全不相交,那麼沒有影響(因此一定要選區間1),否則區間1和區間2最多隻能選一個。如果不選區間2,黑色部分其實是沒有任何影響的(它不會擋住任何一個區間),區間1的有效部分其實變成了灰色部分,它被區間2所包含!由剛纔的結論,區間2是不能選的。依此類推,不能因爲選任何區間而放棄區間1,因此選擇區間1是明智的。
選擇了區間1以後,需要把所有和區間1相交的區間排除在外,需要記錄上一個被選擇的區間編號。這樣,在排序後只需要掃描一次即可完成貪心過程,得到正確結果。
例題1
題目描述
X軸上有N條線段,每條線段有1個起點S和終點E。最多能夠選出多少條互不重疊的線段。(注:起點或終點重疊,不算重疊)。
原題鏈接:51Nod不重疊的線段
代碼如下:
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
typedef struct
{
int s, e;
}Line;
Line L[50005];
bool cmp(Line a, Line b)
{
return a.e < b.e;
}
int main()
{
ios::sync_with_stdio(false);
int n;
cin >> n;
for (int i = 0; i < n; i++) {
cin >> L[i].s >> L[i].e;
}
sort(L, L + n, cmp);
int end = L[0].e;
int ans = 1;
for (int i = 1; i < n; i++) {
if (L[i].s >= end) {
ans++;
end = L[i].e;
}
}
cout << ans;
return 0;
}
例題2(用優先隊列解決)
優先隊列用法詳解(priority_queue)
題目描述
有若干個活動,第i個開始時間和結束時間是[Si,fi),同一個教室安排的活動之間不能交疊,求要安排所有活動,最少需要幾個教室?
原題鏈接:51Nod活動安排問題
代碼如下:
#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <vector>
#include <queue>
using namespace std;
typedef struct
{
int s, f;
}Line;
Line L[50005];
bool cmp(Line a, Line b)
{
return a.s < b.s;
}
int main()
{
ios::sync_with_stdio(false);
int n;
priority_queue<int, vector<int>, greater<int> > myqueue;
cin >> n;
for (int i = 0; i < n; i++) {
cin >> L[i].s >> L[i].f;
}
sort(L, L + n, cmp);
myqueue.push(L[0].f);
int ans = 1;
for (int i = 1; i < n; i++) {
if (L[i].s < myqueue.top()) {
ans++;
myqueue.push(L[i].f);
}
else {
myqueue.pop();
myqueue.push(L[i].f);
}
}
cout << ans;
return 0;
}
區間選點問題
數軸上有n個閉區間[ai, bi]。取儘量少的點,使得每個區間內都至少有一個點(不同區間內含的點可以是同一個)。
- 分析
如果區間i內已經有一個點被取到,則稱此區間已經被滿足。受上一題的啓發,下面先討論區間包含的情況。由於小區間被滿足時大區間一定也被滿足,所以在區間包含的情況下,大區間不需要考慮。把所有區間按b從小到大排序
(b相同時a從大到小排序),則如果出現區間包含的情況,小區間一定排在前面。第一個區間應該取哪一個點呢?此處的貪心策略是:取最後一個點,如圖8-8所示。
根據剛纔的討論,所有需要考慮的區間的a也是遞增的,可以把它畫成圖8-8的形式。如果第一個區間不取最後一個,而是取中間的,如灰色點,那麼把它移動到最後一個點後,被滿足的區間增加了,而且原先被滿足的區間現在一定被滿足。不難看出,這樣的貪心策略是正確的。
區間覆蓋問題
數軸上有n個閉區間[ai, bi],選擇儘量少的區間覆蓋一條指定線段[s,t]。
- 分析
突破口仍然是區間包含和排序掃描,不過先要進行一次預處理。每個區間在[s, t]外的部分都應該預先被切掉,因爲它們的存在是毫無意義的。預處理後,在相互包含的情況下,小區間顯然不應該考慮。
把各區間按照a從小到大排序。如果區間1的起點不是s,無解(因爲其他區間的起點更大,不可能覆蓋到s點),否則選擇起點在s的最長區間。選擇此區間[ai, bi] 後,新的起點應該設置爲bi,並且忽略所有區間在bi之前的部分,就像預處理一樣。雖然貪心策略比上題複雜,但是仍然只需要一次掃描,如圖8-9所示。s爲當前有效起點(此前部分已被覆蓋),則應該選擇區間2。
例題
題目描述
給出N條線段的起點和終點,從中選出2條線段,這兩條線段的重疊部分是最長的。輸出這個最長的距離。如果沒有重疊,輸出0。
原題鏈接:51No線段的重疊
代碼如下:
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>
using namespace std;
typedef struct
{
int s, e;
}Line;
Line L[50005];
bool cmp(Line a, Line b)
{
if (a.s < b.s)return true;
if (a.s == b.s && a.e < b.e)return true;
return false;
}
int main()
{
ios::sync_with_stdio(false);
int n;
int a, b;
int Max = 0;
cin >> n;
for (int i = 0; i < n; i++) {
cin >> L[i].s >> L[i].e;
}
sort(L, L + n, cmp);// 按起點的大小排序,若起點相同則按終點
for (int i = 0; i < n; i++) {
int l = 0;
for (int j = i + 1; j < n; j++) { // j代表i的下一個起點
if (L[j].s > L[i].e) break; // 線段無交集
if (L[i].e - L[j].s < Max) break; // 交集小於Max就退出循環
a = min(L[i].e, L[j].e);
b = max(L[i].s, L[j].s);
l = a - b;
if (l > Max)
Max = l;
}
}
cout << Max << endl;
return 0;
}