C語言動態(tài)規(guī)劃之背包問題詳解
01背包問題
給定n種物品,和一個容量為C的背包,物品i的重量是w[i],其價值為v[i]。問如何選擇裝入背包的物品,使得裝入背包中的總價值最大?(面對每個武平,只能有選擇拿取或者不拿兩種選擇,不能選擇裝入某物品的一部分,也不能裝入物品多次)
- 聲明一個數(shù)組
f[n][c]的二維數(shù)組,f[i][j]表示在面對第i件物品,且背包容量為j時所能獲得的最大價值。 - 根據(jù)題目要求進(jìn)行打表查找相關(guān)的邊界和規(guī)律
- 根據(jù)打表列寫相關(guān)的狀態(tài)轉(zhuǎn)移方程
- 用程序?qū)崿F(xiàn)狀態(tài)轉(zhuǎn)移方程
真題演練:
一個旅行者有一個最多能裝M公斤的背包,現(xiàn)在有n件物品,它們的重量分別是W1、W2、W3、W4、…、Wn。它們的價值分別是C1、C3、C2、…、Cn,求旅行者能獲得最大價值。
輸入描述:
第一行:兩個整數(shù),M(背包容量,M<= 200)和N(物品數(shù)量,N<=30);
第2…N+1行:每行兩個整數(shù)Wi,Ci,表示每個物品的質(zhì)量與價值。
輸出描述:
僅一行,一個數(shù),表示最大總價值
樣例:
輸入:
10 4
2 1
3 3
4 5
7 9
輸出:
12
解題步驟
定義一個數(shù)組dp[i][j]表示容量為j時,拿第i個物品時所能獲取的最大價值。
按照題目要求進(jìn)行打表,列出對應(yīng)的dp表。
| W[i](質(zhì)量) | V[i](價值) | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ||
| 2 | 1 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| 3 | 3 | 0 | 0 | 1 | 3 | 3 | 4 | 4 | 4 | 4 | 4 | 4 |
| 4 | 5 | 0 | 0 | 1 | 3 | 5 | 5 | 6 | 8 | 8 | 9 | 9 |
| 7 | 9 | 0 | 0 | 1 | 3 | 5 | 5 | 6 | 9 | 9 | 10 | 12 |
對于一個動態(tài)規(guī)劃問題設(shè)置下標(biāo)時最好從0開始,因?yàn)閯討B(tài)規(guī)劃經(jīng)常會和上一個狀態(tài)有關(guān)系!從上面的dp表可以看出來對于一個物品我們拿還是不難需要進(jìn)行兩步來判斷。第一步:判斷背包當(dāng)前的容量j是否大于物品當(dāng)前的質(zhì)量,如果物品的質(zhì)量大于背包的容量那么就舍棄。第二步:如果背包可以裝下這個物品,就需要判斷裝下該物品獲取的最大價值是不是大于不裝下這個物品所獲取的最大價值,如果大于那么就把東西裝下!根據(jù)這樣的思想我們可以得到狀態(tài)轉(zhuǎn)移方程:
如果單簽背包的容量可以裝下物品:
dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
如果當(dāng)前背包的容量裝不下該物品:
dp[i][j]=dp[i-1][j];
#include <stdio.h>
int max(const int a,const int b)
{
return a>b ? a:b;
}
int main()
{
int w[35]={0},v[35]={0},dp[35][210]={0};
int n,m;
scanf("%d %d",&m,&n);
int i,j;
for(i=1;i<=n;i++){
scanf("%d %d",&w[i],&v[i]);
}
for(i=1;i<=n;i++){
for(j=1;j<=m;j++){
if(j>=w[i])//如果當(dāng)前背包的容量大于商品的質(zhì)量
{
dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);//判斷是否應(yīng)該拿下
}
else//大于背包的當(dāng)前容量
{
dp[i][j]=dp[i-1][j];
}
}
}
for(int k=0;k<=n;k++)
{
for(int l=0;l<=m;l++)
{
printf("%d ",dp[k][l]);
}
printf("\n");
}
printf("%d\n",dp[n][m]);
}

通過運(yùn)行以上程序可以看到最終的輸出dp表和我們的預(yù)期是相符合的!但是并沒有結(jié)束,動態(tài)規(guī)劃有一個后無效性原則(當(dāng)前狀態(tài)只與前一個狀態(tài)有關(guān))。那么我們就可以對dp數(shù)組進(jìn)行壓縮處理,將二維數(shù)組轉(zhuǎn)換成一維數(shù)組。每一次選擇物品對這個數(shù)組進(jìn)行更新就可以啦!那么就可以將狀態(tài)轉(zhuǎn)移方程壓縮成為 dp[j]=max(dp[j],dp[j-w[i]]+v[i]) 。不過我們需要注意的是在壓縮過后我們需要逆序刷新數(shù)組的值,如果正序刷新的話就不能保存上一個階段對應(yīng)獲取最大價值的值了。那么我們就可以寫出以下程序:
#include <stdio.h>
int max(const int a,const int b)
{
return a>b ? a:b;
}
int main()
{
int w[35]={0},v[35]={0},dp[210]={0};
int n,m;
scanf("%d %d",&m,&n);
int i,j;
for(i=1;i<=n;i++){
scanf("%d %d",&w[i],&v[i]);
}
for(i=1;i<=n;i++){
for(j=m;j>=0;j--){
if(j>=w[i])//如果當(dāng)前背包的容量大于商品的質(zhì)量
{
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);//判斷是否應(yīng)該拿下
}
}
for(int k=0;k<=m;k++)
{
printf("%d ",dp[k]);
}
printf("\n");
}
printf("%d\n",dp[n][m]);
}

可以看出和上面輸出的dp表并沒有什么區(qū)別!
完全背包問題
題目描述:
設(shè)有n種物品,每種物品有一個重量及一個價值,但每種物品的數(shù)量都是無限的,有一個背包,最大載重量為m,今從n中物品中選取若干件(同一種物品可以多次選?。┦蛊滟|(zhì)量小于等于m,而價值的和為最大。
輸入:
第一行:兩個整數(shù),M(背包容量,M<= 200)和N(物品數(shù)量,N<=30);
第2…N+1行:每行兩個整數(shù)Wi,Ci,表示每個物品的質(zhì)量與價值。
輸出:
僅一行,一個數(shù),表示最大總價值。
樣例:
輸入: 10 4 2 1 3 3 4 5 7 9 輸出: 12
與01背包問題不同的是這不是每個物品選擇拿與不拿的問題了,而是要選擇幾個該物品,最終選擇價值最大的。那么我們可以在01背包的問題上繼續(xù)進(jìn)行思考這個問題,01背包中我們知道了之前的狀態(tài),那么我無非就是要判斷拿k個物品和不拿時進(jìn)行比較,如果價值比之前大就拿下。而每個種類的物品最多只能拿取j/w[i]個,再加一重循環(huán)不就可以啦!程序的核心代碼如下:
for(i=1;i<=n;i++){
for(j=m;j>=0;j--){
for(k=0;k<=j/w[i];k++)
{
dp[j]=max(dp[j],dp[j-k*w[i]]+k*v[i]);//判斷是否應(yīng)該拿下k個商品
}
}
}
通過代碼可以發(fā)現(xiàn)通過這種樸素的算法是需要三重循環(huán)的,好像對時間復(fù)雜度比較高。那么我們也借鑒01背包來對完全背包進(jìn)行打表!
| w[i](質(zhì)量) | v[i](價值) | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ||
| 2 | 1 | 0 | 0 | 1 | 1 | 2 | 2 | 3 | 3 | 4 | 4 | 5 |
| 3 | 3 | 0 | 0 | 1 | 3 | 3 | 4 | 6 | 6 | 7 | 9 | 9 |
| 4 | 5 | 0 | 0 | 1 | 3 | 5 | 5 | 6 | 8 | 10 | 10 | 11 |
| 7 | 9 | 0 | 0 | 1 | 3 | 5 | 5 | 6 | 9 | 10 | 10 | 12 |
根據(jù)表中紅色標(biāo)記的數(shù)值來看,需要注意的是如果背包的容量不能裝下當(dāng)前物品的質(zhì)量。那么當(dāng)前容量所能裝下價值最大的物品就等于上一個物品所能保存的最大價值。重點(diǎn)看一下4是怎么來的,這個4并不是從 i-1來的,而是從i來的。通過正序推出該物品的價值。狀態(tài)轉(zhuǎn)移方程就可以寫成是 :dp[i][j]=max(dp[i-1][j],dp[i][j-w[i]]+v[i]) 和01背包的唯一區(qū)別是max的第二個參數(shù)。01背包是i-1,而完全背包是i,而且是通過正序推理得到的狀態(tài)轉(zhuǎn)移方程。
根據(jù)狀態(tài)轉(zhuǎn)移方程應(yīng)該很快就能寫出程序了吧!但是根據(jù)dp的后無效性原則,對動態(tài)規(guī)劃狀態(tài)轉(zhuǎn)移方程進(jìn)行壓縮!壓縮過后就是dp[j]=max(dp[j],dp[j-w[i]]+v[i]) ,小伙伴們一看是不是和01背包的狀態(tài)轉(zhuǎn)移方程一模一樣啊!但是但是兩個有個重大的區(qū)別:01背包使用的是上一條的數(shù)據(jù),所以需要逆序避免覆蓋之前的值,而完全背包是從當(dāng)前更新后的數(shù)據(jù)進(jìn)行相關(guān)的操作的 。通過以上分析我們可以寫出如下程序:
#include <stdio.h>
int max(const int a,const int b)
{
return a>b ? a:b;
}
int main()
{
int w[35]={0},v[35]={0},dp[210]={0};
int n,m;
scanf("%d %d",&m,&n);
int i,j;
for(i=1;i<=n;i++){
scanf("%d %d",&w[i],&v[i]);
}
for(i=1;i<=n;i++){
for(j=0;j<=m;j++){
if(j>=w[i])//如果當(dāng)前背包的容量大于商品的質(zhì)量
{
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);//判斷是否應(yīng)該拿下
}
}
for(int k=0;k<=m;k++)
{
printf("%d ",dp[k]);
}
printf("\n");
}
printf("%d\n",dp[m]);
}

通過以上代碼的輸出可以看出打印的dp表和我們推測的并沒有什么區(qū)別,程序正確!
多重背包問題
題目描述:
為了慶祝班級在校運(yùn)會上取得了全校第一名的好成績,班主任決定開一場慶功會,為此撥款購買獎品犒勞運(yùn)動員。期望撥款金額能夠購買最大價值的獎品,可以補(bǔ)充他們的精力和體力。
輸入:
第一行輸入兩個數(shù)n(n<=500),m(m<=6000),其中n代表希望購買的獎品的種數(shù),m表示撥款金額。
接下來的n行,每行3個數(shù),w,v,s分別表示第i種獎品的價格、價值(價格與價值是不同的概念)和能購買的最大數(shù)量(買0件到s件均可),其中w<=100,v<=1000,s<=10;
輸出:
一行:一個數(shù),表示此次購買能獲得的最大價值(注意!不是價格)。
示例:
輸入:
5 1000
輸出:
80 20 4
40 50 9
30 50 7
40 30 6
20 20 1
與完全背包不同的是:完全背包每個物品的個數(shù)是無限的,而多重背包是每個物品只能拿指定的件數(shù)。那么最容易想到的方法就是把相同的物品分開,比如說有n個a物品,就將它分成a1 a2 a3 a4…an然后用01背包的方法去解決。那么我們就可以寫出以下核心代碼:
for(i=1;i<=n;i++){
for(j=m;j>=0;j--){
for(k=0;k<=s[i]&&j>=k*w[i];k++)
{
dp[j]=max(dp[j],dp[j-k*w[i]]+k*v[i]);//從01背包的狀態(tài)轉(zhuǎn)移方程,去增加第i個物品拿k個的循環(huán)
}
}
}
通過以上的代碼可以看出當(dāng)s[i]特別大的時候那么就會消耗非常多的時間復(fù)雜度,那么肯定是有優(yōu)化的方法的!那么我們可以通過二進(jìn)制來對這個同一個物品應(yīng)該拿取幾個進(jìn)行優(yōu)化。我們可以通過以下問題進(jìn)行研究:
有1000個蘋果,10個箱子怎么放,不管我想拿多少個蘋果,都可以成箱成箱的拿?
用二進(jìn)制的思想就是每一個箱子代表二進(jìn)制對應(yīng)的位數(shù),那么210大于1024應(yīng)該是可以滿足題目條件的。那么每個箱子放的蘋果分別是1,2,4,8,16,32,…488(1000-512)。需要一個蘋果拿第一箱,需要兩個蘋果拿第二項(xiàng),需要三個蘋果拿一二箱。那么對于需要拿1000箱的問題本來要循環(huán)1000次,經(jīng)過優(yōu)化以后只用循環(huán)10次就可以啦!那么我們就可以寫出以下程序啦!
for(i=1;i<=n;i++){
for(j=m;j>=0;j--){
for(k=0;k<=s[i]&&j>=k*w[i];k<<=1)
{
if((k<<1)>s[i]&&j>=k*w[i])
{
dp[j]=max(dp[j],dp[j-(s[i]-k)*w[i]]+(s[i]-k)*v[i]);
}
else
dp[j]=max(dp[j],dp[j-k*w[i]]+k*v[i]);//從01背包的狀態(tài)轉(zhuǎn)移方程,去增加第i個物品拿k個的循環(huán)
}
}
}
動態(tài)規(guī)劃解題思路
對于動態(tài)規(guī)劃問題我們一般的思路如下:
- 判斷是動態(tài)規(guī)劃的解題思路以后立馬定義一個數(shù)組,把數(shù)組對應(yīng)的下標(biāo)、對應(yīng)的值想清楚。
- 然后根據(jù)題目意思找規(guī)律進(jìn)行打表,找出初始狀態(tài)以及一些邊界條件。
- 根據(jù)打表的數(shù)據(jù)找出狀態(tài)轉(zhuǎn)移方程。
- 最后根據(jù)狀態(tài)轉(zhuǎn)移方程進(jìn)行編寫程序。
到此這篇關(guān)于C語言動態(tài)規(guī)劃之背包問題詳解的文章就介紹到這了,更多相關(guān)C語言 背包內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C++實(shí)現(xiàn)自定義撤銷重做功能的示例代碼
在使用c++做界面開發(fā)的時候,尤其是實(shí)現(xiàn)白板功能時需要自己實(shí)現(xiàn)一套撤銷重做功能.如果是qt則有QUndoable對象,可以直接拿來用。但是如果是使用gdi繪圖,則可能需要自己實(shí)現(xiàn)了。本文就來用C++實(shí)現(xiàn)自定義撤銷重做功能,需要的可以參考一下2022-12-12
C++實(shí)現(xiàn)LeetCode(80.有序數(shù)組中去除重復(fù)項(xiàng)之二)
這篇文章主要介紹了C++實(shí)現(xiàn)LeetCode(80.有序數(shù)組中去除重復(fù)項(xiàng)之二),本篇文章通過簡要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-07-07
C++實(shí)現(xiàn)約瑟夫環(huán)的循環(huán)單鏈表
這篇文章主要為大家詳細(xì)介紹了C++實(shí)現(xiàn)約瑟夫環(huán)的循環(huán)單鏈表,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-10-10
C語言中無符號數(shù)和有符號數(shù)之間的運(yùn)算
C語言中有符號數(shù)和無符號數(shù)進(jìn)行運(yùn)算默認(rèn)會將有符號數(shù)看成無符號數(shù)進(jìn)行運(yùn)算,其中算術(shù)運(yùn)算默認(rèn)返回?zé)o符號數(shù),邏輯運(yùn)算當(dāng)然是返回0或1了。下面通過一個例子給大家分享C語言中無符號數(shù)和有符號數(shù)之間的運(yùn)算,一起看看吧2017-09-09
Visual Studio 2022 的安裝和創(chuàng)建C++項(xiàng)目(圖文教程)
本文主要介紹了Visual Studio 2022 的安裝和創(chuàng)建C++項(xiàng)目,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05

