数据结构笔记--背包问题

目录

1--0-1背包问题

2--分割等和子集

3--最后一块石头的重量II

4--目标和

5--一和零

6--完全背包问题

7--零钱兑换II

8--组合总和IV

9--零钱兑换

10--完全平方数

11--单词拆分


1--0-1背包问题

0-1 背包问题的特征:

        一共有 n 个物品,但每个物品只能选择一次;

二维 dp 解法:

        dp[i][j] 表示背包容量为 j ,可以在 0-i 种物品选取,其最大价值;

        初始化:dp[0][j] = value[0](j >= weight[0]),dp[i][0] = 0;

        状态转移方程:dp[i][j] == std::max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i]),其中 dp[i-1][j] 表示不选择物品 i,dp[i-1][j-weight[i]] + value[i] 表示选择物品 i;

        遍历顺序:两个 for 循环来正序遍历物品 i 和背包容量 j(可以先遍历物品 i,再遍历背包容量 j;也可以先遍历背包容量 j,再遍历物品 i);

一维 dp 解法:

        dp[j] 表示背包容量为 j 时,其最大价值;

        初始化:dp[0] = 0;

        状态转移方程:dp[j] =  std::max(dp[j], dp[j - weight[i]] + value[i]),其中 dp[j - weight[i]] + value[i] 表示选取物品 i 时;

        遍历顺序:两个 for 循环,第一个 for 循环正序遍历物品,第二个 for 循环倒叙(j 从大到小)遍历背包容量;(不能先遍历背包容量,再遍历物品);

2--分割等和子集

主要思路:

        可以转换为 0-1 背包问题,将一半的和作为背包的容量,另一半的和作为物品的价值,同时每个物品的价值和重量相同;

        计算背包的容量:target = sum / 2;

        初始化:dp[0] = 0;

        状态转移方程:dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);j 表示背包的容量,dp[j]表示背包容量为 j 时,背包所装物品的最大价值;

        遍历顺序:经典0-1背包一维遍历顺序,先正序遍历物品,再倒叙遍历背包容量;

        当 dp[target] == target 时,表明恰好可以将数组划分为两个子集,一个子集的和作为背包容量,另一个子集的和作为物品的价值;

#include <iostream>
#include <vector>

class Solution {
public:
    bool canPartition(std::vector<int>& nums) {
		int sum = 0;
		for(int num : nums){
			sum += num;
		}
		if(sum % 2 == 1) return false; // 不能二等分
		int target = sum / 2; // 一半和当作容量,另一半和作为价值
		std::vector<int> dp(target+1, 0);
		
		// 初始化
		dp[0] = 0;
		// 遍历
		for(int i = 0; i < nums.size(); i++){
			for(int j = target; j >= nums[i]; j--){ // j >= nums[i] 确保容量大于重量,这里物品的重量和价值相同,均为nums[i]
				dp[j] = std::max(dp[j], dp[j - nums[i]] + nums[i]);
			}
		}
		if(dp[target] == target) return true; // 一半作为容量,另一半作为价值,因此可以等分
		return false;
    }
};

int main(int argc, char *argv[]) {
	// nums = [1,5,11,5]
	std::vector<int> test = {1, 5, 11, 5};
	Solution S1;
	bool res = S1.canPartition(test);
	if(res) std::cout << "true" << std::endl;
	else std::cout << "false" << std::endl; 
	return 0;
}

3--最后一块石头的重量II

主要思路:

        类似于上题,可以转换为 0-1 背包问题;用一半的石头作为背包容量,另一半石头作为物品;

        背包装尽可能多的石头,最后两者相减相消即可得到最小的重量;

#include <iostream>
#include <vector>

class Solution {
public:
    int lastStoneWeightII(std::vector<int>& stones) {
		int sum = 0;
		for(int num : stones){
			sum += num;
		}
		int vec = sum / 2; // 一半作为容量
		std::vector<int> dp(vec+1, 0);
		// 初始化
		dp[0] = 0;
		// 遍历
		for(int i = 0; i < stones.size(); i++){
			for(int j = vec; j >= stones[i]; j--){
				dp[j] = std::max(dp[j], dp[j - stones[i]] + stones[i]); // 装尽可能多的石头
			}
		}
		return sum - dp[vec] - dp[vec]; // sum - dp[vec] 表示另一半的石头
    }
};

int main(int argc, char *argv[]) {
	// stones = [2,7,4,1,8,1]
	std::vector<int> test = {2, 7, 4, 1, 8, 1};
	Solution S1;
	int res = S1.lastStoneWeightII(test);
	std::cout << res << std::endl;
	return 0;
}

4--目标和

主要思路:
        转化为 0-1 背包问题,一部分数值连同 target 转化为背包容量,剩余一部分数值转化为物品,求解恰好装满背包容量的方法数;

        dp[j] 表示背包容量为 j 时,装满背包的方法数;

        状态转移方程:dp[j] += dp[j - nums[i]],其实质是:当背包已经装了nums[i]时,剩余容量为 j - nums[i],此时装满剩余容量的方法数为 dp[j - nums[i]],遍历不同的 nums[i] 将方法数相加即可;

        是有点难理解。。。

#include <iostream>
#include <vector>

class Solution {
public:
    int findTargetSumWays(std::vector<int>& nums, int target) {
        int sum = 0;
        for(int num : nums) sum += num;
        if(sum < std::abs(target)) return 0; // 数组全部元素相加相减都不能构成target
        if((sum + target) % 2 == 1) return 0; // 不能二等分

        int bagsize = (sum + target) / 2;
        std::vector<int> dp(bagsize + 1, 0);
        dp[0] = 1;
        for(int i = 0; i < nums.size(); i++){ // 遍历物品
            for(int j = bagsize; j >= nums[i]; j--){ // 遍历背包容量
                dp[j] += dp[j - nums[i]];
            }
        }
        return dp[bagsize];
    }
};

int main(int argc, char *argv[]) {
    // nums = [1, 1, 1, 1, 1], target = 3
    std::vector<int> test = {1, 1, 1, 1, 1};
    int target = 3;
    Solution S1;
    int res = S1.findTargetSumWays(test, target);
    std::cout << res << std::endl;
	return 0;
}

5--一和零

主要思路:

        转化为 0-1 背包问题,背包容量为能装 m 个 0 和 n 个 1;

        dp[i][j] 表示0背包容量为i,1背包容量为j时,能装 strs 最大子集的长度;

        初始化 dp[0][0] = 0, dp[i][0] = 0; dp[0][j] = 0;

        状态转移方程:dp[i][j] = max(dp[i][j], dp[i-m1][j-n1] + 1);m1 和 n1 分别表示字符串 0 和 1 的个数,dp[i-m1][j-n1] + 1 表示装上当前字符串后子集的长度;

#include <iostream>
#include <vector>
#include <string>

class Solution {
public:
    int findMaxForm(std::vector<std::string>& strs, int m, int n) {
        std::vector<std::vector<int>> dp(m + 1, std::vector<int>(n + 1, 0));
        for(int i = 0; i < strs.size(); i++){ // 遍历物品
            int m1 = 0, n1 = 0;
            for(char c : strs[i]){ // 统计当前字符串 0 和 1 字符的个数
                if(c == '0') m1++;
                else n1++;
            }
            // 遍历背包容量
            for(int j = m; j >= m1; j--){
                for(int k = n; k >= n1; k--){
                    dp[j][k] = std::max(dp[j][k], dp[j - m1][k - n1] + 1);
                }
            }
        }
        return dp[m][n];
    }
};

int main(int argc, char *argv[]) {
    // strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
    std::vector<std::string> test = {"10", "0001", "111001", "1", "0"};
    int m = 5, n = 3;
    Solution S1;
    int res = S1.findMaxForm(test, m, n);
    std::cout << res << std::endl;
	return 0;
}

6--完全背包问题

完全背包问题的特征:

        一共有 n 个物品,但每个物品可以任意选择多次;

一般解法:

        两层 for 循环从小到大遍历物品和背包容量;

        当先遍历物品,再遍历背包时,结果与物品顺序无关,一般用于组合问题;

        当先遍历背包,再遍历物品时,结果与物品顺序有关,一般用于排列问题;

7--零钱兑换II

主要思路:

        本题所有硬币可以任意取多次,因此是一个完全背包问题;

        对于上例,[2 2 1] 和[1 2 2] 是等价的,因此本题对应于组合问题,应先遍历物品,再遍历背包容量;

#include <iostream>
#include <vector>

class Solution {
public:
    int change(int amount, std::vector<int>& coins){
        std::vector<int> dp(amount + 1, 0); // dp[j] 表示背包容量为 j 时,凑成总金额为 j 的组合数
        dp[0] = 1; // 初始化
        for(int i = 0; i < coins.size(); i++){ // 遍历物品
            for(int j = coins[i]; j <= amount; j++){ // 遍历背包容量
                dp[j] += dp[j - coins[i]];
            }
        }
        return dp[amount];
    }
};

int main(int argc, char *argv[]) {
    // amount = 5, coins = [1, 2, 5]
    std::vector<int> test = {1, 2, 5};
    int amount = 5;
    Solution S1;
    int res = S1.change(amount, test);
    std::cout << res << std::endl;
	return 0;
}

8--组合总和IV

主要思路:

        本题所有整数可以任意取多次,因此是一个完全背包问题;

        对于上例,[2 2 1] 和[1 2 2] 不是等价的,因此本题对应于排列问题,应先遍历背包容量,再遍历物品;

#include <iostream>
#include <vector>

class Solution {
public:
    int combinationSum4(std::vector<int>& nums, int target) {
        std::vector<int> dp(target + 1, 0); // dp[j] 表示target为j时对应的组合个数
        dp[0] = 1; // 初始化
        for(int i = 0; i <= target; i++){
            for(int j = 0; j < nums.size(); j++){
                // C++测试用例有两个数相加超过int的数据,所以需要在if里加上dp[i] >= INT_MAX - dp[i - num]。
                if(i < nums[j] || dp[i] >= INT_MAX - dp[i - nums[j]]) continue; // 跳过背包容量小于物品容量的情况
                dp[i] += dp[i - nums[j]];
            }
        }
        return dp[target];
    }
};

int main(int argc, char *argv[]) {
    // nums = [1, 2, 3], target = 4
    std::vector<int> test = {1, 2, 3};
    int target = 4;
    Solution S1;
    int res = S1.combinationSum4(test, target);
    std::cout << res << std::endl;
	return 0;
}

9--零钱兑换

主要思路:

        本题所有硬币可以任意取多次,因此是一个完全背包问题;

#include <iostream>
#include <vector>
#include <limits.h>

class Solution {
public:
    int coinChange(std::vector<int>& coins, int amount) {
        std::vector<int> dp(amount + 1, INT_MAX); // dp[j] 表示凑成总金额为j时的最少硬币数
        dp[0] = 0;
        for(int i = 0; i < coins.size(); i++){ // 先遍历物品
            for(int j = coins[i]; j <= amount; j++){ // 再遍历背包容量
                if(dp[j - coins[i]] != INT_MAX){ // 如果dp[j - coins[i]]是初始值则跳过
                    // dp[j - coins[i]] != INT_MAX 确保dp[j - coins[i]] 确保组合中的硬币相加 == j - coins[i],即恰好组合成金额
                    dp[j] = std::min(dp[j - coins[i]] + 1, dp[j]);
                }
            }
        }
        if (dp[amount] == INT_MAX) return -1;
        return dp[amount];
    }
};

int main(int argc, char *argv[]) {
    // coins = [1, 2, 5], amount = 11
    std::vector<int> test = {1, 2, 5};
    int amount = 11;
    Solution S1;
    int res = S1.coinChange(test, amount);
    std::cout << res << std::endl;
	return 0;
}

10--完全平方数

主要思路:

        完全平方数可以任意选取多次,因此可以转换为完全背包问题;

#include <iostream>
#include <vector>
#include <limits.h>

class Solution {
public:
    int numSquares(int n) {
        std::vector<int> dp(n + 1, INT_MAX); // dp[j] 表示构成整数 j 使用的最少完全平方数
        dp[0] = 0; // 初始化
        for(int i = 1; i*i <= n; i++){ // 遍历物品
            for(int j = i*i; j <= n; j++){ // 遍历背包
                dp[j] = std::min(dp[j], dp[j - i*i] + 1);
            }
        }
        return dp[n];
    }
};

int main(int argc, char *argv[]) {
    int n = 12;
    Solution S1;
    int res = S1.numSquares(n);
    std::cout << res << std::endl;
	return 0;
}

11--单词拆分

主要思路:

        本题中字符串列表的字符串可以任意选择多次,因此也可以转换为完全背包问题;

#include <iostream>
#include <vector>
#include <string>
#include <unordered_set>

class Solution {
public:
    bool wordBreak(std::string s, std::vector<std::string>& wordDict) {
        std::unordered_set<std::string> hash; // 先将字符串存到哈希表中,便于后面查询
        for(std::string word : wordDict){
            hash.insert(word);
        }
        std::vector<bool> dp(s.length() + 1, false); // dp[j] 表示目标字符串的前j个字符,其能由字符串列表中字符串构成
        dp[0] = true;
        for(int i = 1; i <= s.length(); i++){ // 遍历背包
            for(int j = 0; j <= i; j++){ // 遍历物品
                // dp[j] 可以由列表构成,同时s个字符构成的字符串,其剩余字符串也能由列表构成
                if(dp[j] == true && hash.find(s.substr(j, i-j)) != hash.end()){ // 这里将s.substr(j, i-j)认为是即将加入到背包的物品
                    dp[i] = true;
                    break; // 找到一种成功的组合就 break
                }
            }
        }
        return dp[s.length()];
    }
};

int main(int argc, char *argv[]) {
    // s = "leetcode", wordDict = ["leet", "code"]
    std::string str = "leetcode";
    std::vector<std::string> wordDict = {"leet", "code"};
    Solution S1;
    bool res = S1.wordBreak(str, wordDict);
    if(res) std::cout << "true" << std::endl;
    else std::cout << "false" << std::endl;
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值