0/1背包问题
题目:有n(n <= 100) 个物品和一个容量为 m ( m <= 10000 )的背包。
第 i 个物品的容量是 c[i], 价值是 w[i]。现在需要选择一些物品放入背包,并且总容量不能超过背包容量,求能够达到的物品的最大总价值。
之所以叫 0/1 背包,是因为每种物品只有一个,可以选择放入背包或者不放,而 0 代表不放,1 代表放。
1.设计状态
状态(i , j) 表示 前 i 个物品 放入到 容量为 j 的背包中(0 <= i <= n , 0 <= j <= m)。
令 dp[i][j] 表示状态 (i, j) 下该背包得到的最大价值,即前 i 个物品放入容量为 j 的背包所得到的最大总价值;
2. 状态转移方程
dp[i][j] = max(dp[i-1][j] , dp[i-1][j-c[i]] + w[i]);
对于第 i 个物品有两种选择 放 或 不放。
1 不放
第 i 个物品不放入容量为 j 的背包,则问题转变为 “ 前 i - 1 个物品放入容量为 j 的背包 ” ,则
dp[i][j] = dp[i-1][j];
2 放
第 i 个物品放入容量为 j 的背包, 则问题转变为 “ 前 i - 1 个物品放入容量为 j - c[i] 的背包 ”,则此时的最大价值就是 “ 前 i - 1 个物品放入 容量为 j - c[i] 的背包 ” 的最大价值 加上 第 i 个物品的最大价值,即
dp[i][j] = dp[i-1][j-c[i]] + w[i];
上述两种情况取大者,便是 所求的 前 i 个物品放入容量为 j 的背包所得到的最大总价值。
3 初始状态
状态在进行转移的时候,(i, j) 不是来自 (i-1, j),就是来自 (i-1, j - c[i]),所以必然
有一个初始状态,而这个初始状态就是 (0, 0),含义是 " 下标为0的物品放入一个背包容量为 0 的背
包",这个状态下的最大价值为 0,即 dp[0][0] = 0 ;
很明显当 j < weight[0]的时候,dp[0][j] 应该是 0,因为背包容量比编号0的物品重量还小。
当j >= weight[0]时,dp[0][j] 应该是value[0],因为背包容量放足够放编号0物品。
4. 无效状态
**首先从dp[i][j]的定义出发,如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0。**当 j = 0 是,无论 i 为何值( i != 0 ) 都没有意义,因为背包容量为 0 ,,总价值必然为0,这种状态称为无效状态,不能进行状态转移 ,我们通过状态初始化进行规避。
5. 状态初始化
对于无效状态,我们可以用一个很小的数来保证它无法成为最优解,error 可以设置为 : -10000;
下面举例说明:
如图所示:
每个格子代表一个状态,(0,0) 代表初始状态,蓝色的格子代表已经求得的状态,灰色的格子代表无效状态,红色的格子代表当前正在进行转移的状态,图中的第 i 行代表了前 i 个物品对应容量的最优值,第 5个物品的容量为 4,价值为 9,则有状态转移如下:
**dp[5][4] = max( dp[5-1][4] , dp[5-1][4-c[5]] + w[5])
= max( dp[5-1][4] , dp[5-1][4-4] + 9)
= max(dp[4][4] , dp[4][0] + 9) **
6.例题剖析
1.分割等和子集
解题思路:
每个数都有两种选择 选 / 不选 —》 0 / 1 背包
1.设计状态
2.状态转移方程:
dp[i][j] = dp[i-1][j] | dp[i-1][j-nums[i]];
// 不选 , 选 其一满足即可成立
3.分析状态
j = 0 -> dp[0][0] = 1; //0个数组合为 0 显然合理,所以为1
j > 0 -> dp[0][j] = 0; //0个数组合为 j 显然不合理,所以为0 (无效状态)
4.整合思路,编写代码:
int dp[200][20001];
**// dp[i][j] -> 从前i个数中能否组合为 j
// Yes 1 No 0**
bool canPartition(int* nums, int numsSize){
int sum =0;
memset(dp,0,sizeof(dp));
dp[0][0]=1; **//不选**
dp[1][nums[0]]=1; **//选**
sum = nums[0];
for(int i=1;i<numsSize;++i)
{
sum += nums[i];
for(int j=0;j<=sum;++j) **//计算有效的状态,j > sum 为无效状态**
{
dp[i][j]=dp[i-1][j];
if(j - nums[i] >= 0) {
dp[i][j] |= dp[i-1][j-nums[i]];
}
}
}
if(sum & 1) return false; **//和为奇数 无法拆分**
return dp[numsSize-1][sum/2];
}
2. 可被3整除的最大和
解题思路:
每个数都有两种选择 选 / 不选 —》 0 / 1 背包
1.设计状态
2.状态转移方程:
dp[i][j] = max(dp[i-1][j], dp[i-1][((j-nums[i]%3) + 3) % 3] + nums[i]);
// 不选 选 (为了得到余数为 j ,所以这样处理)
// 例如: nums[2] = 8;
// dp[2][1] = max( dp[1][j] , dp[1][((1 - 2 %3)+3)%3] + 8);
//要求余数为 1, 第一种不选nums[2], 第二种: 前 i - 1个 余数为 nums[i]%3 的最大和 加上 nums[i]
// 如: 8 和 5 -> %3 = 2 (8+5) % 3 = 1 ;
3.分析状态
memset(dp,-1,sizeof(dp)); //初始化为无效状态
j = 0 -> dp[0][0] = 0; //不选 nums[0],最大和为 0
j = 0 -> dp[0][nums[0]%3] = 0; //选 nums[0],最大和为 nums[0]
状态转移图:灰色代表无效状态,蓝色代表初始状态。
4.整合思路,编写代码:
int dp[40001][3];// dp[i][j]->前i个数和,余数为j,的最大和
int maxSumDivThree(int* nums, int numsSize) {
memset(dp, -1, sizeof(dp));
dp[0][0] = 0; //不选
dp[0][nums[0] % 3] = nums[0]; //选nums[0]
for (int i = 1; i < numsSize; ++i) {
for (int j = 0; j < 3; ++j) {
int prev = ((j - nums[i] % 3) + 3) % 3;
if (dp[i-1][prev] != -1)
dp[i][j] = fmax(dp[i-1][j], dp[i - 1][prev] + nums[i]);
else
dp[i][j]=dp[i-1][j];
}
}
return dp[numsSize - 1][0];
}