本篇文章設計的題目均在AcWing的2-12題。
一、01揹包
要求:每種物品只能選擇0個或1個,即對於每種物品只有選或者不選兩種情況。
題目描述:(題目鏈接)
有 件物品和一個容量是 的揹包。每件物品只能使用一次。第 件物品的體積是 ,價值是 。求解將哪些物品裝入揹包,可使這些物品的總體積不超過揹包容量,且總價值最大。輸出最大價值。
輸入格式:
第一行兩個整數,,用空格隔開,分別表示物品數量和揹包容積。接下來有 行,每行兩個整數 ,用空格隔開,分別表示第 件物品的體積和價值。
輸出格式:
輸出一個整數,表示最大價值。
數據範圍:
輸入樣例:
4 5
1 2
2 4
3 4
4 5
輸出樣例:
8
思路1:二維數組記錄
f[i][j]
表示只看前i
物品,總體積是j
的情況下總價值最大是多少。最大值答案就在f[n][0-V]
中枚舉最大值即可,res=max(f[n][0-V])
。
假設我們將前i-1
個物品已計算完畢,考慮第i
個物品,體積爲j
時,第i
個物品只有兩種選擇,即選或者不選。
- 若不選,則相當於只考慮前
i-1
個物品且體積爲j
的情況,這種情況下f[i][j]=f[i-1][j]
; - 若選,則相當於揹包容量只剩下
j-v[i]
,這種情況下f[i][j]=f[i-1][j-v[i]]+w[i]
最終結果就相當於在這兩種情況下取個最大值即可。還有個問題就是其初始化,很簡單,就是沒有物品給你選擇,揹包容量爲0時f[0][0]=0
。
時間複雜度和空間複雜度都爲。
程序(CPP):
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int f[N][N];
int v[N], w[N];
int main()
{
int n, m; cin >> n >> m;
for(int i = 1; i <= n; i++)
cin >> v[i] >> w[i];
f[0][0] = 0;
for(int i = 1; i <= n; i++)
for(int j = 0; j <= m; j++)
{
f[i][j] = f[i-1][j];
if(j >= v[i]) // 需要判斷一下當前容積能裝下該物品
f[i][j] = max(f[i][j], f[i-1][j-v[i]]+w[i]);
}
int res = 0;
for(int i = 0; i <= m; i++)
res = max(res, f[n][i]);
cout << res << endl;
return 0;
}
思路2:滾動數組優化爲一維
程序(CPP):
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int f[N];
int v[N], w[N];
int main()
{
int n, m; cin >> n >> m;
for(int i = 1; i <= n; i++)
cin >> v[i] >> w[i];
for(int i = 1; i <= n; i++)
for(int j = m; j >= v[i]; j--)
f[j] = max(f[j], f[j-v[i]]+w[i]);
cout << f[m] << endl;
return 0;
}
二、完全揹包
要求:每種物品可以選0個或者無限個.
題目描述:(題目鏈接)
有 件物品和一個容量是 的揹包,每種物品都有無限件可用。第 件物品的體積是 ,價值是 。求解將哪些物品裝入揹包,可使這些物品的總體積不超過揹包容量,且總價值最大。輸出最大價值。
輸入格式:
第一行兩個整數,,用空格隔開,分別表示物品數量和揹包容積。接下來有 行,每行兩個整數 ,用空格隔開,分別表示第 件物品的體積和價值。
輸出格式:
輸出一個整數,表示最大價值。
數據範圍:
輸入樣例:
4 5
1 2
2 4
3 4
4 5
輸出樣例:
10
思路1:
f[i][j]
表示只看前i
物品,總體積是j
的情況下總價值最大是多少。最大值答案就在f[n][0-V]
中枚舉最大值即可,res=max(f[n][0-V])
。
假設我們將前i-1
個物品已計算完畢,考慮第i
個物品,體積爲j
時,第i
個物品只有多種選擇,即選0個,1個,k個。我們可以再加上一重循環,枚舉每個物品能選的個數k
,注意k
的範圍爲(int k=0; k*v[i]<=j; k++)
程序(CPP):
#include <iostream>
using namespace std;
const int N = 1010;
int f[N][N], v[N], w[N];
int main()
{
int n, m; cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> v[i] >> w[i];
for(int i = 1; i <= n; i++)
for(int j = 0; j <= m; j++)
for(int k = 0; k * v[i] <= j; k++)
f[i][j] = max(f[i][j], f[i-1][j - k*v[i]] + k*w[i]);
int res = 0;
for(int i = 0; i <= m; i++) res = max(res, f[n][i]);
cout << res << endl;
return 0;
}
思路2:利用滾動數組優化到一維,同01揹包優化思想
#include <iostream>
using namespace std;
const int N = 1010;
int f[N], v[N], w[N];
int main()
{
int n, m; cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> v[i] >> w[i];
for(int i = 1; i <= n; i++)
for(int j = m; j >= v[i]; j--)
for(int k = 0; k * v[i] <= j; k++)
f[j] = max(f[j], f[j - k*v[i]] + k*w[i]);
cout << f[m] << endl;
return 0;
}
思路3:
f[i]
表示總體積是i
的情況下最大價值是多少,最終答案就是max(f[i])
,其實也就是f[最大體積]
,因爲體積越大肯定能裝下物品的最大價值越高,至少不會降低。這裏面體積j
從小到大枚舉表示f[i]
可能從第i
個物品轉移過來;從大到小枚舉的話表示只能從i-1
個物品轉移過來。因此完全揹包相對於01揹包只需要體積從小到大枚舉。
程序(CPP):
#include <iostream>
using namespace std;
const int N = 1010;
int f[N], v[N], w[N];
int main()
{
int n, m; cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> v[i] >> w[i];
for(int i = 1; i <= n; i++)
for(int j = v[i]; j <= m; j++)
f[j] = max(f[j], f[j-v[i]]+w[i]);
cout << f[m] << endl;
return 0;
}
Note:如果題目問的是恰好用了m
的體積,問最大價值?這時候只需要初始化的時候除了f[0]=0
外其他賦值爲-inf
即可。
三、多重揹包
要求:每種物品能選擇的個數給個限制。
思路1:直接枚舉每種物品能選擇的個數k
即可。
#include <iostream>
using namespace std;
const int N = 110;
int f[N][N];
int main()
{
int n, m; cin >> n >> m;
for(int i = 1; i <= n; i++)
{
int v, w, num; cin >> v >> w >> num;
for(int j = 0; j <= m; j++)
for(int k = 0; k <= num && k*v <= j; k++)
f[i][j] = max(f[i][j], f[i-1][j - k*v] + k*w);
}
int res = 0;
for(int i = 0; i <= m; i++) res = max(res, f[n][i]);
cout << res << endl;
return 0;
}
思路2:滾動數組優化空間
#include <iostream>
using namespace std;
const int N = 110;
int f[N];
int main()
{
int n, m; cin >> n >> m;
for(int i = 1; i <= n; i++)
{
int v, w, num; cin >> v >> w >> num;
for(int j = m; j >= v; j--)
for(int k = 0; k <= num && k*v <= j; k++)
f[j] = max(f[j], f[j - k*v] + k*w);
}
cout << f[m] << endl;
return 0;
}
思路3:二進制優化
假設有a個物品b,那麼我們將物品b拆分爲a份,每一份看作一個單獨的物品,這樣就將多重揹包轉化爲01揹包啦!但我們需要按照一份一份拆分嗎?其實是不必的,比如有7個物品b,那麼我們只需要拆分爲1個b,2個b,4個b,這樣我們能表示出任意一個物品b,假設我們選擇6個b,那麼就是4+2。即給定n個物品b,我們根據二進制拆分法能拆出來log(n)向上取整個數,這些數能表示出[0-n]中任意一個數。藉助於這個思想,我們就可以進行優化,假設有n個物品,揹包容量爲V,每種物品個數不超過num,則時間複雜度由暴力的變爲。
這裏還有個問題,如何找出個數使得這些數的組合能表示[0,n]中任意一個數。假設,我們取前三個數爲1,2,4,最後一個數不能是8,因爲是8的話就能表示出[0-15]間任意一個數,而每種物品個數不能超過10。那最後一個數怎麼選呢?其實這個數就是10-1-2-4=3,因爲1,2,4能表示出[0-7]間任意一個數,再拿出一個數3,與[0-7]間數相加,就可以得到[8-10]間任意一個數且不會超過10,因此1,2,4,3這4個數,就可以拼出任意[0-10]區間的數。
程序(CPP):
#include <iostream>
#include <vector>
using namespace std;
const int N = 2010;
int f[N];
struct Good
{
int v, w;
};
int main()
{
int n, m; cin >> n >> m;
vector<Good> goods;
for(int i = 1; i <= n; i++)
{
int v, w, s; cin >> v >> w >> s;
// 二進制拆分物品
for(int k = 1; k <= s; k *= 2)
{
s -= k;
goods.push_back({v*k, w*k});
}
if(s>0) goods.push_back({v*s, w*s});
}
// 01揹包
for(auto good: goods)
for(int j = m; j >= good.v; j--)
f[j] = max(f[j], f[j - good.v] + good.w);
cout << f[m] << endl;
return 0;
}
四、混合揹包問題
要求:每種物品的個數分爲多種情況。如下圖所示:
其實這個題跟多重揹包完全一樣,無非就是要設置下每種物品個數上限,最樸素的,直接按照多重揹包的不優化版寫出程序如下:
#include <iostream>
using namespace std;
const int N = 1010;
int f[N];
int main()
{
int n, m; cin >> n >> m;
for(int i = 1; i <= n; i++)
{
int v, w, s; cin >> v >> w >> s;
if(s==-1) s = 1; // 設置物品數上限爲1
else if(s==0) s = 1010; // 設置物品無限個,不超過揹包體積
for(int j = m; j >= v; j--) //
for(int k = 0; k <= s && k * v <= j; k++)
f[j] = max(f[j], f[j - k * v] + k * w);
}
cout << f[m] << endl;
return 0;
}
利用二進制優化才能過這個題,如下:
#include <iostream>
#include <vector>
using namespace std;
const int N = 1010;
int f[N];
struct Good
{
int v, w;
};
int main()
{
vector<Good> goods;
int n, m; cin >> n >> m;
for(int i = 1; i <= n; i++)
{
int v, w, s; cin >> v >> w >> s;
if(s==-1) s = 1;
else if(s==0) s = 1010;
for(int k = 1; k <= s; k *= 2)
{
s -= k;
goods.push_back({k * v, k * w});
}
if(s>0) goods.push_back({s * v, s * w});
}
// 01揹包
for(auto good: goods)
for(int j = m; j >= good.v; j--)
f[j] = max(f[j], f[j - good.v] + good.w);
cout << f[m] << endl;
return 0;
}
五、二維費用的揹包問題
要求:一維揹包問題可能只有一個限制,比如只有揹包容量的限制;二維的話有兩個限制,比如揹包容量和物品總重量限制。來個題就明白啦:
樣例:
輸入:
4 5 6
1 2 3
2 4 4
3 4 5
4 5 6
輸出:
8
思路:
f[i][j]
表示體積是i
,重量是j
時最大價值。第一重循環枚舉物品,第二重循環枚舉體積,第三重循環枚舉重量。因爲是01揹包,故而都是從大到小枚舉。程序如下:
#include <iostream>
using namespace std;
const int N = 1010;
int f[N][N];
int main()
{
int n, V, M; cin >> n >> V >> M;
for(int i = 1; i <= n; i++)
{
int v, m, w; // 體積、重量、價值
cin >> v >> m >> w;
for(int j = V; j >= v; j--)
for(int k = M; k >= m; k--)
f[j][k] = max(f[j][k], f[j - v][k - m] + w);
}
cout << f[V][M] << endl;
return 0;
}
六、分組揹包問題
要求:把物品分成若干組,每組裏面最多選一件,來個題目:
樣例:
輸入:
3 5
2
1 2
2 4
1
3 4
1
4 5
輸出:
8
思路:
f[j]
表示在j
體積下的最大價值。與01揹包類似,第一重循環物品,第二重循環從大到小循環體積,然後依次把該組的每一個物品要麼放進去要麼不放進去。假設每一組有s
個物品,則
for(int i = 1; i <= n; i++)
for(int j = m; j >= v; j--)
f[j] = max(f[j], f[j - v[0]]+w[0],..., f[j - v[s-1]]+w[s-1])
程序:
#include <iostream>
using namespace std;
const int N = 110;
int f[N], v[N], w[N];
int main()
{
int n, m; cin >> n >> m;
for(int i = 1; i <= n; i++)
{
int s; cin >> s;
for(int k = 1; k <= s; k++) cin >> v[k] >> w[k];
for(int j = m; j >= 0; j--)
for(int k = 1; k <= s; k++)
if(j >= v[k]) f[j] = max(f[j], f[j - v[k]] + w[k]);
}
cout << f[m] << endl;
return 0;
}
七、揹包問題求方案數
輸入樣例:
4 5
1 2
2 4
3 4
4 6
輸出樣例:
2
思路:
求方案數的時候就不能再像之前定義f[j]
表示體積不超過j
的情況下最大價值,而應該表示爲體積恰好爲j
的情況下的最大價值,我們可以在初始化的時候將f[j]
除了f[0]=0
外其他全部初始化爲負無窮。同時還需要新增加一個數組g[j]
,表示體積恰好爲j
的情況下得到最大價值的方案數。g[j]
更新方案如下所述:
g[0]=1
f[j]=max(f[j], f[j-v[i]]+w[i])
,當f[j]
由f[j]
轉移而來時更新g[j]+=g[j]
,當其由f[j-v[i]]+w[i]
轉移而來時更新g[j]+=g[j-v[i]]
,當二者相同時g[j]+=g[j]+g[j-v[i]]
- 求揹包實現最大價值時的方案數,自然先求出揹包所裝物品的最大價值
maxW
,再求出用了多大的體積j
實現了最大價值,最後答案就是這些實現最大價值的體積下對應的方案數之和
#include <iostream>
using namespace std;
const int N = 1010;
const int mod = 1e9+7, INF = 1e9;
int f[N]; // f[N]表示恰好體積是j的情況下最大價值
int g[N]; // g[N]表示體積是j的情況下最大方案數
int main()
{
int n, m; cin >> n >> m;
g[0] = 1; // 初始方案數賦值
for(int i = 1; i <= m; i++) f[i] = -INF; // 賦值爲負無窮才能得到體積恰好爲j時的最大價值
for(int i = 1; i <= n; i++)
{
int v, w; cin >> v >> w;
for(int j = m; j >= v; j--)
{
int t = max(f[j], f[j-v]+w);
int s = 0; // 記錄當前體積下方案數
if(t==f[j]) s += g[j]; // 是從體積j轉移而來
if(t==f[j-v]+w) s += g[j-v]; // 是從體積j-v轉移而來
s = s % mod;
f[j] = t, g[j] = s;
}
}
int maxW = 0; // 搜素最大價值
for(int i = 0; i <= m; i++) maxW = max(maxW, f[i]);
int res = 0; // 記錄總的方案數
for(int i = 0; i <= m; i++)
{
if(maxW == f[i]) // 當前體積下得到最大價值
{
res += g[i];
res %= mod;
}
}
cout << res << endl;
return 0;
}
八、最優物品選擇方案
輸入樣例:
4 5
1 2
2 4
3 4
4 6
輸出樣例:
2
思路:見博客
九、有依賴的揹包問題
要求:選一件物品前必須選其依賴的物品