当前位置: 首页 > article >正文

Day39 | 动态规划 :完全背包应用 零钱兑换零钱兑换II

Day39 | 动态规划 :完全背包应用 零钱兑换&&零钱兑换II

动态规划应该如何学习?-CSDN博客

01背包模板 | 学习总结-CSDN博客

完全背包模板总结-CSDN博客

难点:

代码都不难写,如何想到完全背包并把具体问题抽象为完全背包才是关键

文章目录

  • Day39 | 动态规划 :完全背包应用 零钱兑换&&零钱兑换II
    • 322.零钱兑换
      • 思路分析:
      • 1.回溯法
      • 2.记忆化搜索
      • 3.动态规划
      • 4.滚动数组
    • 518.零钱对换II
      • 思路分析:
      • 1.回溯暴力枚举
      • 2.记忆化搜索
      • 3. 1:1翻译为动态规划
      • 4.滚动数组优化
    • 遍历顺序先遍历背包容量后遍历物品和先遍历物品后背包容量
      • 先遍历物品后遍历背包容量,求的是可以凑成target的组合
      • 先背包容量后物品,求的是可以凑成target的排列

322.零钱兑换

[322. 零钱兑换 - 力扣(LeetCode)](https://leetcode.cn/problems/partition-equal-subset-sum/description/)

思路分析:

完全背包模板总结-CSDN博客

322. 零钱兑换 - 力扣(LeetCode)

套用模板时候的注意点

1.这道题的金币可以重复选,所以是完全背包不是01背包

2.这道题是返回最少的金币数量,所以dfs/dp的含义就是前i种金币可以凑成金额为c的面额的最少金币数量是多少

3.由于求的是金币数量,所以面额amount就是背包的容量,物品的重量就是自己的面额,每个物品的价值都为1,因为dfs和dp的含义是最少的金币的数量是多少

4.求的是最少,所以要把max换成min,初始化的时候要初始化为INT_MAX/2而不是0(除以2是因为返回的INT_MAX+1后会溢出,导致报错)

1.回溯法

1.参数和返回值

i是物品编号,表示从前i个物品里面选

c是容量

coins是w[i]

v[i]的值全都为1这里就不写了

int dfs(int i,int c,vector<int>& coins)

2.终止条件

i小于0说明到了树形结构的最下面了

如果同时容量c==0,那说明正好可以凑够,我们就找到了一个合法的方案,返回0(不返回1的原因是我们dfs的含义是最少金币的数量,而不是能凑够amount的方案数量)

如果容量不是0就凑不够,返回INT_MAX/2

如果容量不够当前的硬币,那就递归下一个物品去了,最后返回的就是在前i-1种金币能凑够amount的最小金额数

		if(i<0)
            if(c==0)
                return 0;
            else
                return INT_MAX/2;
        if(c<coins[i])
            return dfs(i-1,c,coins);

3.本层逻辑

递归公式max换成min

 return min(dfs(i-1,c,coins),dfs(i,c-coins[i],coins)+1);

完整代码:

当然是超时的

class Solution {
public:
    int dfs(int i,int c,vector<int>& coins)
    {
        if(i<0)
            if(c==0)
                return 0;
            else
                return INT_MAX/2;
        if(c<coins[i])
            return dfs(i-1,c,coins);
        return min(dfs(i-1,c,coins),dfs(i,c-coins[i],coins)+1);
    }
    int coinChange(vector<int>& coins, int amount) {
        int res=dfs(coins.size()-1,amount,coins);
        if(res<INT_MAX/2)
            return res;
        else
            return -1;
    }
};

2.记忆化搜索

就是还是全都初始化为-1,每次返回前给dp赋值,碰到不是-1的那就是算过的,那就直接返回计算过的结果,不需要再次递归了

完整代码:

class Solution {
public:
    int dfs(int i,int c,vector<int>& coins,vector<vector<int>>& dp)
    {
        if(i<0)
            if(c==0)
                return 0;
            else
                return INT_MAX/2;
        if(dp[i][c]!=-1)
            return dp[i][c];
        if(c<coins[i])
            return dp[i][c]=dfs(i-1,c,coins,dp);
        return dp[i][c]=min(dfs(i-1,c,coins,dp),dfs(i,c-coins[i],coins,dp)+1);
    }
    int coinChange(vector<int>& coins, int amount) {
        vector<vector<int>> dp(coins.size(),vector<int>(amount+1,-1));
        int res=dfs(coins.size()-1,amount,coins,dp);
        if(res<INT_MAX/2)
            return res;
        else
            return -1;
    }
};

3.动态规划

笔者选择的是dp数组的物品编号从1开始

递归的边界条件是

i<0的时候c==0是返回0的。而我们这里的dp数组编号从1开始,那就是i<1且c==0的时候是等于0的

所以dp[0][0]的初始值就为0,其他的都是INT_MAX/2

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        vector<vector<int>> dp(coins.size()+1,vector<int>(amount+1,INT_MAX/2));
        dp[0][0]=0;
        for(int i=1;i<=coins.size();i++)
            for(int j=0;j<=amount;j++)
                if(j<coins[i-1])
                    dp[i][j]=dp[i-1][j];
                else
                    dp[i][j]=min(dp[i-1][j],dp[i][j-coins[i-1]]+1);
        if(dp[coins.size()][amount]<INT_MAX/2)
            return dp[coins.size()][amount];
        else
            return -1;
    }
};

4.滚动数组

把第一维度全都删掉即可

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        vector<int> dp(amount+1,INT_MAX/2);
        dp[0]=0;
        for(int i=1;i<=coins.size();i++)
            for(int j=coins[i-1];j<=amount;j++)
                dp[j]=min(dp[j],dp[j-coins[i-1]]+1);
        if(dp[amount]<INT_MAX/2)
            return dp[amount];
        else
            return -1;
    }
};

518.零钱对换II

[518. 零钱兑换 II - 力扣(LeetCode)](https://leetcode.cn/problems/ones-and-zeroes/description/)

思路分析:

这回求的是方案数量很明显,求方案数量那dfs的的含义就是在前i种金币里面有多少种方法可以恰好凑够当前的容量c,递推公式和01背包变形类似

就把max换成+就可以了,即两种情况的方案数加起来就是当前i,c的总方案数量

1.回溯暴力枚举

就挑重点说吧

终止条件如果c==0说明恰好可以凑够,那说明我们找到了合法的方案,由于我们dfs的含义是返回合法的方案数量,所以我们每找到一个合法方案返回的就是1,否则就是0.

完整代码

当然是超时的

class Solution {
public:
    int dfs(int i,int c,vector<int>& coins)
    {
        if(i<0)
            if(c==0)
                return 1;
            else
                return 0;
        if(c<coins[i])
            return dfs(i-1,c,coins);
        return dfs(i-1,c,coins)+dfs(i,c-coins[i],coins);
    }
    int change(int amount, vector<int>& coins) {
        return dfs(coins.size()-1,amount,coins);
    }
};

2.记忆化搜索

还是老样子

初始化dp为-1,如果不是-1说明计算过了直接返回

并且在返回之前给dp赋值

class Solution {
public:
    int dfs(int i,int c,vector<int>& coins,vector<vector<int>>& dp)
    {
        if(i<0)
            if(c==0)
                return 1;
            else
                return 0;
        if(dp[i][c]!=-1)
            return dp[i][c];
        if(c<coins[i])
            return dp[i][c]=dfs(i-1,c,coins,dp);
        return dp[i][c]=dfs(i-1,c,coins,dp)+dfs(i,c-coins[i],coins,dp);
    }
    int change(int amount, vector<int>& coins) {
        vector<vector<int>> dp(coins.size(),vector<int>(amount+1,-1));
        return dfs(coins.size()-1,amount,coins,dp);
    }
};

3. 1:1翻译为动态规划

递归终止条件就是动态规划的初始化

i<0且c==0的时候就是返回1,而我们这里dp数组中的物品编号是从1开始的。所以,dp[0][0]就等于1。

(dp数组物品编号从0开始的太麻烦了就不写了),注意一点事dp数组中物品编号是从1开始的,而coins数组却是从0开始的

所以比较和加减coins[i]的地方都是coins[i-1]

因为dp数组中的第i个元素的重量在coins 的i-1的位置存储着

注意定义为unsigned,不然有个测试案例过不去

完整代码:

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<vector<unsigned>> dp(coins.size()+1,vector<unsigned>(amount+1,0));
        dp[0][0]=1;
        for(int i=1;i<=coins.size();i++)
            for(int j=0;j<=amount;j++)
                if(j<coins[i-1])
                    dp[i][j]=dp[i-1][j];
                else
                    dp[i][j]=dp[i-1][j]+dp[i][j-coins[i-1]];
        return dp[coins.size()][amount];
    }
};

4.滚动数组优化

就是删掉第一维度就完事

从1开始

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<unsigned> dp(amount+1,0);
        dp[0]=1;
        for(int i=1;i<=coins.size();i++)
            for(int j=coins[i-1];j<=amount;j++)
                    dp[j]=dp[j]+dp[j-coins[i-1]];
        return dp[amount];
    }
};

从0开始

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<unsigned> dp(amount+1,0);
        dp[0]=1;
        for(int i=0;i<coins.size();i++)
            for(int j=coins[i];j<=amount;j++)
                    dp[j]=dp[j]+dp[j-coins[i]];
        return dp[amount];
    }
};

遍历顺序先遍历背包容量后遍历物品和先遍历物品后背包容量

先遍历物品后遍历背包容量,求的是可以凑成target的组合

经典题目就是兑换零钱II

先背包容量后物品,求的是可以凑成target的排列

经典题目:
Day40 | 动态规划 :完全背包应用 组合总和IV(类比爬楼梯)-CSDN博客


http://www.kler.cn/a/389347.html

相关文章:

  • 使用 start-local 脚本在本地运行 Elasticsearch
  • ML 系列: 第 24 节 — 离散概率分布(泊松分布)
  • 【金融风控】特征评估与筛选详解
  • Unity3D 包体裁剪与优化详解
  • 鸿蒙进阶篇-属性动画-animateTo转场动画
  • 【JAVA】Java基础—面向对象编程:封装—保护类的内部数据
  • 用Puppeteer点击与数据爬取:实现动态网页交互
  • 自动驾驶革命:从特斯拉到百度,谁将主宰未来交通?
  • 沈阳乐晟睿浩科技有限公司抖音小店展望未来
  • 【机器学习】机器学习中用到的高等数学知识
  • ZooKeeper在kafka集群中有何作用
  • 冗余连接 代随写法的C#版本
  • 腾讯混元宣布大语言模型和3D模型正式开源
  • Java灵魂拷问13个为什么,你都会哪些?
  • 多用户商城系统的功能及设计和开发
  • Linux 系统结构
  • 什么是电机绕组热保护,它们如何限制浪涌电流?
  • SpringBoot基础系列学习(四):Thymeleaf模板
  • Django中间件应该怎么使用
  • 把握鸿蒙生态崛起的机遇:开发者视角的探讨
  • Linux 共享内存
  • 戴尔R930服务器增加 Intel X710-DA2双万兆光口含模块
  • 服务器被病毒入侵如何彻底清除?
  • Intern大模型训练营(四):使用Hugging Face下载模型
  • RoseTTAFold PositionalEncoding类解读
  • (C++11)委托构造函数--C++