掘金刷题之路 Day 1 | 豆包MarsCode AI刷题

287 阅读8分钟

徒步旅行中的补给问题

考察: 动态规划

在拿到题目时,我的第一反应是使用模拟遍历的方式来解决,但在思考过程中并没有找到合适的方法。 如果没有完全读懂题目,可以借助 Marscode AI 小助手来帮助我们总结题意和分析样例,这样我们能更快速地理解题目。 只需要将需求输入 AI,便能轻松获得所需的解答支持。如下图所示。

如果有不懂的问题也可以及时通过ai进行提问

如果还是没有什么想法,那我们也可以借助ai给出部分代码提示

功能亮点 经过这些操作后大大减少了我们解答题目的难度,可以帮我们快速掌握解题步骤!!!

这个题目描述了一个典型的最优化问题,我们需要帮助小R在徒步旅行中每天保持有足够的食物,同时尽量减少总花费。

问题分析

  • 总路程需要 N,也就是说小R需要有足够的食物来度过 N 天。
  • 每天消耗 1 份食物,如果食物不足,小R就无法继续旅行。
  • 每天会经过一个补给站,每个补给站的食物价格不同,小R可以选择在这些补给站购买食物。
  • 小R最多只能同时携带 K 份食物,也就是说背包容量是 K

目标是以最小的花费完成旅程,即在所有补给站的选择中找到一种方式,使得小R每天都有足够的食物且花费最少。

动态规划的思路

我们可以用动态规划来解决这个问题,将问题拆解为多个小阶段,通过递推关系找到最优解。定义状态 dp[i][bag] 来表示第 i 天结束时,小R手里有 bag 份食物时的最小花费。

状态定义

  • dp[i][bag]:表示在第 i 天结束时,小R背包中有 bag 份食物的最小花费。
    • i 的范围是从 0N,表示第 i 天。
    • bag 的范围是从 0K,表示当前携带的食物数量。

状态转移

在第 i 天,小R可以做如下决策:

  1. 不购买食物:如果小R有足够的食物(即 bag > 0),则可以不购买。这样第 i 天结束时,小R会少掉一天的食物,即 bag - 1。转移方程为: image.png 表示不买食物的情况下,从前一天的 bag 状态转移到 bag - 1

  2. 购买食物:小R可以购买若干份食物,但背包的总量不能超过 K。假设在第 i 天,小R决定购买 j 份食物,其中 1<=j<=k-bag,那么:

    • 小R在购买 j 份食物之后,新的食物数量为 bag + j - 1(减去 1 是因为当天需要消耗一份食物)。
    • 购买 j 份食物的花费为 j * price[i],其中 price[i] 是第 i 天补给站的食物价格。
    • 状态转移方程为:

image.png

边界条件

  • 初始状态:dp[0][0] = 0,表示在第 0 天开始时,小R不需要任何花费且没有食物。
  • 其他状态初始化为一个很大的值,表示这些状态不可达。

最终目标

在第 N 天结束时,我们需要找到所有可能的携带食物数量(0K)中的最小花费,即:

image.png

AC代码

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

int solution(int n, int k, vector<int> price) {
    vector<vector<int>> dp(n + 1, vector<int>(k + 1, 100000));
    dp[0][0] = 0;

    for (int i = 0; i < n; ++i) {
        for (int bag = 0; bag <= k; ++bag) {
            if (dp[i][bag] == 100000) continue;

            // 不购买食物的情况
            if (bag > 0) {
                dp[i + 1][bag - 1] = min(dp[i + 1][bag - 1], dp[i][bag]);
            }

            // 购买食物的情况
            for (int j = 1; j <= k - bag; ++j) {
                int new_bag = bag + j - 1;
                dp[i + 1][new_bag] = min(dp[i + 1][new_bag], dp[i][bag] + j * price[i]);
            }
        }
    }

    int result = 100000;
    for (int bag = 0; bag <= k; ++bag) {
        result = min(result, dp[n][bag]);
    }

    return result;
}

int main() {
    // 测试用例
    cout << (solution(5, 2, {1, 2, 3, 3, 2}) == 9) << endl;
    return 0;
}

复杂度分析

  • 时间复杂度O(N * K^2),因为我们有两层循环遍历天数和当前携带的食物数量,还需要遍历购买的食物数量 j
  • 空间复杂度O(N * K),因为我们使用了二维的 dp 数组来存储每一天每个可能携带食物数量的最小花费。

扩展关于背包问题识别

识别一个问题是否可以使用动态规划的方法需要一定的经验积累和对问题结构的理解。对于该题目来说,能够识别出可以使用动态规划的原因,主要是通过对题目的特性、约束条件和解决路径的分析。

1. 理解问题的特性

首先,我们要理解题目给出的条件和目标:

  1. 多阶段决策:在给定的 n 个商品中,我们需要决定每一个商品是买还是不买,这个决策是逐步进行的。每个商品的选择都会影响最终的成本。
  2. 累积约束:存在一个累积约束,即如果不购买商品,我们会累积一定的未购买状态,而这个累积状态是有限的(最多为 k)。这意味着我们的决策不仅要考虑当前商品的选择,还要考虑到之前累积的状态,这是一种典型的“状态依赖性”问题。

2. 识别最优子结构

动态规划的核心特点之一是最优子结构,即问题的最优解可以通过其子问题的最优解来推导。对本题而言:

  • 当前决策:对第 i 个商品,可以选择买一定数量或不买。
  • 后续决策:买或不买第 i 个商品的决策会影响接下来商品的选择以及最终的总成本。

这样,我们可以发现,针对每个商品的选择会产生一个新的状态,而最终的最优解依赖于这些状态的变化。通过构造每个状态之间的依赖关系,我们能够找到最优解。

3. 状态和状态转移的定义

动态规划通常通过状态定义和状态转移来解决问题。对于本题:

  1. 状态定义

    • 定义 dp[i][bag] 为前 i 个商品处理完之后,当前累积的“未购买状态”为 bag 的最小花费。
  2. 状态转移

    • 根据当前商品的选择(买或不买)来更新 dp[i][bag]dp[i+1][bag'] 的状态。
    • 如果买商品,累计的状态会增加,花费也会增加。
    • 如果不买商品,累积的状态会减少。

通过状态的定义和状态的转移方式,我们可以逐步递推出所有可能的情况,然后通过迭代更新来找到最终的最优解。

4. 重叠子问题

动态规划的另一个核心特点是重叠子问题。本题中,很多状态在递归或者遍历的过程中会被多次访问,比如对于同一个商品索引 i 和累积状态 bag,可能会有多条路径到达同一个状态。

  • 使用动态规划可以避免对相同状态进行重复计算,从而提高效率。
  • 通过引入 memodp 数组,可以存储已经计算过的状态,这样对于重复访问的状态,可以直接使用之前的计算结果,避免重复计算。

5. 决策过程分析

动态规划的适用场景通常是这样的:我们需要做一系列决策,每一个决策都会影响未来的结果,而每一步的决策是基于之前的状态来做出最优选择的。本题中的决策过程是逐个商品地进行购买与否的决策,并且每次决策都会影响累积的未购买状态以及成本,这使得动态规划成为一种有效的解决方案。

6. 动态规划的步骤推导

为了进一步验证是否能使用动态规划来解决本题,我们可以按照动态规划的思路一步步推导,看看是否能够成功解决:

  1. 状态初始化

    • 初始状态 dp[0][0] = 0,表示在没有处理任何商品时,花费为 0,且累积的未购买状态为 0。
    • 其他状态初始为一个极大值,表示无法达到。
  2. 状态转移方程

    • 如果选择买 j 份第 i 个商品,那么新的累积状态 new_bag = bag + j - 1,总花费增加为 dp[i][bag] + j * data[i]
    • 如果选择不买,则减少累积状态。

7. 总之

通过上述分析可以看出,该题符合动态规划的三大特征:

  1. 最优子结构:最优解可以由子问题的最优解推导出来。
  2. 重叠子问题:在解决问题的过程中存在大量的重复计算。
  3. 状态转移关系:可以通过当前的状态来推导出下一个状态。