1 基础知识点
首先设置一个二维数组dp[][],令dp[i][j]表示前i个物品装进容量为j的背包能获得的最大价值。通过设置这么一个二维数组,dp[n][m]的值就是0-1背包问题的解。
只考虑第i件物品时,可将情况分为 是否放入第i件物品 两种:
- 对于容量为j的背包,如果不放入第i件物品,那么这个问题就转换成将前i-1个物品放入容量为j的背包的问题,即dp[i][j] = dp[i-1][j]。
- 对于容量为j的背包,如果放入第i件物品,那么当前背包的容量就变成了j-w[i],并得到这个物品的价值v[i]。之后这个问题就转化成将前i-1个物品放入容量为j-w[i]的背包问题,即dp[i][j] = dp[i-1][j-w[i]]+v[i]。
从以上两种情况可以得到状态转移方程:dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]]+v[i])。转移时要注意j-w[i]的值是否为非负值,若为负则代表当前的容量无法放入第i件物品,不能进行转移。
边界情况处理:dp[i][0] = dp[0][j] = 0 (0<=i<=n, 0<=j<=m)
观察状态转移的特点,可以发现dp[i][j]的转移仅与dp[i-1][j-w[i]]和dp[i-1][j]有关,即仅与二维数组中本行的上一行有关。根据这个特点,可以将原本的二维数组优化为一维数组,并用如下的方式完成状态转移:dp[j] = max(dp[j], dp[j-w[i]]+v[i])。为了保证状态正确转移,必须保证在每次更新中确定状态dp[j]时,dp[j-w[i]]尚未被本次更新修改。这就需要在每次更新中,倒序遍历所有j值,因为只有这样才能保证在确定dp[j]的值时,dp[j-w[i]]的值尚未被修改,从而完成正确的状态转移。
2 例题——点菜问题
代码
#include <iostream>
using namespace std;
int main(){
int C,N;
while(cin>>C>>N){
int dp[1001];
int w[101],v[101];
for(int i=1;i<=N;i++)
cin>>w[i]>>v[i];
for(int i=0;i<=C;i++)
dp[i]=0;
for(int i=1;i<=N;i++){
for(int j=C;j>=w[i];j--){
dp[j] = max(dp[j],dp[j-w[i]]+v[i]);
}
}
cout<<dp[C]<<endl;
}
return 0;
}
3 例题——最小邮票数
解法1(自己想的)
dp[i][j]表示前i张邮票,恰能凑到j值所需要的最小张数,刚开始全部初始为-1
代码
#include <iostream>
using namespace std;
int main(){
int M,N;
while(cin>>M>>N){
int dp[100],val[20];
for(int i=1;i<=N;i++)
cin>>val[i];
for(int i=0;i<=M;i++)
dp[i]=-1;
for(int i=1;i<=N;i++){
for(int j=M;j>=val[i];j--){
int res = -1;
if(dp[j]!=-1) res = dp[j];
if(dp[j-val[i]]!=-1){
if(res==-1) res = dp[j-val[i]]+1;
else res = min(res,dp[j-val[i]]+1);
}
if(j-val[i]==0) res = 1;
dp[j] = res;
}
}
if(dp[M]==-1) cout<<0<<endl;
else cout<<dp[M]<<endl;
}
return 0;
}
解法2(网上大部分人的解题)
- 邮票总值相当于背包容量
- 邮票的面值相当于物品的重量
- 邮票的数量相当于物品的总价值
- 每张邮票的价值默认为1
代码
#include <iostream>
#include <climits>
using namespace std;
int main(){
int M,N;
while(cin>>M>>N){
int dp[100],val[20];
for(int i=1;i<=N;i++)
cin>>val[i];
for(int i=1;i<=M;i++)
dp[i]=INT_MAX-1; //定义初值,因为求最小值,将默认值设较大一些
dp[0]=0; // 这一步很关键
for(int i=1;i<=N;i++){
for(int j=M;j>=val[i];j--){
dp[j] = min(dp[j],dp[j-val[i]]+1);
}
}
if(dp[M]==INT_MAX-1) cout<<0<<endl;
else cout<<dp[M]<<endl;
}
return 0;
}
4 例题——PAT A1068(暂时还未通过测试用例)
暂时还未通过测试用例
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const int MAXN=1e4+1;
const int MAXM=1e2+1;
// 若前i个物品能恰好装满容量为j的背包,则dp[i][j]=1;反之,dp[i][j]=0
int arr[MAXN],dp[MAXM];
vector<int> pre[MAXM]; // pre[j]:能到达j容量的所有前驱容量
vector<int> path,temp;
bool Jud() { // 判断temp路径是否可以更新为最优路径
if(path.size()==0)
return true;
int len1=path.size(),len2=temp.size();
int i=len1-1,j=len2-1;
while(i>=0 && j>=0) {
if(path[i]!=temp[j])
return temp[j] < path[i];
i--,j--;
}
return false;
}
void DFS(int m) { // 寻找最优路径
if(m==0) {
if(Jud()) {
path.clear();
int len = temp.size();
for(int i=0; i<len; i++)
path.push_back(temp[i]);
}
} else {
for(int i=0; i<pre[m].size(); i++) {
temp.push_back(m-pre[m][i]);
DFS(pre[m][i]);
temp.pop_back();
}
}
}
int main() {
int N,M;
cin>>N>>M;
for(int i=1; i<=N; i++)
cin>>arr[i];
sort(arr+1,arr+N+1);
fill(dp,dp+N+1,0);
for(int i=1; i<=N; i++) {
for(int j=M; j>=arr[i]; j--) {
if(j==arr[i] || dp[j-arr[i]]==1) {
dp[j]=1;
pre[j].push_back(j-arr[i]);
}
}
}
if(pre[M].size()==0)
cout<<"No Solution"<<endl;
else {
DFS(M);
int len = path.size();
for(int i=len-1; i>=0; i--) {
if(i!=len-1)
cout<<" ";
cout<<path[i];
}
cout<<endl;
}
return 0;
}
5 总结
对能够划分阶段的问题来说,都可以尝试把阶段作为状态的一维,这样可以使我们更方便地得到满足无后效性的状态。
从滚动数组中可以得到启发:
如果当前设计的状态不满足无后效性,那么不妨把状态进行升维,即增加一维或若干维来表示相应的信息,这样可能就满足无后效性了。