藍橋杯——算法訓練——開心的金明(附0-1揹包講解)

藍橋杯——算法訓練——開心的金明(附0-1揹包講解)

00-11 揹包的簡單應用。
——————————————————————————————————————————————————
資源限制
時間限制:1.0s 內存限制:256.0MB
問題描述
金明今天很開心,家裏購置的新房就要領鑰匙了,新房裏有一間他自己專用的很寬敞的房間。更讓他高興的是,媽媽昨天對他說:“你的房間需要購買哪些物品,怎麼佈置,你說了算,只要不超過NN元錢就行”。
今天一早金明就開始做預算,但是他想買的東西太多了,肯定會超過媽媽限定的NN元。於是,他把每件物品規定了一 個重要度,分爲55:用整數151\sim5表示,第 55 等最重要。他還從因特網上查到了每件物品的價格(都是整數元)。
他希望在不超過NN元(可以等於NN元)的前提下,使每件物品的價格與重要度的乘積的總和最大。
設第 jj 件物品的價格爲 v[j]v[j],重要度爲 p[j]p[j],共選中了 kk 件物品,編號依次爲 j1,j2,,jkj_1,j_2,\dots,j_k,則所求的總和爲:
  v[j1]×p[j1]+v[j2]×p[j2]++v[jk]×p[jk]v[j_1]\times p[j_1]+v[j_2]\times p[j_2]+\dots+v[j_k]\times p[j_k]
請你幫助金明設計一個滿足要求的購物單。
輸入格式
輸入文件的第 11 行,爲兩個正整數,用一個空格隔開:
  N mN\space m,其中 N(<30000)N(<30000)表示總錢數,m(<25)m(<25) 爲希望購買物品的個數。
從第 22 行到第 m+1m+1 行,第 jj 行給出了編號爲 j1j-1 的物品的基本數據,每行有 22 個非負整數:
  v pv\space p,其中 vv 表示該物品的價格 (v10000)(v\leqslant10000)pp 表示該物品的重要度 (15)(1\sim5)
輸出格式
輸出文件只有一個正整數,爲不超過總錢數的物品的價格與重要度乘積的總和的最大值 (<100000000)(<100000000)
樣例輸入
1000 5
800 2
400 5
300 5
400 3
200 2
樣例輸出
3900
——————————————————————————————————————————————————
思路分析:剛拿到這道題的時候,如果沒有了解過揹包問題,很可能誤以爲是貪心,但如果我們仔細想一想,這道題的關鍵實際上應該是最大化地利用給定的金額 NN ,因此它應該是一道揹包題。揹包問題種類繁多,這裏涉及到的是最簡單的 00-11揹包,下面我們藉助該題來看一下 00-11 揹包的核心思路:
現在我們有 mm 件希望購買的物品,它們的價格與重要度分別爲 vi,piv_i,p_i,題目要求給出不超過總錢數的物品的價格與重要度乘積的總和的最大值,這看起來有點煩,我們簡化一下,定義 wi=vi×piw_i=v_i\times p_i 爲每件物品的價值,那麼我們要做的就是從這些物品中挑選出總價格不超過 NN 的物品,然後求出所有挑選方案中價值總和的最大值。
對於這 mm 個物品,每種物品只有兩個選擇:買或者不買,那麼這 mm 個物品就對應了 2m2^m 個挑選方案。最樸素的思路——枚舉,我們將這 2m2^m 個方案枚舉出來,分別價值總和,最後返回最大的。相信大家都聽說過指數爆炸,顯然,這個方案的時間複雜度是不可接受的。優化算法 inging!
先定義量 Rec(m, n)Rec(m,\space n):剩餘金額爲 nn,可購買物品爲 1m1\sim m 時,所能取得的最大化揹包價值(即已購買物品的價值之和)。這裏我們來仔細分析一下金明同學購買物品時的心路歷程:
現在還有第 1i1\sim i 個物品可以買啦,但是手上的錢只有 jj。這時根據是否購買第 ii 個物品,我們可以給出兩個方案。
a. 不買:那麼很簡單,第 ii 個物品直接被 passpass 掉,將目光投向第 1(i1)1\sim( i-1) 個,剩餘金額自然沒有變化,那麼得到 Rec(i, j)=Rec(i1, j)Rec(i,\space j)=Rec(i-1,\space j)
b. 買:首先我們的剩餘金額變爲 jv[i]j-v[i],接下來要做的就是如何用着 jv[i]j-v[i] 繼續購買第 1(i1)1\sim(i-1) 件物品,那麼 Rec(i, j)=Rec(i1,jv[i])+w[i]Rec(i,\space j)=Rec(i-1,j-v[i])+w[i]
我們現在要幹什麼?當然是從這兩個方案中選出價值更高的,依據這一原則,得到如下遞推式:
Rec(i, j)=max(Rec(i1,j), Rec(i1,jv[i])+w[i])Rec(i,\space j)=\max\big(Rec(i-1,j),\space Rec(i-1,j-v[i])+w[i]\big)
但是,這一遞推式並非最終結果,因爲我們還有一些細節沒有考慮到:
a. 如果壓根就沒有物品可供購買,那麼我們拿再多的錢都沒有意義,即 Rec(0, j)=0Rec(0,\space j)=0
b. 如果手上的錢少於第 ii 件物品,那麼我們也壓根兒用不着考慮是否購買它,直接得到 Rec(i, j)=Rec(i1, j)Rec(i,\space j)=Rec(i-1,\space j)
綜上,我們得到 00-11 揹包的通用遞推式:

Rec(i, j)={0i==0Rec(i1, j)j<v[i]max(Rec(i1,j), Rec(i1,jv[i])+w[i])Rec(i,\space j)=\begin{cases}0\hspace{1cm}i==0\\\\Rec(i-1,\space j)\hspace{1cm}j<v[i]\\\\\max\big(Rec(i-1,j),\space Rec(i-1,j-v[i])+w[i]\big)\end{cases}

下面我們試着利用遞推式解決金明的問題,首先自然想到遞歸法,但是請你們畫張圖走一下流程,這個算法的時間和空間複雜度都是非常高的,指數級的,沒意義。我們再來仔細想一想遞推式的推導過程,我們採取的策略實際上是把大問題分解爲子問題,最後合併子問題的最優解,得到大問題的最優解,相信有的同學已經看出來了——這就是動態規劃呀!OK,下面直接把遞推式轉化爲動態規劃的核心——動態方程:

dp[i][j]={0i==0dp[i1][j]j<v[i]max(dp[i1][j], dp[i1][jv[i]]+v[i])dp[i][j]=\begin{cases}0\hspace{1cm}i==0\\\\dp[i-1][j]\hspace{1cm}j<v[i]\\\\\max\big(dp[i-1][j],\space dp[i-1][j-v[i]]+v[i]\big)\end{cases}

解決這道問題,代碼如下:

#include <stdio.h>
#include <string.h>
#include <iomanip>
#include <algorithm>
using namespace std;
int dp[30][30005], w[30], v[30];
//dp[i][j]存儲的值爲還剩j元時,還有1~i可以選取,w[i]存儲i的重要程度,v[i]存儲i的價格
int main(){
	memset(dp, 0, sizeof(dp));
	memset(w, 0, sizeof(w));
	memset(v, 0, sizeof(v));//初始化
	int n, m;//共有n元,m件物品
	scanf("%d%d", &n, &m);
	for(int i=1; i<=m; i++) scanf("%d%d", &v[i], &w[i]);
	for(int i=1; i<=m; i++) w[i]*=v[i];//根據題意改寫重要度,爲重要等級與物品價格的乘積
	for(int i=1; i<=m; i++){//注意是從1開始
		for(int j=0; j<=n; j++){
			if(j<v[i]) dp[i][j]=dp[i-1][j];//如果剩餘的錢少於第i件物品,直接不予考慮
			else dp[i][j]=max(dp[i-1][j], dp[i-1][j-v[i]]+w[i]);
			//若剩餘的錢還可以買第i件物品,比較重要度,選取更高的
		}
	}
	printf("%d\n", dp[m][n]);
	return 0;
}

實際上我們用動態規劃解決 00-11 揹包問題的時候是可以優化一下空間使用的,即講 dpdp 數組由二維降成一維,從而降低算法的空間複雜度。如果我們瞭解過動態規劃,就會知道 dpdp 數組的作用是將子問題的最優解進行打表,從而在合併子問題求解大問題最優解的時候直接搜索。
那麼我們就可以從打表的流程入手,優化一下空間使用。根據上面的代碼,我們知道我們是一行一行,從左到右進行打表的,那麼我們在填寫第 ii 行的值時,只依賴於上一行,那麼我們直接開兩行數組,再不斷更新就可以了。但是,關鍵來了,動態方程的核心 dp[i][j]dp[i][j] 實際上只取決於上一行的前 j+1j+1 個值,那麼我們用一行數組,就能完成動態規劃:
用 dp[N] 存儲不同金額能夠購買的最大價值
(如果覺得有點難理解,建議手動模擬打表過程)
現在我們來看優化之後的代碼:

#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
int dp[30005], w[30], v[30];
int main(){
	int n, m;
	while(scanf("%d%d", &n, &m) != EOF){
		memset(dp, 0, sizeof(dp));
		memset(w, 0, sizeof(w));
		memset(v, 0, sizeof(v));
		for(int i=1; i<=m; i++){
			scanf("%d%d", &v[i], &w[i]);
			w[i]*=v[i];
		}
		for(int i=1; i<=m; i++){
			for(int j=n; j>=0; j--){
				if(j<v[i]) dp[j]=dp[j];
				else dp[j]=max(dp[j], dp[j-v[i]]+w[i]);
			}
		}
		printf("%d\n", dp[n]);
	}
	return 0;
}

效果顯著
在這裏插入圖片描述

最後,給出 00-11 揹包模版:

const int MAX = 100000;//視情況而定
int dp[MAX];//初始化的值同樣需要視情況而定
int vi[MAX], wi[MAX]; // v[i]:物品價值,w[i]:物品重量
//01揹包
void ZeroOnePack(int n, int w){
	//n:總共有n種物品; w:揹包總共可以承受的重量
	for(int i=1; i<=n; i++){
		for(int j=w; j>=0; j--){
			if(j<wi[i]) dp[j] = dp[j];
			else dp[j] = max(dp[j], dp[j-w[i]]+v[i]);
		}
	}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章