代码随想录—01背包

背包问题

0-1 背包

有n种物品,每种物品只有一个,每个物品有自己的价值和重量,然后向背包中增加物品,求获得的最多重量。

回溯算法的暴力求解。?回溯算法

dp数组的定义

二维dp数组

dp[i][j] :[0,i] 的物品任取放到容量为j的背包里

dp[i][j]

1.不放物品i,此时dp[i-1][j]

2.放物品i,此时他的价值dp[i-1][j-weight[i]]+value[i]

dp[i][j] = max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i])

#include<iostream>
using namespace std;

int main() {
    int m, n;
    cin >> m >> n;//m代表种类,n代表小明的行李空间
    int w[5001];
    int v[5001];
    for (int i = 0; i < m; i++)
        cin >> w[i];
    for (int i = 0; i < m; i++)
        cin >> v[i];
    int dp[5001][5001];
    for (int i = 0; i < m; i++) {
        for (int j = 0; j <= n; j++)
            dp[i][j] = 0;
    }
    for (int i = n; i >= 0; i--) {
        if (i < w[0]) {
            break;
        }
        dp[0][i] = v[0];
    }
    for (int i = 1; i < m; i++) {
        for (int j = 1; j <= n; j++) {
            dp[i][j] = dp[i - 1][j];
            if (j - w[i] >= 0)
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);
        }
    }

    cout << dp[m - 1][n];
    return 0;
}
一维dp数组
#include<iostream>
using namespace std;
int dp[5001];
int main() {
    int m, n;
    cin >> m >> n;//m代表种类,n代表小明的行李空间
    //一维dp数组
    int w[5001];
    int v[5001];
    for(int i=0;i<m;i++)
        cin>>w[i];
    for(int i=0;i<m;i++)
        cin>>v[i];
    for(int i=0;i<m;i++){ //遍历物品
        for(int j  = n;j>=0;j--) {//遍历空间
            if(j>=w[i])
                dp[j] = max(dp[j],dp[j-w[i]]+v[i]);            
        }
    }
    cout<<dp[n];
}
回溯法
#include<iostream>

using namespace std;
int m, n;
int w1[5001];
int v1[5001];
int ans = 0;
int visit[5001];
int dfs(int w, int v) { //第i个物品选还是不选
    int maxval = 0;
    if ( w == n) {
        return v;
    }
    for (int i = 0; i < m; i++) {
        if (visit[i] == 0&&w+w1[i]<=n) {
            visit[i] = 1;
            int val1 = dfs(w + w1[i], v + v1[i]);
            int val2 = dfs(w, v);
            int   ans = max(val1,val2);
            visit[i] = 0;
            maxval = max(ans, maxval);
        }
    }
    return maxval;
}

int main() {

    cin >> m >> n;
    for (int i = 0; i < m; i++)
        cin >> w1[i];
    for (int i = 0; i < m; i++)
        cin >> v1[i];
   cout<<dfs(0, 0);
}

例题

最后一块石头的重量

1049. 最后一块石头的重量 II - 力扣(LeetCode)

有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。

每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 xy,且 x <= y。那么粉碎的可能结果如下:

  • 如果 x == y,那么两块石头都会被完全粉碎;
  • 如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x

最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0

示例 1:

输入:stones = [2,7,4,1,8,1]
输出:1
解释:
组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。

示例 2:

输入:stones = [31,26,33,21,40]
输出:5
解题思路

可以转化为0-1背包问题的解题思路,因为这个题干的意思其实是想要找到两堆大小类似的石头来进行碰撞,之后的结果就是最后的一块石头的重量也就是所说的答案。但是如何求两堆总量相似的石头呢?

首先让我们回忆一维背包问题的dp数组的含义:dp[j] : 重量为j的背包所能装的最大的重量。

所以我们先进行求得石头总的重量,之后将重量/2的数值作为最后需要的答案dp[sum/2],之后用sum - 2*dp[sum/2]即求得了答案。

因为在这个题目中dp[sum/2]的含义是重量为dp[sum/2]的背包中能装的最大的重量。也就是说我们尽量向sum/2来凑看能凑的最大重量是多少。

class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        int dp[3101];
        int sum = 0;
        for (int i = 0; i < stones.size(); i++) {
            sum += stones[i];
        }
        int n = sum / 2;
        for (int i = 0; i <= n; i++)
            dp[i] = 0;
        for (int i = 0; i < stones.size(); i++) {//外层是物品
            for (int j = n; j >=0; j--) {
                if (j >= stones[i]) {
                    dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
                   
                }
            }
        }
        return sum - dp[n] * 2;
    }
};

注意一维数组的遍历顺序,应该是先遍历物品,再遍历背包的容量,之后还需要注意的是在内层遍历容量的时候需要倒序进行,否则会造成装入的物品不只是一个而是多个

目标和

494. 目标和 - 力扣(LeetCode)

给你一个非负整数数组 nums 和一个整数 target

向数组中的每个整数前添加 '+''-' ,然后串联起所有整数,可以构造一个 表达式

  • 例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1"

返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

示例 1:

输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3

示例 2:

输入:nums = [1], target = 1
输出:1

提示:

  • 1 <= nums.length <= 20
  • 0 <= nums[i] <= 1000
  • 0 <= sum(nums[i]) <= 1000
  • -1000 <= target <= 1000
解题思路

其实这道题乍一看和最后一块石头的感觉是差不多的,无非就是找到两堆的数字的和互相进行加减之后得到的结果的绝对值和target的值是一样的,但是有个不同点是这个题事实上是求的是数目而不是求最大容量。

同时我们需要注意如果(sum+target)%2!=0则说明根本没有能凑出这个数的。原因如下

l e f t left left : 正数的集合

r i g h t right right : 负数的集合

求的是 : l e f t − r i g h t = t a r g e t left - right = target leftright=target

又因为 s u m = l e f t + r i g h t sum = left + right sum=left+right

所以用 s u m sum sum 来替换 r i g h t right right 得到 l e f t − s u m + l e f t = t a r g e t = > 2 l e f t = r i g h t + t a r g e t left - sum+left = target => 2left = right + target leftsum+left=target=>2left=right+target

所以如果 s u m + t a r g e t sum+target sum+target 不是偶数则一定找不到 l e f t left left 即正数的集合能得到最后的数目。

所以最后可以将问题转换成,装满容量为left (注意这里是装满而不是尽可能的装满)有多少种可能

d p [ j ] : 装满容量为 j 的背包有多少种可能 dp[j] : 装满容量为j的背包有多少种可能 dp[j]:装满容量为j的背包有多少种可能

推导公式为 d p [ j ] + = d p [ j − n u m s [ i ] ] dp[j] += dp[j-nums[i]] dp[j]+=dp[jnums[i]]

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int dp[3101];
        int sum = 0;
        for (int i = 0; i < nums.size(); i++) {
            sum += nums[i];
        }
        if((sum+target)%2!=0||sum+target<0){
            return 0;
        }
        int n = (sum+target)/2;
        dp[0] = 1;
        for (int i = 1; i <= n; i++)
            dp[i] = 0;
        for (int i = 0; i < nums.size(); i++) { // 外层是物品
            for (int j = n; j >= 0; j--) {
                if (j >= nums[i]) {
                    dp[j] +=dp[j-nums[i]];
                }
            }
        }
        return  dp[n];
    }
};

为什么不能使用 d p [ j − n u m s [ i ] ] ∗ d p [ n u m s [ i ] ] ? dp[j-nums[i]]*dp[nums[i]]? dp[jnums[i]]dp[nums[i]]?

因为这样会存在重复,而且这是一种选择问题,一定会出现重复的。!

一和零

给你一个二进制字符串数组 strs 和两个整数 mn

请你找出并返回 strs 的最大子集的长度,该子集中 最多m0n1

如果 x 的所有元素也是 y 的元素,集合 x 是集合 y子集

示例 1:

输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。
其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。

示例 2:

输入:strs = ["10", "0", "1"], m = 1, n = 1
输出:2
解释:最大的子集是 {"0", "1"} ,所以答案是 2 。

提示:

  • 1 <= strs.length <= 600
  • 1 <= strs[i].length <= 100
  • strs[i] 仅由 '0''1' 组成
  • 1 <= m, n <= 100
解题思路

这个题和前面的题一样都是可以使用01背包的问题,通过读题我们可以发现这个题目可以转化成01背包问题。

把数组 s t r s strs strs中的值依次放入背包中找到可以满足0的个数小于m,1的个数小于n的最大的数组长度。

dp[i][j]表示的是满足0的个数为i,1的个数为j的时候最长的数组长度。

dp[i][j] = max(dp[i][j],dp[i-m][j-n]+1),其中m和n依次为strs[i]的0的个数和1的个数。

class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        int zeros[601];
        int ones[601];
        string str;
        int one = 0;
        // 统计每个字符串的i、j
        for (int i = 0; i < strs.size(); i++) {
            str = strs[i];
            one = 0;
            for (int j = 0; j < str.size(); j++) {
                if (str[j] == '1')
                    one++;
            }
            zeros[i] = str.size() - one;
            ones[i] = one;
        }
        int dp[101][101];
        for (int i = 0; i <= m; i++)
            for (int j = 0; j <= n; j++)
                dp[i][j] = 0;
        //i 为0的个数,j为1的个数
        dp[0][0] = 0;
        for (int i = 0; i < strs.size(); i++) {//物品
            for (int j = m; j >= 0; j--) {
                for (int k = n; k >= 0; k--) {
                    if (j >= zeros[i] && k >= ones[i])
                    {
                        // cout << "j: " << j << " k: " << k << " dp[j,k]: " << dp[j][k] << " str[i]: " << strs[i] << " zeros[i] , ones[i]" << zeros[i] << " " << ones[i] << "\n";
                        dp[j][k] = max(dp[j][k], dp[j - zeros[i]][k - ones[i]] + 1);
                       
                    }
                }
            }
        }
        return dp[m][n];
    }
};
  • 19
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值