Leetcode刷题笔记Day17:动态规划Ⅱ

95 阅读6分钟

0-1背包理论基础

  • 卡码网第46题
  • 0-1背包问题最重要的是搞清楚dp数组的含义,dp[i][j]的值表示从0-i件物品中选取不超过总体积j的物品能获得的最大的价值
  • 掌握了上面的这个原则,我们就不难找出递推关系:
  • dp[i][j]=max(dp[i-1][j], dp[i-1][j-volume[i]]+value[i])
  • 据此可以写出代码:
#include<iostream>
#include<vector>
using namespace std;
#define M 5000
#define N 5001
int dp[M][N]; // M是物品种类,N是背包容量
int main(){
    int m, n;
    cin>>m>>n;
    vector<int> volume(m);
    vector<int> value(m);
    for(int i=0; i<m; i++) cin>>volume[i];
    for(int i=0; i<m; i++) cin>>value[i];
​
    // initialize
    // for(int i=0; i<m; i++) dp[i][0]=0;
    // 这里也别漏了等号
    for(int j=volume[0]; j<=n; j++) dp[0][j]=value[0];
​
    for(int i=1; i<m; i++)  // 这里不取等,因为物品数量是m-1
        for(int j=1; j<=n; j++) //这里是背包容量取等
            if(j<volume[i]) dp[i][j]=dp[i-1][j];
            else dp[i][j]=max(dp[i-1][j], 
                              dp[i-1][j-volume[i]]+value[i]);
    cout<<dp[m-1][n];
}
  • 一维滚动数组法(物品正序,容积倒序):
#include<iostream>
#include<vector>
using namespace std;
#define N 5001
int dp[N]; // M是物品种类,N是背包容量
int main(){
    int m, n;
    cin>>m>>n;
    vector<int> volume(m);
    vector<int> value(m);
    for(int i=0; i<m; i++) cin>>volume[i];
    for(int i=0; i<m; i++) cin>>value[i];
     
    // initialize
    // for(int i=0; i<m; i++) dp[i][0]=0;
    // 这里也别漏了等号
    for(int j=volume[0]; j<=n; j++) dp[j]=value[0];
     
    for(int i=1; i<m; i++)  // 这里不取等,因为物品数量是m-1
        for(int j=n; j>=volume[i]; j--) //这里是背包容量取等
            dp[j]=max(dp[j], dp[j-volume[i]]+value[i]);
    cout<<dp[n];
}

分割等和子集

  • 力扣题目链接
  • 以前我们是通过回溯法找到,但是题目并不要求我们找出所有的方案
  • 确定dp数组的含义,不超过sum/2的最大和,相等即返回true
bool canPartition(vector<int>& nums) {
    int sum=0;
    for(auto num:nums) sum+=num;
    if(sum%2) return false;
    vector<int> dp(sum/2+1, 0);
    for(int j=nums[0]; j<=sum/2; j++) dp[j]=nums[0];
    for(int i=1; i<nums.size(); i++)
        for(int j=sum/2; j>=nums[i]; j--)
            dp[j]=max(dp[j], dp[j-nums[i]]+nums[i]);
    if(dp.back()==sum/2) return true;
    else return false;
}

最后一块石头的重量Ⅱ

  • 力扣题目链接
  • 题目的解题思路是把石头分成差不多的两堆,差值就是最小的可能
int lastStoneWeightII(vector<int>& stones) {
    int sum=accumulate(stones.begin(), stones.end(), 0);
    vector<int> dp(sum/2+1, 0);
    for(int j=stones[0]; j<=sum/2; j++) dp[j]=stones[0];
    for(int i=1; i<stones.size(); i++)
        for(int j=sum/2; j>=stones[i]; j--)
            dp[j]=max(dp[j], dp[j-stones[i]]+stones[i]);
    return (sum-dp[sum/2])-dp[sum/2];
}

目标和

  • 力扣题目链接

  • 既然为target,那么就一定有 left组合 - right组合 = target

    left + right = sum,而sum是固定的。right = sum - left

    公式来了, left - (sum - left) = target 推导出 left = (target + sum)/2

  • 确定递推公式

有哪些来源可以推出dp[j]呢?

只要搞到nums[i],凑成dp[j]就有dp[j - nums[i]] 种方法。

例如:dp[j],j 为5,

  • 已经有一个1(nums[i]) 的话,有 dp[4]种方法 凑成 容量为5的背包。
  • 已经有一个2(nums[i]) 的话,有 dp[3]种方法 凑成 容量为5的背包。
  • 已经有一个3(nums[i]) 的话,有 dp[2]中方法 凑成 容量为5的背包
  • 已经有一个4(nums[i]) 的话,有 dp[1]中方法 凑成 容量为5的背包
  • 已经有一个5 (nums[i])的话,有 dp[0]中方法 凑成 容量为5的背包

所以求组合类问题的公式,都是类似这种:

dp[j] += dp[j - nums[i]]
  • 举例验证初始值:

如果数组[0] ,target = 0,那么 bagSize = (target + sum) / 2 = 0。 dp[0]也应该是1, 也就是说给数组里的元素 0 前面无论放加法还是减法,都是 1 种方法。

int findTargetSumWays(vector<int>& nums, int target) {
    int sum=accumulate(nums.begin(), nums.end(), 0);
    if(abs(target)>sum) return 0;
    if((target+sum)%2) return 0;
    int bagSize=(target+sum)/2;
    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 findMaxForm(vector<string>& strs, int m, int n) {
    vector<vector<int>> dp(m+1, vector<int>(n+1, 0));
    for(auto str:strs){ // 遍历物品
        int oneNum=0, zeroNum=0;
        for(char c:str)
            if(c=='0') zeroNum++;
        else oneNum++;
        // 遍历顺序没有关系
        for(int i=m; i>=zeroNum; i--)
            for(int j=n; j>=oneNum; j--)
                dp[i][j]=max(dp[i][j], dp[i-zeroNum][j-oneNum]+1);
    }
    return dp[m][n];
}

完全背包理论基础

  • 卡码网第52题
  • 01背包内嵌的循环是从大到小遍历,为了保证每个物品仅被添加一次
  • 而完全背包的物品是可以添加多次的,所以要从小到大去遍历
#include<iostream>
using namespace std;
#define N 10000 // 材料种类
#define V 10001 // 行李空间
int dp[V];
int main(){
    int n, v; cin>>n>>v;
    int weight, value;
    for(int i=0; i<n; i++){
        cin>>weight>>value;
        for(int j=weight; j<=v; j++)
            dp[j]=max(dp[j], dp[j-weight]+value);
    }
    cout<<dp[v];
}

零钱兑换Ⅱ

int change(int amount, vector<int>& coins) {
    int result=0;
    vector<int> dp(amount+1, 0);
    dp[0]=1;
    for(auto coin: coins)
        for(int j=coin; j<=amount; j++)
            dp[j]+=dp[j-coin];
    return dp[amount];
}

组合总和Ⅳ

  • 这题看似是组合,实际是排列!
  • 如果求组合数就是外层for循环遍历物品,内层for遍历背包
  • 如果求排列数就是外层for遍历背包,内层for循环遍历物品
  • 还得考虑溢出的情况
int combinationSum4(vector<int>& nums, int target) {
    vector<int> dp(target+1, 0);
    dp[0]=1;
    for(int i=1; i<=target; i++)
        for(auto num: nums)
            if(i>=num && dp[i]<INT_MAX-dp[i-num])
                dp[i]+=dp[i-num];
    return dp[target];
}

爬楼梯(进阶版)

#include<iostream>
#include<vector>
using namespace std;
int main(){
    int n, m; cin>>n>>m;
    vector<int> dp(n+1, 0);
    dp[0]=1;
    for(int i=1; i<=n; i++)
        for(int j=1; j<=m; j++)
            if(i>=j)
                dp[i]+=dp[i-j];
    cout<<dp[n];
}

零钱兑换

  • 力扣题目链接
  • 这道题无所谓什么顺序,但是注意初始化和递推公式
int coinChange(vector<int>& coins, int amount) {
    vector<int> dp(amount+1, INT_MAX);  // 区别
    dp[0]=0;
    for(auto coin: coins)
        for(int j=coin; j<=amount; j++)
            if(dp[j-coin]!=INT_MAX) // 如果dp[j-coin]是初始值则跳过
                dp[j]=min(dp[j], dp[j-coin]+1);
    return dp[amount]==INT_MAX ? -1 : dp[amount];
}

完全平方数

int numSquares(int n) {
    vector<int> dp(n+1, INT_MAX);
    dp[0]=0;
    for(int i=1; i<=n; i++)
        for(int j=1; j*j<=i; j++)
            dp[i]=min(dp[i], dp[i-j*j]+1);
    return dp[n];
}

参考资料

[1] 代码随想录

[2] Leetcode题解