
1. 项目概述洛谷P15800动态规划题目解析这道来自洛谷平台的P15800题目是GESP2026年3月六级认证的真题考察的核心算法思想是动态规划在组合数学问题中的应用。题目要求从给定的n个正整数中选取若干个数使得它们的和等于给定的目标值m计算所有可能的选取方案数。在实际编程竞赛和算法面试中这类选数求和问题是动态规划的经典应用场景。与简单的暴力枚举相比动态规划能够将时间复杂度从O(2^n)优化到O(n*m)这在n和m较大时比如n100m10000能带来数百万倍的性能提升。2. 动态规划基础与问题分析2.1 动态规划的核心思想动态规划(Dynamic Programming)通过将原问题分解为相对简单的子问题的方式来求解复杂问题。它有三个关键特征最优子结构问题的最优解包含子问题的最优解重叠子问题递归算法会反复计算相同的子问题无后效性当前状态一旦确定后续决策不受之前决策影响对于选数问题我们可以定义dp[i][j]表示考虑前i个数时和为j的方案数。这个状态定义满足上述三个特征因此适合用动态规划解决。2.2 问题输入输出分析题目典型输入格式n m a1 a2 ... an其中n数字个数 (1 ≤ n ≤ 100)m目标和 (1 ≤ m ≤ 10000)ai每个数字的值 (1 ≤ ai ≤ 1000)输出为一个整数表示选取方案数。例如 输入4 5 1 2 3 4输出2解释有两种方案可以得到和5 (14 和 23)3. 动态规划解法详解3.1 状态转移方程推导我们定义dp[i][j]为考虑前i个数时和为j的方案数。状态转移需要考虑两种情况不选第i个数方案数等于dp[i-1][j]选第i个数方案数等于dp[i-1][j-ai]前提是j ≥ ai因此状态转移方程为dp[i][j] dp[i-1][j] (j ai ? dp[i-1][j-ai] : 0)初始条件dp[0][0] 1 0个数和为0有1种方案dp[0][j] 0 for j 0 0个数和大于0没有方案3.2 空间优化技巧观察状态转移方程可以发现dp[i]只依赖于dp[i-1]因此可以将二维数组优化为一维数组节省空间vectorint dp(m1, 0); dp[0] 1; for(int i 1; i n; i) { for(int j m; j a[i]; j--) { dp[j] dp[j - a[i]]; } }注意内层循环需要从大到小遍历避免重复计算这是背包类问题的常见技巧。4. 完整代码实现与解析4.1 C实现代码#include iostream #include vector using namespace std; int main() { int n, m; cin n m; vectorint a(n1); for(int i 1; i n; i) { cin a[i]; } vectorint dp(m1, 0); dp[0] 1; for(int i 1; i n; i) { for(int j m; j a[i]; j--) { dp[j] dp[j - a[i]]; } } cout dp[m] endl; return 0; }4.2 代码关键点解析输入处理使用vector存储数字从索引1开始更符合问题描述初始化dp数组大小为m1初始化dp[0]1双重循环外层遍历数字内层逆向遍历和状态转移dp[j] dp[j-a[i]]实现状态转移输出最终dp[m]即为答案5. 算法优化与变种讨论5.1 时间与空间复杂度分析时间复杂度O(n*m)两重循环空间复杂度O(m)使用一维数组优化5.2 常见变种问题每个数字只能选一次本题情况每个数字可以选无限次完全背包问题需要输出具体方案而不仅是方案数数字包含负数的情况求最接近m的和不一定要等于m对于变种3输出具体方案可以在动态规划后通过回溯法找出所有方案void backtrack(int i, int j, vectorint path) { if(j 0) { // 输出方案 for(int num : path) cout num ; cout endl; return; } if(i 0 || j 0) return; // 不选a[i] backtrack(i-1, j, path); // 选a[i] if(j a[i] dp[i-1][j-a[i]] 0) { path.push_back(a[i]); backtrack(i-1, j-a[i], path); path.pop_back(); } }6. 实战技巧与注意事项6.1 常见错误与调试技巧数组越界确保dp数组大小足够m1初始化错误忘记初始化dp[0]1循环顺序错误内层循环必须逆向遍历整数溢出方案数可能很大考虑使用long long调试技巧可以打印中间dp表观察状态转移是否正确6.2 性能优化建议输入优化对于大规模数据使用快速输入方法ios::sync_with_stdio(false); cin.tie(0);提前终止如果某个dp[j]已经不可能达到可以跳过空间优化如前述使用一维数组6.3 测试用例设计设计测试用例时应考虑边界情况n1m1无解情况所有数字都大于m大数情况测试整数溢出重复数字数组中有重复数字极端数据n100m10000示例测试用例// 样例1 3 5 1 2 3 // 应输出2 // 样例2 4 6 1 2 3 4 // 应输出3 (123, 24, 15) // 样例3 5 10 2 4 6 8 10 // 应输出3 (28, 46, 10)7. 动态规划学习路径建议对于想系统学习动态规划的选手建议按照以下顺序进阶基础背包问题01背包本题类型完全背包多重背包线性动态规划最长上升子序列(LIS)最长公共子序列(LCS)最大子数组和区间动态规划矩阵链乘法石子合并最优二叉搜索树树形动态规划树的最大独立集树的直径树上背包问题状态压缩动态规划旅行商问题(TSP)棋盘覆盖问题对于GESP六级考生重点掌握前两类即可应对大多数考题。洛谷题库中有大量分类练习题建议从普及-难度的动态规划题目开始刷起。