【动态规划算法】(完全背包问题从状态定义到空间优化)


🔥承渊政道: 个人主页
❄️个人专栏: 《C语言基础语法知识》 《数据结构与算法》 《C++知识内容》 《Linux系统知识》 《算法刷题指南》 《测评文章活动推广》 《大模型语言路线学习》
✨逆境不吐心中苦,顺境不忘来时路!✨ 🎬 博主简介:

在动态规划的学习过程中,背包问题 几乎是绕不开的一座"大山".它不仅是动态规划中的经典模型,也是很多算法题的底层抽象来源.无论是求最大价值、方案数量,还是判断是否能够恰好装满,很多问题本质上都可以转化为背包模型来解决.而在背包问题的体系中,完全背包问题 是非常重要的一类.相比01背包中"每个物品只能选择一次"的限制,完全背包允许每个物品被选择多次,甚至可以无限次选择.正是这个看似简单的变化,使得它在状态转移、遍历顺序以及空间优化上,都与01背包有着明显区别.本文将围绕 "完全背包问题从状态定义到空间优化" 这一主题展开,按照由浅入深的方式,逐步讲清楚完全背包的核心思想.我们会先从题目模型出发,理解什么样的问题可以抽象成完全背包;然后分析二维 DP 的状态定义和状态转移方程;接着进一步推导如何将二维 DP 优化为一维 DP;最后总结完全背包常见题型、代码模板以及容易出错的细节.学习完全背包,最重要的不是单纯记住代码,而是理解每一个状态背后的含义.只有明白 dp[i][j] 表示什么,知道当前状态是如何由之前的状态推导而来,才能真正掌握这类问题的解题方法.当题目稍微变形,比如求最大价值、求组合数、求排列数、判断可行性时,也能快速判断应该如何定义状态、如何初始化、如何遍历.希望通过这篇文章,你能够真正理解完全背包的本质,掌握从二维 DP 到一维 DP 的优化思路,并在之后遇到类似问题时,不再只是套公式,而是能够根据题意独立分析出正确的动态规划解法.废话不多说,下面跟着小编的节奏🎵一起去疯狂的学习吧!

目录

1.完全背包问题背景介绍

在动态规划算法中,背包问题是一类非常经典的问题模型.它通常描述的是:给定一个容量有限的背包,以及若干种物品,每种物品都有对应的体积、重量或花费,同时也可能具有一定的价值,要求在不超过背包容量的前提下,选择合适的物品,使得最终收益最大、方案数最多,或者判断是否能够刚好装满背包.

在最基础的 01 背包问题 中,每个物品只能选择一次.也就是说,对于每件物品,我们只有两种选择: 或者 不选 .而 完全背包问题 则是在此基础上进行扩展:每种物品可以被选择无限次.也就是说,只要背包容量允许,同一种物品可以重复放入背包.

正是因为"物品可以重复选择"这个特点,完全背包问题在现实场景中非常常见.例如:

买东西时,某种商品可以买多件;

兑换零钱时,每种面额的硬币可以使用多枚;

凑金额时,某个数字可以被重复使用;

选择材料时,同一种材料可以取多份;

做任务或获得收益时,同一种操作可以反复执行.

这些问题表面上看起来不完全一样,但它们背后都具有一个共同特征:每种选择可以重复使用,并且需要在某个限制条件下求最优结果或方案数量.这正是完全背包模型的核心.

从算法角度来看,完全背包问题通常可以抽象为:

给定 n 种物品和一个容量为 V 的背包,每种物品有体积 v[i] 和价值 w[i],每种物品可以选择任意多次,求在总体积不超过 V 的情况下,能够获得的最大价值.

它和 01 背包的区别可以简单概括为:

cpp 复制代码
01 背包:每个物品最多选 1 次
完全背包:每个物品可以选 0 次、1 次、2 次、......无限次

因此,完全背包的状态转移也会发生变化.对于当前第 i 种物品来说,我们不再只是考虑"选一次"或"不选",而是要考虑它可以被重复选择.也正因为如此,在进行一维 DP 空间优化时,完全背包的容量遍历顺序通常是 从小到大,这样才能保证当前物品可以被重复使用.

完全背包问题是动态规划中非常重要的基础模型.很多经典算法题都可以归纳到这个模型中,例如零钱兑换、零钱兑换方案数、完全平方数、组合总和等.掌握完全背包,不仅能够帮助我们解决一类具体题目,更重要的是能够加深对动态规划中"状态定义、状态转移、初始化和遍历顺序"的理解.

可以说,完全背包问题是从 01 背包走向更复杂背包模型的重要一步.理解它之后,再学习多重背包、分组背包、二维费用背包等问题时,也会更加容易.


2.完全背包(OJ题)


算法思路:解法(动态规划):

背包问题的状态表示非常经典,如果大家不知道怎么来的,就把它当成一个模板记住吧~

我们先解决第一问:
1.状态表示:
dp[i][j] 表示:从前 i 个物品中挑选,总体积不超过 j,所有的选法中,能挑选出来的最大价值.(这里是和01背包一样哒)

2.状态转移方程:

线性 dp 状态转移方程分析方式,一般都是根据最后一步的状况,来分情况讨论.但是最后一个物品能选很多个,因此我们的需要分很多情况:

i. 选 0 个第 i 个物品:此时相当于是去前 i - 1 个物品中挑选,总体积不超过 j.此时最大价值为 dp[i - 1][j];

ii. 选 1 个第 i 个物品:此时相当于是去前 i - 1 个物品中挑选,总体积不超过 j - v[i].因为挑选了一个 i 物品,此时最大价值为 dp[i - 1][j - v[i]] + w[i];

iii. 选 2 个第 i 个物品:此时相当于是去前 i - 1 个物品中挑选,总体积不超过 j - 2 * v[i].因为挑选了两个 i 物品,此时最大价值为 dp[i - 1][j - 2 * v[i]] + 2 * w[i];

iv. ......

综上,我们的状态转移方程为:

复制代码
`dp[i][j]=max(dp[i-1][j], dp[i-1][j-v[i]]+w[i], dp[i-1][j-2*v[i]]+2*w[i]...)`

当我们发现,计算一个状态的时候,需要一个循环才能搞定的时候,我们要想到去优化.优化的方向就是用一个或者两个状态来表示这一堆的状态,通常就是用数学的方式做一下等价替换.我们发现第二维是有规律的变化的,因此我们去看看 dp[i][j - v[i]] 这个状态:

dp[i][j-v[i]]=max(dp[i-1][j-v[i]], dp[i-1][j-2*v[i]]+w[i], dp[i-1][j-3*v[i]]+2*w[i]...)

我们发现,把 dp[i][j - v[i]] 加上 w[i] 正好和 dp[i][j] 中除了第一项以外的全部一致,因此我们可以修改我们的状态转移方程为:
dp[i][j] = max(dp[i - 1][j], dp[i][j - v[i]] + w[i]).

3.初始化:

我们多加一行,方便我们的初始化,此时仅需将第一行初始化为 0 即可.因为什么也不选,也能满足体积不小于 j 的情况,此时的价值为 0.

4.填表顺序:

根据状态转移方程,我们仅需从上往下填表即可.

5.返回值:

根据状态表示,返回 dp[n][V].

接下来解决第二问:

第二问仅需微调一下 dp 过程的五步即可.

因为有可能凑不齐 j 体积的物品,因此我们把不合法的状态设置为 -1.

1.状态表示:
dp[i][j] 表示:从前 i 个物品中挑选,总体积正好等于 j,所有的选法中,能挑选出来的最大价值.

2.状态转移方程:
dp[i][j] = max(dp[i - 1][j], dp[i][j - v[i]] + w[i]).

但是在使用 dp[i][j - v[i]] 的时候,不仅要判断 j >= v[i],又要判断 dp[i][j - v[i]] 表示的情况是否存在,也就是 dp[i][j - v[i]] != -1.

3.初始化:

我们多加一行,方便我们的初始化:

i. 第一个格子为 0,因为正好能凑齐体积为 0 的背包;

ii. 但是第一行后面的格子都是 -1,因为没有物品,无法满足体积大于 0 的情况.

4.填表顺序:

根据状态转移方程,我们仅需从上往下填表即可.

5.返回值:

由于最后可能凑不成体积为 V 的情况,因此返回之前需要特判一下.

空间优化:

背包问题基本上都是利用滚动数组来做空间上的优化:

i. 利用滚动数组优化;

ii. 直接在原始代码上修改.

在完全背包问题中,优化的结果为:

i. 仅需删掉所有的横坐标.






核心代码

cpp 复制代码
#include <iostream>
#include <string.h>
using namespace std;

//定义数据范围上限
const int N = 1010;

//全局变量定义
//n:物品数量  V:背包最大容量
//v[]:物品体积  w[]:物品价值
int n, V, v[N], w[N];
//二维dp数组:dp[i][j] 表示前i个物品,背包容量为j时的最大价值
int dp[N][N];

int main()
{
    //1.输入数据
    //读取物品数量n和背包容量V
    cin >> n >> V;
    //循环读取n个物品的体积和价值(下标从1开始)
    for(int i = 1; i <= n; i++)
        cin >> v[i] >> w[i];

    //第一问:完全背包(不要求装满背包)
    //状态定义:前i个物品,容量j,能装的最大价值(物品可重复选)
    //遍历物品
    for(int i = 1; i <= n; i++)
        //遍历背包容量
        for(int j = 0; j <= V; j++)
        {
            //情况1:不选第i个物品,价值等于前i-1个物品的结果
            dp[i][j] = dp[i - 1][j];
            //情况2:选第i个物品(完全背包:可重复选,因此用dp[i]而非dp[i-1])
            //前提:当前容量 >= 物品体积
            if(j >= v[i]) 
                //取两种情况的最大值
                dp[i][j] = max(dp[i][j], dp[i][j - v[i]] + w[i]);
        }
    //输出结果:不要求装满时,容量V的最大价值
    cout << dp[n][V] << endl;

    //第二问:完全背包(必须装满背包)
    //重置dp数组为0
    memset(dp, 0, sizeof dp);
    //初始化:必须装满的核心设置
    //dp[0][0] = 0:0个物品,容量0,价值0(合法)
    //dp[0][j>0] = -1:0个物品无法装满容量>0的背包,标记为非法状态
    for(int j = 1; j <= V; j++) dp[0][j] = -1;

    //动态规划填表
    for(int i = 1; i <= n; i++)
        for(int j = 0; j <= V; j++)
        {
            //不选第i个物品
            dp[i][j] = dp[i - 1][j];

            //选第i个物品:需要满足 容量足够 + 前序状态合法
            if(j >= v[i] && dp[i][j - v[i]] != -1)
                dp[i][j] = max(dp[i][j], dp[i][j - v[i]] + w[i]);
        }

    //输出结果:
    //dp[n][V] = -1 → 无法装满,输出0
    //否则输出装满后的最大价值
    cout << (dp[n][V] == -1 ? 0 : dp[n][V]) << endl;
    
    return 0;
}

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;

// 定义数据范围上限
const int N = 1010;

// 全局变量定义
// n:物品数量  V:背包最大容量
// v[]:物品体积  w[]:物品价值
int n, V, v[N], w[N];

// 二维 dp 数组
// dp[i][j] 表示前 i 个物品,背包容量为 j 时的最大价值
int dp[N][N];

// 求解函数:返回两个结果
// first:完全背包,不要求装满
// second:完全背包,必须装满
pair<int, int> solve(vector<pair<int, int>> items, int capacity)
{
    // 赋值给全局变量
    n = items.size();
    V = capacity;

    for (int i = 1; i <= n; i++)
    {
        v[i] = items[i - 1].first;
        w[i] = items[i - 1].second;
    }

    // 第一问:完全背包,不要求装满背包
    memset(dp, 0, sizeof dp);

    // 状态定义:
    // dp[i][j] 表示前 i 个物品,背包容量为 j 时能获得的最大价值
    for (int i = 1; i <= n; i++)
    {
        for (int j = 0; j <= V; j++)
        {
            // 情况1:不选第 i 个物品
            dp[i][j] = dp[i - 1][j];

            // 情况2:选第 i 个物品
            // 完全背包可以重复选,所以使用 dp[i][j - v[i]]
            if (j >= v[i])
            {
                dp[i][j] = max(dp[i][j], dp[i][j - v[i]] + w[i]);
            }
        }
    }

    int answer1 = dp[n][V];

    // 第二问:完全背包,必须装满背包
    memset(dp, 0, sizeof dp);

    // 初始化非法状态
    // dp[0][0] = 0 表示 0 个物品装满容量 0 是合法的
    // dp[0][j] = -1 表示 0 个物品无法装满容量 j
    for (int j = 1; j <= V; j++)
    {
        dp[0][j] = -1;
    }

    for (int i = 1; i <= n; i++)
    {
        for (int j = 0; j <= V; j++)
        {
            // 情况1:不选第 i 个物品
            dp[i][j] = dp[i - 1][j];

            // 情况2:选第 i 个物品
            // 需要容量足够,并且前面的状态是合法的
            if (j >= v[i] && dp[i][j - v[i]] != -1)
            {
                dp[i][j] = max(dp[i][j], dp[i][j - v[i]] + w[i]);
            }
        }
    }

    int answer2 = (dp[n][V] == -1 ? 0 : dp[n][V]);

    return {answer1, answer2};
}

void printItems(const vector<pair<int, int>>& items)
{
    cout << "[";

    for (int i = 0; i < items.size(); i++)
    {
        cout << "(体积:" << items[i].first
             << ", 价值:" << items[i].second << ")";

        if (i != items.size() - 1)
        {
            cout << ", ";
        }
    }

    cout << "]";
}

void runTest(
        vector<pair<int, int>> items,
        int capacity,
        int expected1,
        int expected2
)
{
    pair<int, int> result = solve(items, capacity);

    cout << "物品列表 = ";
    printItems(items);
    cout << endl;

    cout << "背包容量 = " << capacity << endl;

    cout << "不要求装满的最大价值 = " << result.first << endl;
    cout << "期望结果 = " << expected1 << endl;

    cout << "必须装满的最大价值 = " << result.second << endl;
    cout << "期望结果 = " << expected2 << endl;

    if (result.first == expected1 && result.second == expected2)
    {
        cout << "测试通过" << endl;
    }
    else
    {
        cout << "测试失败" << endl;
    }

    cout << "------------------------" << endl;
}

int main()
{
    // 测试用例1:普通情况
    // 物品:
    // 体积 1,价值 2
    // 体积 2,价值 4
    // 体积 3,价值 4
    // 容量 5
    //
    // 不要求装满:可以选 5 个体积为 1 的物品,总价值 10
    // 必须装满:同样可以装满,总价值 10
    runTest(
            {
                    {1, 2},
                    {2, 4},
                    {3, 4}
            },
            5,
            10,
            10
    );

    // 测试用例2:容量无法被任何组合刚好装满
    // 物品体积为 2 和 4,背包容量为 5
    // 不要求装满:最多装体积 4,价值 8
    // 必须装满:无法刚好装满,输出 0
    runTest(
            {
                    {2, 3},
                    {4, 8}
            },
            5,
            8,
            0
    );

    // 测试用例3:可以重复选择同一个物品
    // 选择 3 个体积为 3、价值为 5 的物品
    // 总体积 9,总价值 15
    runTest(
            {
                    {3, 5},
                    {5, 8}
            },
            9,
            15,
            15
    );

    // 测试用例4:存在多种装法,选择价值最大的
    // 容量 10
    // 可以选 5 个体积 2 价值 3 的物品,总价值 15
    // 也可以选 2 个体积 5 价值 10 的物品,总价值 20
    runTest(
            {
                    {2, 3},
                    {5, 10}
            },
            10,
            20,
            20
    );

    // 测试用例5:背包容量为 0
    // 不管是否要求装满,最大价值都是 0
    runTest(
            {
                    {1, 10},
                    {2, 20}
            },
            0,
            0,
            0
    );

    // 测试用例6:只有一个物品
    // 体积 3,价值 7,容量 10
    // 不要求装满:最多选 3 个,总体积 9,总价值 21
    // 必须装满:无法刚好装满容量 10,输出 0
    runTest(
            {
                    {3, 7}
            },
            10,
            21,
            0
    );

    return 0;
}


核心代码

cpp 复制代码
//空间优化后
#include <iostream>
#include <string.h>
using namespace std;

//定义数据最大范围,适配题目数据规模
const int N = 1010;

//全局变量定义
//n:物品数量  V:背包最大容量
//v[]:存储每个物品的体积  w[]:存储每个物品的价值
int n, V, v[N], w[N];
//一维dp数组:滚动数组优化,dp[j]表示背包容量为j时的最大价值
int dp[N];

int main()
{
    //1.输入数据
    //读取物品总数n 和 背包最大容量V
    cin >> n >> V;
    //循环读取n个物品的体积和价值(物品下标从1开始)
    for(int i = 1; i <= n; i++)
        cin >> v[i] >> w[i];

    //第一问:完全背包(不要求装满背包)
    //完全背包核心:物品可**无限次选取**
    //外层循环:遍历每一个物品
    for(int i = 1; i <= n; i++)
        //内层循环:**正序遍历**背包容量!(完全背包一维优化核心)
        //正序遍历:允许同一个物品被重复选取(和01背包逆序遍历相反)
        for(int j = v[i]; j <= V; j++)
            //状态转移方程:
            //dp[j] = 不选当前物品的价值(原值) vs 选当前物品的价值(dp[j-v[i]]+w[i])
            dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
    //输出结果:背包容量为V时的最大价值(不要求装满)
    cout << dp[V] << endl;

    //第二问:完全背包(必须装满背包)
    //重置dp数组为0,重新初始化
    memset(dp, 0, sizeof dp);
    //必须装满的核心初始化:
    //dp[0] = 0:容量0,价值0(合法,刚好装满)
    //dp[j>0] = -0x3f3f3f3f:用极小值标记**非法状态**(无法装满)
    for(int j = 1; j <= V; j++) dp[j] = -0x3f3f3f3f;

    //完全背包动态规划(逻辑和第一问一致)
    for(int i = 1; i <= n; i++)
        for(int j = v[i]; j <= V; j++)
            dp[j] = max(dp[j], dp[j - v[i]] + w[i]);

    //输出结果:
    //如果dp[V] < 0 → 无法装满背包,输出0
    //否则输出装满背包时的最大价值
    cout << (dp[V] < 0 ? 0 : dp[V]) << endl;

    return 0;
}

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;

// 定义数据最大范围,适配题目数据规模
const int N = 1010;

// 全局变量定义
// n:物品数量  V:背包最大容量
// v[]:存储每个物品的体积  w[]:存储每个物品的价值
int n, V, v[N], w[N];

// 一维 dp 数组
// dp[j] 表示背包容量为 j 时的最大价值
int dp[N];

// 求解函数:返回两个答案
// first:完全背包,不要求装满
// second:完全背包,必须装满
pair<int, int> solve(vector<pair<int, int>> items, int capacity)
{
    n = items.size();
    V = capacity;

    for (int i = 1; i <= n; i++)
    {
        v[i] = items[i - 1].first;
        w[i] = items[i - 1].second;
    }

    // 第一问:完全背包,不要求装满背包
    memset(dp, 0, sizeof dp);

    // 完全背包核心:
    // 外层遍历物品,内层正序遍历容量
    // 正序遍历允许当前物品被重复选取
    for (int i = 1; i <= n; i++)
    {
        for (int j = v[i]; j <= V; j++)
        {
            dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
        }
    }

    int answer1 = dp[V];

    // 第二问:完全背包,必须装满背包
    memset(dp, 0, sizeof dp);

    // 必须装满的核心初始化
    // dp[0] = 0,表示容量 0 可以合法装满
    // dp[j > 0] = 极小值,表示初始状态下无法装满
    for (int j = 1; j <= V; j++)
    {
        dp[j] = -0x3f3f3f3f;
    }

    // 完全背包动态规划
    for (int i = 1; i <= n; i++)
    {
        for (int j = v[i]; j <= V; j++)
        {
            dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
        }
    }

    int answer2 = (dp[V] < 0 ? 0 : dp[V]);

    return {answer1, answer2};
}

void printItems(const vector<pair<int, int>>& items)
{
    cout << "[";

    for (int i = 0; i < items.size(); i++)
    {
        cout << "(体积:" << items[i].first
             << ", 价值:" << items[i].second << ")";

        if (i != items.size() - 1)
        {
            cout << ", ";
        }
    }

    cout << "]";
}

void runTest(
        vector<pair<int, int>> items,
        int capacity,
        int expected1,
        int expected2
)
{
    pair<int, int> result = solve(items, capacity);

    cout << "物品列表 = ";
    printItems(items);
    cout << endl;

    cout << "背包容量 = " << capacity << endl;

    cout << "不要求装满的最大价值 = " << result.first << endl;
    cout << "期望结果 = " << expected1 << endl;

    cout << "必须装满的最大价值 = " << result.second << endl;
    cout << "期望结果 = " << expected2 << endl;

    if (result.first == expected1 && result.second == expected2)
    {
        cout << "测试通过" << endl;
    }
    else
    {
        cout << "测试失败" << endl;
    }

    cout << "------------------------" << endl;
}

int main()
{
    // 测试用例1:普通情况
    // 物品:
    // 体积 1,价值 2
    // 体积 2,价值 4
    // 体积 3,价值 4
    // 容量 5
    // 最优:选 5 个体积为 1 的物品,价值 10
    runTest(
            {
                    {1, 2},
                    {2, 4},
                    {3, 4}
            },
            5,
            10,
            10
    );

    // 测试用例2:必须装满时无法刚好装满
    // 体积只有 2 和 4,容量为 5,无法凑出 5
    // 不要求装满:选体积 4 的物品,价值 8
    // 必须装满:无法装满,输出 0
    runTest(
            {
                    {2, 3},
                    {4, 8}
            },
            5,
            8,
            0
    );

    // 测试用例3:可以重复选择同一个物品
    // 选 3 个体积为 3、价值为 5 的物品
    // 总体积 9,总价值 15
    runTest(
            {
                    {3, 5},
                    {5, 8}
            },
            9,
            15,
            15
    );

    // 测试用例4:多种装法,选择价值最大的
    // 容量 10
    // 可以选 5 个体积 2、价值 3 的物品,总价值 15
    // 也可以选 2 个体积 5、价值 10 的物品,总价值 20
    runTest(
            {
                    {2, 3},
                    {5, 10}
            },
            10,
            20,
            20
    );

    // 测试用例5:背包容量为 0
    // 容量 0 本身就是装满状态,最大价值为 0
    runTest(
            {
                    {1, 10},
                    {2, 20}
            },
            0,
            0,
            0
    );

    // 测试用例6:只有一个物品,容量不能刚好装满
    // 体积 3,价值 7,容量 10
    // 不要求装满:最多选 3 个,总体积 9,总价值 21
    // 必须装满:无法凑出 10,输出 0
    runTest(
            {
                    {3, 7}
            },
            10,
            21,
            0
    );

    // 测试用例7:只有一个物品,容量可以刚好装满
    // 体积 5,价值 11,容量 20
    // 可以选 4 个,总价值 44
    runTest(
            {
                    {5, 11}
            },
            20,
            44,
            44
    );

    // 测试用例8:不要求装满和必须装满结果不同
    // 容量 7
    // 不要求装满:选 1 个体积 6、价值 20 的物品,价值 20
    // 必须装满:只能选 体积3 + 体积4,价值 6 + 7 = 13
    runTest(
            {
                    {3, 6},
                    {4, 7},
                    {6, 20}
            },
            7,
            20,
            13
    );

    return 0;
}

3.零钱兑换(OJ题)


算法思路:解法(动态规划):

先将问题转化成我们熟悉的题型.

i. 在一些物品中挑选一些出来,然后在满足某个限定条件下,解决一些问题,大概率是背包模型;

ii. 由于每一个物品都是无限多个的,因此是一个完全背包问题.

接下来的分析就是基于完全背包的方式来的.

1.状态表示:
dp[i][j] 表示:从前 i 个硬币中挑选,总和正好等于 j,所有的选法中,最少的硬币个数.

2.状态转移方程:

线性 dp 状态转移方程分析方式,一般都是根据最后一步的状况,来分情况讨论.但是最后一个物品能选很多个,因此我们的需要分很多情况:

i. 选 0 个第 i 个硬币:此时相当于就是去前 i - 1 个硬币中挑选,总和正好等于 j.此时最少的硬币个数为 dp[i - 1][j];

ii. 选 1 个第 i 个硬币:此时相当于就是去前 i - 1 个硬币中挑选,总和正好等于 j - coins[i].因为挑选了一个 i 硬币,此时最少的硬币个数为 dp[i - 1][j - coins[i]] + 1;

iii. 选 2 个第 i 个硬币:此时相当于就是去前 i - 1 个硬币中挑选,总和正好等于 j - 2 * coins[i].因为挑选了两个 i 硬币,此时最少的硬币个数为 dp[i - 1][j - 2 * coins[i]] + 2;

iv. ......

结合我们在完全背包里面的优化思路,我们最终得到的状态转移方程为:
dp[i][j] = min(dp[i - 1][j], dp[i][j - coins[i]] + 1).

这里教给大家一个技巧,就是相当于把第二种情况 dp[i - 1][j - coins[i]] + 1 里面的 i - 1 变成 i 即可.

3.初始化:

初始化第一行即可.

这里因为取 min,所以我们可以把无效的地方设置成无穷大(0x3f3f3f3f).

因为这里要求正好凑成总和为 j,因此,需要把第一行除了第一个位置的元素,都设置成无穷大.

4.填表顺序:

根据状态转移方程,我们仅需从上往下填表即可.

5.返回值:

根据状态表示,返回 dp[n][V].但是要特判一下,因为有可能凑不到.





核心代码

cpp 复制代码
//解题思路:完全背包问题 - 每种硬币可无限使用,求凑成总金额的最小硬币数
class Solution
{
public:
    //函数功能:返回凑成总金额amount所需的最小硬币数,无法凑成返回-1
    int coinChange(vector<int>& coins, int amount)
    {
        //动态规划四步走:创建dp表 -> 初始化 -> 填表 -> 返回值

        //定义无穷大常量:因为求最小值,用极大值表示无法凑成的状态
        const int INF = 0x3f3f3f3f;
        // n:硬币的种类数量
        int n = coins.size();

        //1.创建 dp 表
        //二维dp数组定义:
        //dp[i][j] 表示:使用前 i 种硬币,凑成总金额 j 所需的**最小硬币数量**
        vector<vector<int>> dp(n + 1, vector<int>(amount + 1));

        //2.初始化
        //0种硬币时:
        //凑金额0:需要0个硬币(默认初始化)
        //凑金额j>0:无法凑成,赋值为无穷大INF
        for(int j = 1; j <= amount; j++) 
            dp[0][j] = INF;

        //3.填表(完全背包核心)
        //外层循环:遍历所有硬币种类(i从1开始)
        for(int i = 1; i <= n; i++)
            //内层循环:遍历所有金额
            for(int j = 0; j <= amount; j++)
            {
                //情况1:不选第i种硬币
                //最小数量 = 前i-1种硬币凑j的最小数量
                dp[i][j] = dp[i - 1][j];
                
                //情况2:选第i种硬币(完全背包:可重复选)
                //前提:当前金额 >= 硬币面值
                if(j >= coins[i - 1])
                    //取「不选」和「选」的最小值
                    //选的话:dp[i][j-硬币面值] + 1(多加1个当前硬币)
                    dp[i][j] = min(dp[i][j], dp[i][j - coins[i - 1]] + 1);
            }

        //4.返回值
        //最终结果:n种硬币凑amount的最小数量
        //如果结果 >= INF,说明无法凑成,返回-1;否则返回最小数量
        return dp[n][amount] >= INF ? -1 : dp[n][amount];
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

// 解题思路:完全背包问题
// 每种硬币可无限使用,求凑成总金额的最小硬币数
class Solution
{
public:
    // 函数功能:返回凑成总金额 amount 所需的最小硬币数,无法凑成返回 -1
    int coinChange(vector<int>& coins, int amount)
    {
        // 动态规划四步走:创建 dp 表 -> 初始化 -> 填表 -> 返回值

        // 定义无穷大常量:因为求最小值,用极大值表示无法凑成的状态
        const int INF = 0x3f3f3f3f;

        // n:硬币的种类数量
        int n = coins.size();

        // 1. 创建 dp 表
        // dp[i][j] 表示:
        // 使用前 i 种硬币,凑成总金额 j 所需的最小硬币数量
        vector<vector<int>> dp(n + 1, vector<int>(amount + 1, 0));

        // 2. 初始化
        // 0 种硬币时:
        // 凑金额 0:需要 0 个硬币
        // 凑金额 j > 0:无法凑成,赋值为 INF
        for (int j = 1; j <= amount; j++)
        {
            dp[0][j] = INF;
        }

        // 3. 填表:完全背包
        for (int i = 1; i <= n; i++)
        {
            for (int j = 0; j <= amount; j++)
            {
                // 情况1:不选第 i 种硬币
                dp[i][j] = dp[i - 1][j];

                // 情况2:选第 i 种硬币
                // 完全背包:当前硬币可以重复使用,所以是 dp[i][j - coins[i - 1]]
                if (j >= coins[i - 1])
                {
                    dp[i][j] = min(dp[i][j], dp[i][j - coins[i - 1]] + 1);
                }
            }
        }

        // 4. 返回值
        return dp[n][amount] >= INF ? -1 : dp[n][amount];
    }
};

void printVector(const vector<int>& coins)
{
    cout << "[";

    for (int i = 0; i < coins.size(); i++)
    {
        cout << coins[i];

        if (i != coins.size() - 1)
        {
            cout << ", ";
        }
    }

    cout << "]";
}

void runTest(Solution& solution, vector<int> coins, int amount, int expected)
{
    int result = solution.coinChange(coins, amount);

    cout << "coins = ";
    printVector(coins);
    cout << endl;

    cout << "amount = " << amount << endl;
    cout << "最少硬币数 = " << result << endl;
    cout << "期望结果 = " << expected << endl;

    if (result == expected)
    {
        cout << "测试通过" << endl;
    }
    else
    {
        cout << "测试失败" << endl;
    }

    cout << "------------------------" << endl;
}

int main()
{
    Solution solution;

    // 测试用例1:经典示例
    // 11 = 5 + 5 + 1,所以最少需要 3 枚硬币
    runTest(solution, {1, 2, 5}, 11, 3);

    // 测试用例2:无法凑成
    // 只有面值 2,无法凑成金额 3
    runTest(solution, {2}, 3, -1);

    // 测试用例3:金额为 0
    // 凑成金额 0 不需要任何硬币
    runTest(solution, {1}, 0, 0);

    // 测试用例4:刚好使用一枚硬币
    runTest(solution, {1, 3, 4}, 4, 1);

    // 测试用例5:贪心不一定最优
    // 6 = 3 + 3,只需要 2 枚
    // 如果贪心先选 4,则 4 + 1 + 1 需要 3 枚
    runTest(solution, {1, 3, 4}, 6, 2);

    // 测试用例6:只有一种硬币,可以凑成
    // 10 = 5 + 5
    runTest(solution, {5}, 10, 2);

    // 测试用例7:只有一种硬币,无法凑成
    runTest(solution, {5}, 11, -1);

    // 测试用例8:多种硬币
    // 27 = 10 + 10 + 5 + 2
    runTest(solution, {2, 5, 10, 1}, 27, 4);

    // 测试用例9:硬币面值都大于金额
    runTest(solution, {7, 8, 9}, 5, -1);

    // 测试用例10:重复面值也可以正常处理
    runTest(solution, {1, 2, 2, 5}, 11, 3);

    return 0;
}


核心代码

cpp 复制代码
//空间优化后
//解题思路:完全背包问题 - 硬币可无限次使用,求凑成总金额的最小硬币数
class Solution
{
public:
    //函数功能:计算凑成总金额amount所需的最小硬币数,无法凑成则返回-1
    int coinChange(vector<int>& coins, int amount)
    {
        //动态规划标准四步:创建dp表 → 初始化 → 填表 → 返回值
        
        //定义无穷大常量:用于标记无法凑成的状态(因为求最小值,初始化为极大值)
        const int INF = 0x3f3f3f3f;
        //n:硬币的种类数量
        int n = coins.size();

        //1.创建 dp 表 + 初始化
        //一维滚动数组:dp[j] 表示 凑成总金额为 j 所需的最小硬币数
        //初始化为无穷大:代表默认所有金额都无法凑成
        vector<int> dp(amount + 1, INF);
        //基准条件:凑成金额 0,需要 0 个硬币
        dp[0] = 0;

        //3.填表(完全背包核心)
        //外层循环:遍历每一种硬币(物品)
        for(int i = 1; i <= n; i++)
            //内层循环:正序遍历金额(背包容量)
            //正序遍历 = 完全背包专属:允许硬币被重复选取
            for(int j = coins[i - 1]; j <= amount; j++)
                //状态转移方程:
                //不选当前硬币:保持 dp[j] 原值
                //选当前硬币:dp[j - 当前硬币面值] + 1(多加1枚当前硬币)
                //取两种情况的最小值
                dp[j] = min(dp[j], dp[j - coins[i - 1]] + 1);

        //4.返回值
        //若最终值仍为无穷大 → 无法凑成该金额,返回-1
        //否则返回最小硬币数
        return dp[amount] >= INF ? -1 : dp[amount];
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

// 空间优化后
// 解题思路:完全背包问题
// 硬币可无限次使用,求凑成总金额的最小硬币数
class Solution
{
public:
    // 函数功能:计算凑成总金额 amount 所需的最小硬币数,无法凑成则返回 -1
    int coinChange(vector<int>& coins, int amount)
    {
        // 动态规划标准四步:创建 dp 表 → 初始化 → 填表 → 返回值

        // 定义无穷大常量:
        // 用于标记无法凑成的状态
        const int INF = 0x3f3f3f3f;

        // n:硬币的种类数量
        int n = coins.size();

        // 1. 创建 dp 表 + 初始化
        // dp[j] 表示凑成总金额 j 所需的最小硬币数
        vector<int> dp(amount + 1, INF);

        // 基准条件:
        // 凑成金额 0,需要 0 个硬币
        dp[0] = 0;

        // 2. 填表:完全背包
        // 外层遍历硬币,内层正序遍历金额
        for (int i = 1; i <= n; i++)
        {
            for (int j = coins[i - 1]; j <= amount; j++)
            {
                // 不选当前硬币:dp[j] 保持原值
                // 选当前硬币:dp[j - coins[i - 1]] + 1
                dp[j] = min(dp[j], dp[j - coins[i - 1]] + 1);
            }
        }

        // 3. 返回值
        return dp[amount] >= INF ? -1 : dp[amount];
    }
};

void printVector(const vector<int>& coins)
{
    cout << "[";

    for (int i = 0; i < coins.size(); i++)
    {
        cout << coins[i];

        if (i != coins.size() - 1)
        {
            cout << ", ";
        }
    }

    cout << "]";
}

void runTest(Solution& solution, vector<int> coins, int amount, int expected)
{
    int result = solution.coinChange(coins, amount);

    cout << "coins = ";
    printVector(coins);
    cout << endl;

    cout << "amount = " << amount << endl;
    cout << "最少硬币数 = " << result << endl;
    cout << "期望结果 = " << expected << endl;

    if (result == expected)
    {
        cout << "测试通过" << endl;
    }
    else
    {
        cout << "测试失败" << endl;
    }

    cout << "------------------------" << endl;
}

int main()
{
    Solution solution;

    // 测试用例1:经典示例
    // 11 = 5 + 5 + 1
    // 最少需要 3 枚硬币
    runTest(solution, {1, 2, 5}, 11, 3);

    // 测试用例2:无法凑成
    // 只有面值 2,无法凑成金额 3
    runTest(solution, {2}, 3, -1);

    // 测试用例3:金额为 0
    // 凑成金额 0 不需要任何硬币
    runTest(solution, {1}, 0, 0);

    // 测试用例4:刚好使用一枚硬币
    runTest(solution, {1, 3, 4}, 4, 1);

    // 测试用例5:贪心不一定最优
    // 6 = 3 + 3,只需要 2 枚
    // 如果贪心先选 4,则 4 + 1 + 1 需要 3 枚
    runTest(solution, {1, 3, 4}, 6, 2);

    // 测试用例6:只有一种硬币,可以凑成
    // 10 = 5 + 5
    runTest(solution, {5}, 10, 2);

    // 测试用例7:只有一种硬币,无法凑成
    runTest(solution, {5}, 11, -1);

    // 测试用例8:多种硬币
    // 27 = 10 + 10 + 5 + 2
    runTest(solution, {2, 5, 10, 1}, 27, 4);

    // 测试用例9:所有硬币面值都大于 amount
    runTest(solution, {7, 8, 9}, 5, -1);

    // 测试用例10:硬币面值有重复
    runTest(solution, {1, 2, 2, 5}, 11, 3);

    // 测试用例11:较大金额
    // 100 = 25 * 4
    runTest(solution, {1, 5, 10, 25}, 100, 4);

    // 测试用例12:存在硬币但无法凑成
    // 面值都是偶数,无法凑成奇数 7
    runTest(solution, {2, 4, 6}, 7, -1);

    return 0;
}

4.零钱兑换II(OJ题)


算法思路:解法(动态规划):

先将问题转化成我们熟悉的题型.

i. 在一些物品中挑选一些出来,然后在满足某个限定条件下,解决一些问题,大概率是背包模型;

ii. 由于每一个物品都是无限多个的,因此是一个完全背包问题.

接下来的分析就是基于完全背包的方式来的.

1.状态表示:
dp[i][j] 表示:从前 i 个硬币中挑选,总和正好等于 j,一共有多少种选法.

2.状态转移方程:

线性 dp 状态转移方程分析方式,一般都是根据最后一步的状况,来分情况讨论.但是最后一个物品能选很多个,因此我们的需要分很多情况:

i. 选 0 个第 i 个硬币:此时相当于就是去前 i - 1 个硬币中挑选,总和正好等于 j.此时最少的硬币个数为 dp[i - 1][j];

ii. 选 1 个第 i 个硬币:此时相当于就是去前 i - 1 个硬币中挑选,总和正好等于 j - coins[i].因为挑选了一个 i 硬币,此时最少的硬币个数为 dp[i - 1][j - coins[i]] + 1;

iii. 选 2 个第 i 个硬币:此时相当于就是去前 i - 1 个硬币中挑选,总和正好等于 j - 2 * coins[i].因为挑选了两个 i 硬币,此时最少的硬币个数为 dp[i - 1][j - 2 * coins[i]] + 2;

iv. ......

结合我们在完全背包里面的优化思路,我们最终得到的状态转移方程为:
dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i]] + 1.

这里教给大家一个技巧,就是相当于把第二种情况 dp[i - 1][j - coins[i]] + 1 里面的 i - 1 变成 i 即可.

3.初始化:

初始化第一行即可.

第一行表示没有物品,没有物品正好能凑能和为 0 的情况.因此 dp[0][0] = 1,其余位置都是 0 种情况.

4.填表顺序:

根据状态转移方程,我们仅需从上往下填表即可.

5.返回值:

根据状态表示,返回 dp[n][V].






核心代码

cpp 复制代码
class Solution {
public:
    int change(int amount, vector<int>& coins) {
        //题目保证最终答案在 32 位有符号整数范围内
        //这里设置一个上限,防止中间状态计算时数值过大导致溢出
        const long long LIMIT = INT_MAX;

        //dp[i] 表示凑成金额 i 的组合数
        //使用 long long 是为了尽量避免 int 溢出
        vector<long long> dp(amount + 1, 0);

        //凑成金额 0 的方法只有 1 种:
        //什么硬币都不选
        dp[0] = 1;

        //外层遍历硬币
        //这样可以保证统计的是"组合数",而不是"排列数"
        //例如 1 + 2 和 2 + 1 只会被算作同一种组合
        for (int coin : coins) {

            //内层正序遍历金额
            //因为每种硬币可以使用无限次
            //所以 sum 从 coin 开始递增
            for (int sum = coin; sum <= amount; sum++) {

                //状态转移:
                //当前金额 sum 的组合数
                //加上使用一个 coin 后,凑成 sum - coin 的组合数
                dp[sum] += dp[sum - coin];

                //防止中间状态过大导致 long long 溢出
                //因为最终答案只需要返回 int 范围内的结果
                //所以超过 INT_MAX 后可以进行截断
                if (dp[sum] > LIMIT) {
                    dp[sum] = LIMIT;
                }
            }
        }

        //dp[amount] 就是凑成目标金额 amount 的组合数
        return dp[amount];
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        // 题目保证最终答案在 32 位有符号整数范围内
        // 这里设置一个上限,防止中间状态计算时数值过大导致溢出
        const long long LIMIT = INT_MAX;

        // dp[i] 表示凑成金额 i 的组合数
        vector<long long> dp(amount + 1, 0);

        // 凑成金额 0 的方法只有 1 种:什么硬币都不选
        dp[0] = 1;

        // 外层遍历硬币,保证统计的是组合数,不是排列数
        for (int coin : coins) {
            // 正序遍历金额,因为每种硬币可以无限使用
            for (int sum = coin; sum <= amount; sum++) {
                dp[sum] += dp[sum - coin];

                // 防止中间状态过大导致溢出
                if (dp[sum] > LIMIT) {
                    dp[sum] = LIMIT;
                }
            }
        }

        return dp[amount];
    }
};

void test(int amount, vector<int> coins, int expected) {
    Solution solution;
    int result = solution.change(amount, coins);

    cout << "amount = " << amount << ", coins = [";
    for (int i = 0; i < coins.size(); i++) {
        cout << coins[i];
        if (i != coins.size() - 1) {
            cout << ", ";
        }
    }
    cout << "]" << endl;

    cout << "输出结果: " << result << endl;
    cout << "期望结果: " << expected << endl;

    if (result == expected) {
        cout << "测试通过" << endl;
    } else {
        cout << "测试失败" << endl;
    }

    cout << "------------------------" << endl;
}

int main() {
    // 示例 1
    // 组合方式:
    // 5
    // 2 + 2 + 1
    // 2 + 1 + 1 + 1
    // 1 + 1 + 1 + 1 + 1
    test(5, {1, 2, 5}, 4);

    // 示例 2
    // 无法用硬币 2 凑成金额 3
    test(3, {2}, 0);

    // 示例 3
    // 只有一种方式:10
    test(10, {10}, 1);

    // 边界测试:amount = 0
    // 什么都不选,也是一种方案
    test(0, {1, 2, 5}, 1);

    // 普通测试
    test(4, {1, 2, 3}, 4);

    // 普通测试
    // 组合为:
    // 10
    // 5 + 5
    // 5 + 2 + 2 + 1
    // 5 + 2 + 1 + 1 + 1
    // 5 + 1 + 1 + 1 + 1 + 1
    // 2 + 2 + 2 + 2 + 2
    // ...
    test(10, {1, 2, 5}, 10);

    return 0;
}

5.完全平方数(OJ题)


算法思路:解法(动态规划):

这里给出一个用拆分出相同子问题的方式,定义一个状态表示.(用完全背包方式的解法就仿照之前的分析模式就好啦~~)

为了叙述方便,把和为 n 的完全平方数的最少数量简称为最小数量.

对于 12 这个数,我们分析一下如何求它的最小数量:

  • 如果 12 本身就是完全平方数,我们不用算了,直接返回 1;
  • 但是 12 不是完全平方数,我们试着把问题分解一下:
    1. 情况一:拆出来一个 1,然后看看 11 的最小数量,记为 x1;
    2. 情况二:拆出来一个 4,然后看看 8 的最小数量,记为 x2;(为什么拆出来 4,而不拆出来 2 呢?)
    3. 情况三:拆出来一个 9 ......

其中,我们接下来求 118 的时候,其实又回到了原来的问题上.

因此,我们可以尝试用 dp 的策略,将 12346 等等这些数的最小数量依次保存起来.再求较大的 n 的时候,直接查表,然后找出最小数量.

1.状态表示:
dp[i] 表示:和为 i 的完全平方数的最少数量.

2.状态转移方程:

对于 dp[i],根据思路那里的分析我们知道,可以根据小于等于 i 的所有完全平方数 x 进行划分:

  • x = 1 时,最小数量为:1 + dp[i - 1];

  • x = 4 时,最小数量为:1 + dp[i - 4] ......

一直枚举到 x <= i 为止.

为了方便枚举完全平方数,我们采用下面的策略:for(int j = 1; j * j <= i; j++)

综上所述,状态转移方程为:

复制代码
`dp[i] = min(dp[i], dp[i - j * j] + 1)`

3.初始化:

n = 0 的时候,没法拆分,结果为 0;

n = 1 的时候,显然为 1.

4.填表顺序:

从左往右.

5.返回值:

根据题意,返回 dp[n] 的值.





核心代码

cpp 复制代码
//题目:给定整数n,求和为n的完全平方数的最少数量
//本质:完全背包问题(完全平方数可无限选,求最小物品数)
class Solution
{
public:
    //函数功能:返回和为n的最少完全平方数的数量
    int numSquares(int n)
    {
        //dp数组定义:dp[i] 表示 组成数字 i 的最少完全平方数的个数
        vector<int> dp(n + 1);
        
        //初始化:数字1只能由1个1²组成,最少数量为1
        dp[1] = 1;

        //外层循环:从2开始遍历到目标数n,依次计算每个数的最小平方数个数
        for(int i = 2; i <= n; i++) 
        {
            //初始最坏情况:i 由 i 个 1² 组成,数量为 1 + dp[i-1]
            dp[i] = 1 + dp[i - 1];
            
            //内层循环:枚举所有小于等于i的完全平方数 j²
            for(int j = 2; j * j <= i; j++) 
                //状态转移:选当前平方数j²,更新最小值
                //dp[i - j*j] + 1:组成i-j²的最小数量 + 当前1个平方数j²
                dp[i] = min(dp[i], dp[i - j * j] + 1); 
        }
        
        //返回最终结果:组成n的最少完全平方数数量
        return dp[n];
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

// 题目:给定整数 n,求和为 n 的完全平方数的最少数量
// 本质:完全背包问题
// 完全平方数可无限选,求最小物品数
class Solution
{
public:
    // 函数功能:返回和为 n 的最少完全平方数的数量
    int numSquares(int n)
    {
        // dp[i] 表示组成数字 i 的最少完全平方数的个数
        vector<int> dp(n + 1, 0);

        // 特殊情况:n = 0
        if (n == 0)
        {
            return 0;
        }

        // 初始化:
        // 数字 1 只能由 1 个 1² 组成
        dp[1] = 1;

        // 从 2 开始遍历到目标数 n
        for (int i = 2; i <= n; i++)
        {
            // 最坏情况:i 由 i 个 1² 组成
            dp[i] = 1 + dp[i - 1];

            // 枚举所有小于等于 i 的完全平方数 j²
            for (int j = 2; j * j <= i; j++)
            {
                // 选择当前平方数 j²
                dp[i] = min(dp[i], dp[i - j * j] + 1);
            }
        }

        // 返回最终结果
        return dp[n];
    }
};

void runTest(Solution& solution, int n, int expected)
{
    int result = solution.numSquares(n);

    cout << "n = " << n << endl;
    cout << "最少完全平方数数量 = " << result << endl;
    cout << "期望结果 = " << expected << endl;

    if (result == expected)
    {
        cout << "测试通过" << endl;
    }
    else
    {
        cout << "测试失败" << endl;
    }

    cout << "------------------------" << endl;
}

int main()
{
    Solution solution;

    // 测试用例1:经典示例
    // 12 = 4 + 4 + 4
    runTest(solution, 12, 3);

    // 测试用例2:经典示例
    // 13 = 4 + 9
    runTest(solution, 13, 2);

    // 测试用例3:n = 1
    // 1 = 1
    runTest(solution, 1, 1);

    // 测试用例4:n 本身是完全平方数
    // 16 = 16
    runTest(solution, 16, 1);

    // 测试用例5:n = 2
    // 2 = 1 + 1
    runTest(solution, 2, 2);

    // 测试用例6:n = 3
    // 3 = 1 + 1 + 1
    runTest(solution, 3, 3);

    // 测试用例7:n = 4
    // 4 = 4
    runTest(solution, 4, 1);

    // 测试用例8:n = 5
    // 5 = 4 + 1
    runTest(solution, 5, 2);

    // 测试用例9:n = 27
    // 27 = 9 + 9 + 9
    runTest(solution, 27, 3);

    // 测试用例10:n = 43
    // 43 = 25 + 9 + 9
    runTest(solution, 43, 3);

    // 测试用例11:n = 100
    // 100 = 100
    runTest(solution, 100, 1);

    // 测试用例12:n = 0
    // 如果自己测试允许 n = 0,则答案为 0
    runTest(solution, 0, 0);

    return 0;
}


🚀真正的勇者不是流泪的人,而是含泪奔跑的人!


敬请期待下一篇文章内容【动态规划算法】(一文讲透二维费用的背包问题)


每日心灵鸡汤:就做你自己吧,没有很好也没关系
最近很喜欢的一句话:每个人都有阴暗面,没有什么好奇怪的,对不同的人有不同的态度,喜欢就交流,不喜欢就陌路,不用理解,各有各的路.每当有人说我人好的时候,我常常就会想,其实并不是我好,而是我付出的善意真诚好.他们也会给予我同样的善意真心,不是我人好,而是他们也都很好.性格因人而异,我不主张盲目的善良,也不接受道德绑架,要有不伤害人的教养,也不缺保护自己的能力.在一百个人口中有一百个样子,所以为什么要在乎别人口中的自己呢?学会与很多声音共处,但不会被左右.尊重所有声音,但只成为自己.

相关推荐
超级大福宝1 小时前
【力扣48. 旋转图像】超好记忆版 + 口诀
c++·算法·leetcode
玩转单片机与嵌入式1 小时前
学习嵌入式AI(TInyML),只需掌握这点python基础即可!
人工智能·python·学习
爱写代码的倒霉蛋1 小时前
2023年天梯赛L1-8
数据结构·算法
lzh200409191 小时前
深入学习Linux进程间通信:共享内存
linux·c++
ErizJ1 小时前
Linux|学习笔记
linux·笔记·学习
apollowing1 小时前
启发式算法WebApp实验室:从搜索策略到群体智能的能力进阶(上)
算法·启发式算法·web app
大数据三康2 小时前
Java字符统计:从输入到输出的完整解析
java·学习·循环结构
特种加菲猫2 小时前
多态:让代码拥有“千变万化”的能力
开发语言·c++
952362 小时前
SpringAOP
java·后端·学习·spring