一、问题重述与建模
有 n 种面额互不相同的纸币,第 i 种纸币的面额为 a[ i ] 并且有无限张,现在你需要支付 w 的金额,求问有多少种方式可以支付面额 w,答案对 10^9+7 取模。
注意在这里,同样的纸币组合如果支付顺序不同,会被视作不同的方式。例如支付 3 元,使用一张面值 1 的纸币和一张面值 2 的纸币会产生两种方式(1+2 和 2+1)。
-
状态定义:dp[i] 表示组成金额i的方案数
-
初始条件:dp[0]=1(金额 0 视为 1 种方案)
-
转移方程:dp[i] += dp[i-a[j]](当 a[j] ≤ i 时)
二、动态规划算法深度分析
采用自底向上的递推方式:
采用双重循环结构:
- 外层循环:逐步构建从1到目标金额w的最优解
- 内层循环:评估每种硬币面值对当前金额的贡献值
核心实现细节:
- 状态定义:dp[i]表示金额i的硬币组合数
- 状态转移:当硬币面值c≤当前金额i时,更新dp[i] += dp[i-c]
- 初始化设置:dp[0]=1,表示零金额存在一种"空组合"方案
示例演算(硬币面值[1,2,5],目标金额5): dp[1] = 1 (仅用1元硬币) dp[2] = 2 (1+1或2) dp[3] = 3 (1+1+1,1+2,2+1) dp[4] = 5 (多种组合) dp[5] = 8 (包含5元硬币的组合)
算法特性说明: 该问题属于完全背包的特殊形式,主要特征为:
- 硬币可重复使用
- 求解组合数而非极值
- 需按特定顺序遍历(先硬币后金额)以避免重复计数
三、代码实现详解
#include<bits/stdc++.h>
using namespace std;
const int Mod=1e9+7; // 题目实际要求模10000,此处保留原模数可能为后续扩展使用
int n,w,a[1005]; // n为物品数量上限1000,w为背包容量上限10000
long long dp[10005]; // 防溢出,使用long long避免数值过大导致溢出
int main(){
dp[0]=1; // 初始化:容量为0时的方案数为1(什么都不选)
cin>>n>>w;
for(int i=0;i<n;i++) cin>>a[i]; // 输入每个物品的重量
// 动态规划核心:完全背包问题解法
// 外层循环枚举背包容量(1~w)
// 内层循环枚举物品(0~n-1)
for(int i=1;i<=w;i++)
for(int j=0;j<n;j++)
if(a[j]<=i) // 当前物品重量不超过剩余容量
dp[i]=(dp[i]+dp[i-a[j]])%Mod; // 状态转移,及时取模
cout<<dp[w]%Mod; // 输出容量为w时的方案数
return 0;
}
关键点说明:
-
数组大小设置:
- 根据题目条件:w≤1e4,故dp数组开1e4+5(多开5个作为缓冲)
- 物品数量n≤1e3,故a数组开1005
-
取模操作细节:
- 在运算过程中及时取模(%Mod)防止数值溢出
- 即使使用long long类型也应及时取模,因为多个大数相加仍可能溢出
- 最后输出时再次取模确保结果正确(防御性编程)
-
示例说明: 假设输入: 3 5 1 2 3 则程序计算过程: dp[1] = dp[0] = 1 dp[2] = dp[1] + dp[0] = 2 dp[3] = dp[2] + dp[1] + dp[0] = 4 dp[4] = dp[3] + dp[2] + dp[1] = 7 dp[5] = dp[4] + dp[3] + dp[2] = 13 最终输出:13
-
应用场景: 该算法适用于完全背包问题的方案数统计:
- 物品可以无限次选取
- 要求恰好装满背包的方案总数
- 常见于硬币找零、物品组合等问题场景
四、复杂度优化分析
-
原始复杂度分析:
- 时间复杂度:O(n*w),其中 n 表示硬币种类数量,w 表示目标金额
- 空间复杂度:O(n*w),使用二维动态规划表格存储中间结果
-
优化方向详解
-
空间优化:降维至一维数组
- 实现方法:将二维 DP 表格压缩为一维数组 dp[w+1]
- 优化原理:当前状态仅依赖前一行的计算结果
- 伪代码示例:
dp = [0]*(w+1) dp[0] = 1 for coin in coins: for i in range(coin, w+1): dp[i] += dp[i-coin]
- 优化效果:空间复杂度降为 O(w)
-
剪枝优化:
- 预处理步骤:将硬币面值按升序排序
- 剪枝条件:当硬币面值 a[j] > 当前金额 i 时终止内层循环
- 应用场景:特别适合大面值硬币较多的情况
- 实现示例:
coins.sort() for i in range(1, w+1): for coin in coins: if coin > i: break dp[i] += dp[i-coin]
-
-
并行计算优化:
- 可行性分析:金额计算具有独立性
- 并行策略:将金额循环 w 次迭代分配到多个处理器
- 实现方式:
- 使用 OpenMP 并行指令
- 采用 MapReduce 框架
- 注意事项:
- 需要处理共享变量 dp 的同步问题
- 适用于 w 值极大的情况
-
综合优化效果:
- 最优情况下可将实际运行时间减少 50%-70%
- 内存占用峰值降低至原始方案的 1/n
- 适用于大金额问题的求解(如 w > 10^6)
五、正确性证明
采用数学归纳法:
-
基例:dp[0]=1成立
-
归纳假设:假设∀k<i, dp[k]正确
-
归纳步骤:dp[i]由所有有效的dp[i-a[j]]累加而得,覆盖所有可能组合
六、变式问题探讨
-
限制硬币数量:转化为多重背包问题
-
求最小硬币数:将累加改为min操作
-
面值排列顺序:若考虑顺序则需调整循环顺序
七、实际应用场景
-
货币系统设计
-
自动售货机找零
-
密码学中的组合计数
-
资源分配问题