代码随想录算法训练营第三十二天 | 完全背包、518. 零钱兑换 II、377. 组合总和 Ⅳ、卡码网57. 爬楼梯

完全背包理论

有N件物品和⼀个最多能背重量为W的背包。第 i 件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放⼊背包多次),求解将哪些物品装入背包里物品价值总和最大。
完全背包和01背包问题唯一不同的地方就是,每种物品有无限件。
eg:
背包最大重量为4。
物品为:
在这里插入图片描述
每件商品都有无限个!
问背包能背的物品最大价值是多少?

  1. 确定dp数组的含义
    dp[i][j] 表示背包容量为j的背包能背[0~i]中物品的最大价值。
  2. 确定递推公式。
    不装当前物品 i : dp[i][j] = dp[i - 1][j]
    装当前物品 i : dp[i][i] = dp[i][j - weight[i]] + value[i]
    因为每件商品有无限个,所以不是dp[i - 1][i - weight[i]], 而是dp[i][j - weight[i]], 之前这个物品装入过,但因为有空间,还可以再装入。
    dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i])
  3. 初始化
    dp[i][0]: 背包容量为0,什么物品都装不下,所以为0。
    因为dp[i][j] 由上方和左方推倒而来,所以dp[0][j] 需要初始化。
    只要容量能装下物品0,就可劲装:
    j > weight[0]: dp[0][j] = dp[0][j - weight[0]] + value[0];
  4. 确定遍历顺序
    完全背包的物品是可以添加多次的,所以要从小到大去遍历
    // 先遍历物品,再遍历背包
    for(int i = 0; i < weight.size(); i++)    // 遍历物品
    { 
    	for(int j = weight[i]; j <= bagWeight ; j++)     // 遍历背包容量
    	{
    		dp[i][j] = max(dp[i-1][j], dp[i][j - weight[i]] + value[i]);
    	}
    }
    
    // 先遍历背包,再遍历物品
    for(int j = 0; j <= bagWeight; j++)     // 遍历背包容量
    { 
    	for(int i = 0; i < weight.size(); i++)    // 遍历物品
    	{
    		if (j - weight[i] >= 0) 
    			dp[i][j] = max(dp[i-1][j], dp[i][j - weight[i]]+ value[i]);
    	}
    }
    
  5. 举例推导dp数组
    在这里插入图片描述

纯完全背包的面试题:要求先用二维dp数组实现,然后再用一维dp数组实现,最后在问,两个for循环的先后是否可以颠倒?为什么?

如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。 377.组合总和IV

卡码网52. 携带研究材料

题目链接:卡码网52. 携带研究材料
在这里插入图片描述

#include <iostream>
#include <vector>
using namespace std;

int main() {
    int item, totalWeight;
    cin >> item >> totalWeight;
    vector<int> weight(item, 0);
    vector<int> value(item, 0);
    for (int i = 0; i < item; ++i) {
        cin >> weight[i];
        cin >> value[i];
    }
    
    // dp[i][j] [0~i]类物品中装入容量为j的行李中的最大价值
    vector<vector<int>> dp(item, vector<int>(totalWeight+1, 0));
    // 初始化第一行
    for (int j = weight[0]; j <= totalWeight; ++j) {
        dp[0][j] = dp[0][j - weight[0]] + value[0];
    }
    for (int i = 1; i < item; ++i) {       // 物品
        for(int j = 0; j <= totalWeight; ++j) {   // 容量
            if (j < weight[i]) dp[i][j] = dp[i-1][j];
            else {
                dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]);
            }
        }
    }
    cout << dp[item-1][totalWeight] << endl;
    return 0;
}

518.零钱兑换II

题目链接:518.零钱兑换II
这道题很契合完全背包问题。
coins数组相当于物品,amount相当于背包。
只不过这里的dp[i][j] 不表示价值,而是表示凑成这个amount有多少种方式。多少和494. 目标和有点相似。目标和是01背包问题,这道题是完全背包问题。

  1. 确定dp[i][j]的含义
    dp[i][j] 表示 coins中下标为[0~i]的数凑成金额 j 的组合数。
  2. 确定递推公式
    不包含当前下标为 i 的coin,dp[i][j] = dp[i - 1][j]
    包含当前下标为 i 的coin, dp[i][j] = dp[i][j - coins[i]]
    dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i]]
  3. 初始化
    凑成金额为0的组合数相当于什么都不选,算一种方式,即dp[i][0] = 1;
    当 j % coins[0] == 0, dp[0][j] = 1;
  4. 遍历顺序
    从小到大遍历
  5. 举例推到dp数组
    在这里插入图片描述
class Solution {
public:
    int change(int amount, vector<int>& coins) {
        // dp[i][j] 表示 coins中下标为[0~i]的数凑成金额 j 的组合数。
        int n = coins.size();
        vector<vector<uint64_t>> dp(n, vector<uint64_t>(amount + 1, 0));    // 小面值硬币组合出大金额,组合方式爆炸式增长,可能超出64 位整数上限。
        // 初始化
        for (int j = 0; j <= amount; ++j) {
            if (j % coins[0] == 0) dp[0][j] = 1;
        }

        for (int i = 1; i < n; ++i) {
            dp[i][0] = 1;
            for (int j = 1; j <= amount; ++j) {
                if (j < coins[i]) dp[i][j] = dp[i-1][j];
                else dp[i][j] = dp[i-1][j] + dp[i][j - coins[i]];
            }
        }
        return dp[n - 1][amount];
    }
};

377. 组合总和 Ⅳ

题目链接:377. 组合总和 Ⅳ
这道题和518.零钱兑换II相似,不同之处在于这道题把顺序不同的序列视为不同的组合。

  1. 确定dp[i][j]的含义
    dp[i][j] 表示 使用下标为 0~i 数字凑成总和为 j 的排列数量。
  2. 确定递推公式
    不包含当前下标为 i 的num,dp[i][j] = dp[i - 1][j]
    包含当前下标为 i 的coin, dp[i][j] = dp[n][j - nums[i]]
    dp[i][j] = dp[i - 1][j] + dp[n][j - nums[i]]
    为什么不是 dp[i][j - nums[i]]?
    当使用 nums[i] 时,剩余和 j - nums[i] 的凑法必须允许再次使用所有数字,才能体现排列。
    其中 n 是数组总长度,dp[n][…] 表示所有数字都可用的状态。
  3. 初始化
    凑成为0的组合数相当于什么都不选,算一种方式,即dp[i][0] = 1;
  4. 遍历顺序
    从小到大遍历
  5. 举例推到dp数组
    在这里插入图片描述
class Solution {
public:
    int combinationSum4(vector<int>& nums, int target) {
        // dp[i][j] 表示 nums中下标为[0~i]的数组合成和为 target 的组合排列数。
        int n = nums.size();
        vector<vector<uint64_t>> dp(n+1, vector<uint64_t>(target + 1, 0));   
        // 初始化
        for (int i = 0; i <= n; ++i) {
           dp[i][0] = 1;      // 凑成 0 都有 1 种方法
        }
        for(int j = 1; j <= target; ++j) {
            for (int i = 1; i <= n; ++i) {
                if (j < nums[i-1]) dp[i][j] = dp[i-1][j];
                else dp[i][j] = dp[i-1][j] + dp[n][j - nums[i-1]];
            }
        }
        return dp[n][target];
    }
};

卡码网57. 爬楼梯

题目链接:卡码网57. 爬楼梯
在这里插入图片描述
思路同上题!

#include <iostream>
#include <vector>
using namespace std;

int main() {
    int n, m;
    cin >> n >> m;
    // n 相当于 背包容量, 1 ~ m 相当于物品,可以重复装
    vector<vector<int>> dp(m+1, vector<int>(n+1, 0));
    // 初始化
    for (int i = 0; i <= m; ++i) {
        dp[i][0] = 1;
    } 
    for (int j = 1; j <= n; ++j) {
        for(int i = 1; i <= m; ++i) {
            if (j < i) dp[i][j] = dp[i-1][j];
            else dp[i][j] = dp[i-1][j] + dp[m][j-i];
        }
    } 
    cout << dp[m][n] << endl;  
    return 0;
}
第二十二算法训练营主要涵盖了Leetcode题目中的三道题目,分别是Leetcode 28 "Find the Index of the First Occurrence in a String",Leetcode 977 "有序数组的平方",和Leetcode 209 "长度最小的子数组"。 首先是Leetcode 28题,题目要求在给定的字符串中找到第一个出现的字符的索引。思路是使用双指针来遍历字符串,一个指向字符串的开头,另一个指向字符串的结尾。通过比较两个指针所指向的字符是否相等来判断是否找到了第一个出现的字符。具体实现的代码如下: ```python def findIndex(self, s: str) -> int: left = 0 right = len(s) - 1 while left <= right: if s[left == s[right]: return left left += 1 right -= 1 return -1 ``` 接下来是Leetcode 977题,题目要求对给定的有序数组中的元素进行平方,并按照非递减的顺序返回结果。这里由于数组已经是有序的,所以可以使用双指针的方法来解决问题。一个指针指向数组的开头,另一个指针指向数组的末尾。通过比较两个指针所指向的元素的绝对值的大小来确定哪个元素的平方应该放在结果数组的末尾。具体实现的代码如下: ```python def sortedSquares(self, nums: List[int]) -> List[int]: left = 0 right = len(nums) - 1 ans = [] while left <= right: if abs(nums[left]) >= abs(nums[right]): ans.append(nums[left ** 2) left += 1 else: ans.append(nums[right ** 2) right -= 1 return ans[::-1] ``` 最后是Leetcode 209题,题目要求在给定的数组中找到长度最小的子数组,使得子数组的和大于等于给定的目标值。这里可以使用滑动窗口的方法来解决问题。使用两个指针来表示滑动窗口的左边界和右边界,通过移动指针来调整滑动窗口的大小,使得滑动窗口中的元素的和满足题目要求。具体实现的代码如下: ```python def minSubArrayLen(self, target: int, nums: List[int]) -> int: left = 0 right = 0 ans = float('inf') total = 0 while right < len(nums): total += nums[right] while total >= target: ans = min(ans, right - left + 1) total -= nums[left] left += 1 right += 1 return ans if ans != float('inf') else 0 ``` 以上就是第二十二算法训练营的内容。通过这些题目的练习,可以提升对双指针和滑动窗口等算法的理解和应用能力。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值