【完全背包】LeetCode - 1449. 数位成本和为目标值的最大数字

题目描述

题目链接
在这里插入图片描述
在这里插入图片描述

解法

01背包 vs 完全背包

每件物品要么拿那么不拿(最多拿一次)是 01背包 问题,如果每件物品可以重复拿多次是 完全背包 问题,本题显然是完全背包问题

恰好装满 VS 可以不装满

题目有两种可能,一种是要求背包恰好装满,一种是可以不装满(只要不超过容量就行)。而本题是要求恰好装满的。而这两种情况仅仅影响我们dp数组初始化。

  • 恰好装满。只需要初始化dp[0] 为 0, 其他初始化为负数(无效状态标记)即可。
  • 可以不装满。 只需要全部初始化为 0,即可,

一维 vs 二维

  • 二维表示:dp[i][j] 表示只能选择前i个物品,背包容量为j的情况下,能够得到的最大整数的字符串。
  • 一维表示:dp[j] 表示只能选择前i个物品,背包容量为j的情况下,能够得到的最大整数的字符串。

那么疑问来了,一维中哪来的 i ?

其实二维写法中的 i 在一维表示中用遍历数组的 i 隐晦表示了,这样使得需要开辟的 dp 数组从二维降到了一维,空间复杂度降低了,但思想是一样的,后面我会给出二维写法和一维写法,对比一下就清楚了。

下面的题解都是二维,二维懂了之后一维就轻松了。

初始状态

dp[0][j] 表示选择前 0 个物品,背包容量为 j 的情况下,能够得到的最大整数的字符串。显然只有在 j = 0 时是有效状态,其余均是无效状态。

无效状态可以是 “0” 或者 “#”,只要能区分清楚即可,为了与题目相接,这里设无效状态为 “0”

那么初始化时 dp[0][0] = “” (有效状态,初始为空串),而dp[0][1…target] 初始化为 “0” (无效状态)。其实如果为了方便,可以在定义数组 dp 时直接全都初始化为 “0”,后面再把不是 “0” 的改回去。

状态转移

  • 不选择当前的数位 i

dp[i][j] = dp[i-1][j];

  • 至少选择一个当前的数位 i

dp[i][j] = to_string(i) + dp[i][j-cost[i-1]]; (注意这里的 cost[i-1] 表示第 i 位的成本,因为题目说了 cost 下标是从0开始的)

问题来了,为什么 to_string(i) + dp[i][j-cost[i-1]] 表示至少选择一个当前的数位 i 呢?

因为 dp[i][j - cost[i-1] 表示选择前i个物品, 恰好装进容量为j - cost[i]时能获得的最大值, 这包含了此时背包里第i个物品的个数是0个,1个,2个, 或者3个等等的情况。 方程前面 + to_string(i) 表示放了1个第i件物品进去, 那么此时就包含了背包里第i个物品的个数是1个,2个, 或者3个等等的情况,即至少装了1个第i件物品

要么不装,要么至少装1个,就把所有情况都考虑在内了,此时的状态转移方程才是正确的。综上,dp[i][j] = max_str(dp[i-1][j], to_string(i) + dp[i][j-cost[i-1]]); (max_str 函数需要自己写)

返回值
dp[n][target]

复杂度分析

  • 二维写法
    时间复杂度:O(NV)
    空间复杂度:O(NV)
    N是总个数,V是最大容量

  • 一维写法
    复杂度分析
    时间复杂度:O(NV)
    空间复杂度:O(V)

二维写法

class Solution {
public:
    string max_str(const string &a, const string &b)
    {
        if(a.size() == b.size())
         return a > b ? a : b;
        else
         return a.size() > b.size() ? a : b;
    }
    string largestNumber(vector<int>& cost, int target) {
        int n = cost.size();
        //无效状态用 "0" 表示(只要能区分清楚),比如 dp[0][j] 就都是无效状态,除了dp[0][0]
        vector<vector<string> > dp(n + 1, vector<string>(target + 1, "0"));
        dp[0][0] = "";
        // dp[i][j]表示前i个元素, 恰好构成成本为j时, 构成的最大的整数(整数用字符串表示)
        for(int i = 1; i <= n; i++)
        {
            for(int j = 0; j <= target; j++)
            {
                // 如果这两个要比较字符串都是 "0" 就没有比较的必要了,直接继承 dp[i-1][j] 的结果
                if(j < cost[i - 1] || dp[i][j - cost[i - 1]] == "0")
                 dp[i][j] = dp[i - 1][j];
                else
                {
                    string tmp = to_string(i) + dp[i][j - cost[i - 1]];
                    dp[i][j] = max_str(tmp, dp[i - 1][j]);
                }
            }
        }
        return dp[n][target];
    }
};

一维写法

class Solution {
public:
    string max_str(const string &a, const string &b)
    {
        if(a.size() == b.size())
         return a > b ? a : b;
        else
         return a.size() > b.size() ? a : b;
    }
    //一维写法,二维中的 i 用遍历数组的 i 隐晦表示了
    string largestNumber(vector<int>& cost, int target) {
        int n = cost.size();
        //均初始化为无效状态 "0"
        vector<string> dp(target + 1, "0");
        dp[0] = "";//dp[0][0] 是有效的
        // dp[i][j]表示前i个元素, 恰好构成成本为j时, 构成的最大的整数(整数用字符串表示)
        for(int i = 1; i <= n; i++)
        {
            for(int j = cost[i - 1]; j <= target; j++)
            {
                if(dp[j - cost[i - 1]] != "0")
                {
                    string tmp = to_string(i) + dp[j - cost[i - 1]];
                    dp[j] = max_str(tmp, dp[j]);
                }
            }
        }
        return dp[target];//此时 i 已经等于 n 了,也即此时一维的 dp[target] = 二维的 dp[n][target]
    }
};

发现了吗,一维写法只是把二维中 dp[i][j] 的 [i] 去掉,其余几乎一样

背包问题的套路模板在 这里

### 回答1: 好的,我来用中文回复这个链接:https://leetcode-cn.com/tag/dynamic-programming/ 这个链接是 LeetCode 上关于动态规划的题目集合。动态规划是一种常用的算法思想,可以用来解决很多实际问题,比如最长公共子序列、背包问题、最短路径等等。在 LeetCode 上,动态规划也是一个非常重要的题型,很多题目都需要用到动态规划的思想来解决。 这个链接里包含了很多关于动态规划的题目,按照难度从简单到困难排列。每个题目都有详细的题目描述、输入输出样例、题目解析和代码实现等内容,非常适合想要学习动态规划算法的人来练习和提高自己的能力。 总之,这个链接是一个非常好的学习动态规划算法的资源,建议大家多多利用。 ### 回答2: 动态规划是一种算法思想,通常用于优化具有重叠子问题和最优子结构性质的问题。由于其成熟的数学理论和强大的实用效果,动态规划在计算机科学、数学、经济学、管理学等领域均有重要应用。 在计算机科学领域,动态规划常用于解决最优化问题,如背包问题、图像处理、语音识别、自然语言处理等。同时,在计算机网络和分布式系统中,动态规划也广泛应用于各种优化算法中,如链路优化、路由算法、网络流量控制等。 对于算法领域的程序员而言,动态规划是一种必要的技能和知识点。在LeetCode这样的程序员平台上,题目分类和标签设置十分细致和方便,方便程序员查找并深入学习不同类型的算法。 LeetCode的动态规划标签下的题目涵盖了各种难度级别和场景的问题。从简单的斐波那契数列、迷宫问题到可以用于实际应用的背包问题、最长公共子序列等,难度不断递进且话题丰富,有助于开发人员掌握动态规划的实际应用技能和抽象思维模式。 因此,深入LeetCode动态规划分类下的题目学习和练习,对于程序员的职业发展和技能提升有着重要的意义。 ### 回答3: 动态规划是一种常见的算法思想,它通过将问题拆分成子问题的方式进行求解。在LeetCode中,动态规划标签涵盖了众多经典和优美的算法问题,例如斐波那契数列、矩阵链乘法、背包问题等。 动态规划的核心思想是“记忆化搜索”,即将中间状态保存下来,避免重复计算。通常情况下,我们会使用一张二维表来记录状态转移过程中的中间值,例如动态规划求解斐波那契数列问题时,就可以定义一个二维数组f[i][j],代表第i项斐波那契数列中,第j个元素的值。 在LeetCode中,动态规划标签下有众多难度不同的问题。例如,经典的“爬楼梯”问题,要求我们计算到n级楼梯的方案数。这个问题的解法非常简单,只需要维护一个长度为n的数组,记录到达每一级楼梯的方案数即可。类似的问题还有“零钱兑换”、“乘积最大子数组”、“通配符匹配”等,它们都采用了类似的动态规划思想,通过拆分问题、保存中间状态来求解问题。 需要注意的是,动态规划算法并不是万能的,它虽然可以处理众多经典问题,但在某些场景下并不适用。例如,某些问题的状态转移过程比较复杂,或者状态转移方程中存在多个参数,这些情况下使用动态规划算法可能会变得比较麻烦。此外,动态规划算法也存在一些常见误区,例如错用贪心思想、未考虑边界情况等。 总之,掌握动态规划算法对于LeetCode的学习和解题都非常重要。除了刷题以外,我们还可以通过阅读经典的动态规划书籍,例如《算法竞赛进阶指南》、《算法与数据结构基础》等,来深入理解这种算法思想。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值