淺談貪心算法

貪心算法

貪心算法(英語:greedy algorithm),又稱貪婪算法,是一種在每一步選擇中都採取在當前狀態下最好或最優(即最有利)的選擇,從而希望導致結果是最好或最優的算法。
—— 維基百科

揹包相關問題

經典揹包問題

求最優裝載,給出n個物體,第i個物體重量爲wi。選擇儘量多的物體,使得總重量不超過C。

  • 分析
      由於只關心物體的數量,所以裝重的沒有裝輕的划算。只需把所有物體按重量從小到大排序,依次選擇每個物體,直到裝不下爲止。這是一種典型的貪心算法,它只顧眼前,但卻能得到最優解。

部分揹包問題

  有n個物體,第i個物體的重量爲wi,價值爲vi。在總重量不超過C的情況下讓總價值儘量高。每一個物體都可以只取走一部分,價值和重量按比例計算。

  • 分析
      不能簡單地先拿輕的(輕的可能價值也小),也不能先拿價值大的(可能它特別重),而應該綜合考慮兩個因素。一種直觀的貪心策略是:優先拿價值除以重量的值最大的,直到重量和正好爲C。

乘船問題

  有n個人,第i個人重量爲wi。每艘船的最大載重量均爲C,且最多隻能乘兩個人。用最少的船裝載所有人。

  • 分析
      考慮最輕的人i,他應該和誰一起坐呢?如果每個人都無法和他一起坐船,則唯一的方案就是每人坐一艘船。否則,他應該選擇能和他一起坐船的人中最重的一個j。這樣的方法是貪心的,因此它只是讓“眼前”的浪費最少。可以分爲以下兩種情況:
  1. i不和任何一個人坐同一艘船,那麼可以把j拉過來和他一起坐,總船數不會增加(而且可能會減少)。
  2. 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的大小關係:
  1. a1>a2,如圖8-7(a)所示,區間2包含區間1。前面已經討論過,這種情況下一定不會選擇區間2。不僅區間2如此,以後所有區間中只要有一個i滿足a1>ai,i都不要選。在今後的討論中,將不考慮這些區間。
  2. 排除了情況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;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章