前言:這道題可以說是最經典的0/1揹包問題。
最近算是第一次正式遇到0/1揹包問題(以前都是暴力求解),這篇文章主要將這種類型的題目講細、講清楚。
0/1揹包
個人認爲:0/1揹包思路一想通了,就會容易理解,然後多練習類似的題型鞏固知識,就能掌握。
//代碼核心邏輯
for(int i=0; i<n; i++)
{
for(int j=V; j>=v[i]; j--)
dp[j] = max(dp[j-v[i]]+v[i], dp[j]);
}
例一:裝箱問題
有一個箱子容量爲VV(正整數,0 ≤ V ≤ 20000),同時有n個物品(0 < n < 30,每個物品有一個體積(正整數)。
要求n個物品中,任取若干個裝入箱內,使箱子的剩餘空間爲最小。
輸入格式
1個整數,表示箱子容量
1個整數,表示有n個物品
接下來n行,分別表示這n個物品的各自體積
輸出格式
1個整數,表示箱子剩餘空間。
輸入輸出樣例
輸入
24
6
8
3
12
7
9
7
輸出
0
題目所求:最小剩餘空間 = 揹包空間 - 揹包最多能裝的空間
題目與其是求最小剩餘空間,不如說是求該箱子最多能裝多少空間。
這道最普通的方法就是dfs暴力解法。
暴力dfs解法
主要思路:模擬每個物品按不同順序裝入揹包的情況。這種思路會導致有很多裝入的順序不同,但是最終能裝入揹包的空間是相同的。
可能該代碼能夠進行部分剪枝優化,但是仍然無法完全避免多次重複判斷的情況,從而造成程序運行超時。
#include<bits/stdc++.h>
using namespace std;
int V, n, v[35], ans=20001;
int judge[35]={false};
void dfs(int yy_v)
{
ans = min(ans, V-yy_v);
for(int i=0; i<n; i++)
{
if(judge[i]==false && yy_v+v[i]<=V)
{
judge[i]=true;
dfs(yy_v+v[i]);
judge[i]=false;
}
}
}
int main()
{
cin>>V>>n;
for(int i=0; i<n; i++)
cin>>v[i];
for(int i=0; i<n; i++)
{
if(V>=v[i])
{
judge[i]=true;
dfs(v[i]);
judge[i]=false;
}
}
cout<<ans;
return 0;
}
解題思路
首先我們看dp數組在該代碼中扮演的角色——不同類型的揹包。
dp[j] :揹包總空間爲 j 的揹包。 v[i] :第 i 件物品的空間大小。
步驟一:我們將每個物品都嘗試放入不同大小的揹包。
步驟二: 當該 dp[j] 揹包的總空間 j >= v[i],dp[j]揹包在空包的條件下,能裝得下中物品v[i]。
反之如果 j < v[i],dp[j]揹包無論如何都裝不得下中物品v[i]。
(ps:這也是爲什麼在代碼中 j 要由大到小的原因)
如何推出狀態轉移方程:dp[j] = max(dp[j - v[i]] + v[i], dp[j])?
讓我們想一想,如果揹包 dp[j] 本身就裝了很多空間,它裝的空間甚至比揹包dp[j - v[i]] 加上 v[i] 還要多,聰明的我們肯定是保留 dp[j] 本身的值,才能使揹包dp[j] 裝的空間最多。
反之,如果 dp[j] 本身裝的空間少於揹包dp[j - v[i]] 加上 v[i],那揹包 dp[j] 就能裝更多的空間,並且裝的物品空間就是揹包dp[j - v[i]] 加上 v[i]。
(ps:這裏的空間是指揹包中已裝物品佔用的空間)
步驟三:得出答案ans
揹包dp[V]最小剩餘空間 = 揹包dp[V]總面積 - 揹包dp[V]最多能裝的空間。
ans = V - dp[V]
#include<bits/stdc++.h>
using namespace std;
int V, n, v[35], ans;
int dp[20000];
int main()
{
memset(dp, 0, sizeof(dp));
cin>>V>>n;
for(int i=0; i<n; i++)
cin>>v[i];
for(int i=0; i<n; i++)
{
//將每個物品都放入不同大小的揹包
for(int j=V; j>=v[i]; j--)
dp[j] = max(dp[j-v[i]]+v[i], dp[j]);
}
cout<<V - dp[V];
return 0;
}
例二:kkksc03考前臨時抱佛腳
洛谷題目鏈接
題目描述
這次期末考試,kkksc03 需要考 4 科。因此要開始刷習題集,每科都有一個習題集,分別有s1,s2,s3,s4 道題目,完成每道題目需要一些時間,可能不等。
kkksc03 有一個能力,他的左右兩個大腦可以同時計算 2 道不同的題目,但是僅限於同一科。因此,kkksc03 必須一科一科的複習。
由於 kkksc03 還急着去處理洛谷的 bug,因此他希望儘快把事情做完,所以他希望知道能夠完成複習的最短時間。
輸入格式
本題包含 55 行數據:第 11 行,爲四個正整數s1,s2,s3,s4。
第 2 行,爲A1,A2,…,As1 共 s1 個數,表示第一科習題集每道題目所消耗的時間。
第 3 行,爲B1,B2,…,Bs2 共 s2 個數。
第 4 行,爲C1,C2,…,Cs3 共 s3 個數。
第 5 行,爲D1,D2,…,Ds4 共 s4 個數,意思均同上。
輸出格式
輸出一行,爲複習完畢最短時間。
輸入輸出樣例
輸入
1 2 1 3
5
4 3
6
2 4 3
輸出
20
說明/提示
1≤s1,s2,s3,s4≤20。
1≤A,B,C,D≤60。
這道題很容易做成想到貪心算法,可惜結果並沒有那麼簡單,不一定先複習大的就快、也不一定先複習小的較快。
複習一科最快的方法
一科的不同題目組合起來的複習時間m越接近總複習時間一半M,則該科複習時間就越短。(m>=M)
這裏求最短時間問題,可以利用到0/1揹包。
具體實現思路與可參照上題。
#include<bits/stdc++.h>
using namespace std;
struct node{
int time[25];
int num;
int total;
}km[5];
int f(int a){
int* arr;
int dp[1210], sum, total;
arr = km[a].time;
sum = km[a].num;
total = km[a].total;
memset(dp, 0, sizeof(dp));
sort(arr+1, arr+sum+1);
for(int i=1; i<=sum; i++){
for(int j=total; j>=arr[i]; j--){
dp[j] = max(dp[j-arr[i]]+arr[i], dp[j]);
}
}
for(int i=1; i<=total; i++){
if(dp[i]>=(total+1)/2){
return dp[i];
}
}
}
int main(){
int ans;
cin>>km[1].num>>km[2].num>>km[3].num>>km[4].num;
for(int j=1; j<=4; j++)
{
for(int i=1; i<=km[j].num; i++){
cin>>km[j].time[i];
km[j].total += km[j].time[i];
}
}
ans = f(1) + f(2) + f(3) + f(4);
cout<<ans;
return 0;
}
總結
0/1揹包問題解決方法主要是通過求小規模最優解,推到大規模最優解,難點在於狀態轉移方程的設計。
這種類型的題太常見了,每次都是用暴力求解,經常會失分,太可惜。
希望看完這篇博客的你,對0/1揹包有了更深入的理解,讓解題更加輕鬆、快樂。
一直將自己的學習經驗分享給有需要的人。
我是小鄭,一個堅持不懈的小白