昨天我的舍友给我推荐了一道题:
题目 01背包问题
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 ,价值是 。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入
第一行两个整数 N,V用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 ,用空格隔开,分别表示第 i 件物品的体积和价值。
输出
输出一个整数,表示最大价值。
数据范围
0<N,V≤10000
0<≤1000
当时正好学了深度优先搜索,我就说,这道题应该是搜索。舍友说看到题解用到了动态规划和状态转移之类的,不认为深度优先搜索能解决这个问题。我便回到座位上尝试用搜索解决这类问题,很快啊!我就写出了如下代码:
#include<bits/stdc++.h>
#include<algorithm>
using namespace std;
int currentBest = 0;
int ans=0;
struct item{
int v,w;
int index;
item(int iptV,int iptW,int iptIndex){
v=iptV;w=iptW;index=iptIndex;
}
};
struct bag{
vector<item> items;
int V=0;
int W=0;
bag(){}
};
void addItem(item iptItem,bag& iptBag){
iptBag.V = iptBag.V + iptItem.v;
iptBag.W = iptBag.W + iptItem.w;
iptBag.items.push_back(iptItem);
}
void deleteItem(int index,bag& iptBag){
item iptItem = iptBag.items[index];
iptBag.V = iptBag.V - iptItem.v;
iptBag.W = iptBag.W - iptItem.w;
}
void bfs(bag itemSet,int start,int maxV){
int N = itemSet.items.size();
bool found = true;
for(int i=start;i<N;i++){
bag nextSet = itemSet;
deleteItem(i,nextSet);
if(nextSet.V <= maxV && nextSet.W >= currentBest){
currentBest = nextSet.W;
found = false;
ans++;
}
else if(nextSet.V > maxV){
found = false;
}
if(!found) bfs(nextSet,i+1,maxV);
}
return;
}
int main(){
int N,maxV;
cin >> N >> maxV;
bag Allthings = bag();
for(int i=0;i<N;i++){
int v,w;
cin >> v >> w;
addItem(item(v,w,i),Allthings);
}
if(Allthings.V <= maxV){
cout << Allthings.W << endl;
return 0;
}
bfs(Allthings,0,maxV);
cout << currentBest << endl;
// cout << ans;
return 0;
}
思路是逐个排除物品,看是否能使总体积小于等于背包体积,如果能,去比较总价值与目前最优的大小关系。
我还做了一定的优化:如果去除k+1个物品得到的所有结果都没有去除k个物品的结果好,就不必再向下搜索,避免了无用的计算。在测试用例一中,只搜索了两次就能得到最终结果。
去掉 ans 的注释 第二行就是搜索的次数
但这远远承受不了更庞大的数据量,Submit后只能获得超时的结果,是时候去学习一个新的知识点了:
定义 动态规划
通过把原问题分解成若干相关子问题(所有这些问题在一定意义上答案固定),再利用已知子问题的答案依次计算未知问题答案,最终得到原问题答案的方式,称之为动态规划。
定义 状态
动态规划的状态可以笼统的解释成“问题所在的局面”。一般可以写成: (用变量表示的)所在局面的(最优)答案,或者满足某种性质的方案数。
状态的答案应该只依赖于状态定义的局面和一些状态以外的常量,也就是说,在一般情况下,状态变化并不会影响状态以外的量,该状态之后的演变不受这个状态之前决策的影响,因此如果要用动态规划来解决一道题,请把所有可能的变动加入到状态的定义中去。因为要求解最终状态的答案,所以状态之间需要存在某些关系。一般把这样的计算关系称作转移。
状态转移方程反映了状态与状态之间的关系,类似于递归。利用动态规划解决问题的核心是根据题目给状态一个良好的定义,个人理解:状态定义的局面(即自变量x)能快速得到状态的答案(f(x))。本题中,我们需要处理的问题是在有限的空间V里尽可能装多价值的物品,物品各不相同且都只有一个。什么可以作为自变量x呢?可以尝试去思考一下,例如可以考虑把选择装的物品的集合X作为自变量,自然是可以写出如下的状态转移方程:
f(X,V)为在集合X里选取物品的所有合法方式(总物品体积小于等于V)中的最大价值。
写出状态转移方程,就算是能看懂题目了,初学者可以选取多个状态定义的局面去写状态转移方程,拓宽问题角度,提高对状态这一词的理解。
这样的状态转移方程自然是可以用于编程中的,但枚举的情况相当多。这个状态转移方程定义的没问题,但是状态的局面选取的不够好。对于0-1背包问题,一种常规的思路是:
定义状态的局面为可能用的物品的范围(可能选取前i个物品),状态的答案是只在前i个物品中选取物品装入容积为V的背包所能产生的最大价值,可以得到状态转移方程:
我们只需要使用二维数组就能很快得出答案,代码实现如下:
#include<bits/stdc++.h>
using namespace std;
int main(){
int N,V;
cin >> N >> V;
vector<vector<int>> itemSet;
for(int i=0;i<N;i++){
int v,w;
cin >> v >> w;
itemSet.push_back({v,w});
}
vector<vector<int>> f(N,vector<int>(V+1,0));
for(int j=0;j<=V;j++){
if(itemSet[0][0]<=j)f[0][j]=itemSet[0][1];
}
for(int i=1;i<N;i++){
for(int j=0;j<itemSet[i][0];j++){
f[i][j] = f[i-1][j];
} for(int j=itemSet[i][0];j<=V;j++){
f[i][j] = max(f[i-1][j],f[i-1][j-itemSet[i][0]] + itemSet[i][1]);
}
}
cout << f[N-1][V];
return 0;
}
注意:需要对只选第一个物品的情况进行初始化(即代码中的f[0][j]的情形)
本解答的空间复杂度为,实际上可以优化到,留作习题。
感谢你能看到这里。