【01背包问题——附例题讲解】

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];
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值