V

算法可视化

ZHEN
分享到微博分享到 XGitHub 仓库个人主页
学习工具算法复杂度速查算法学习路径
数据结构数组链表栈队列树堆哈希表图字典树并查集LRU 缓存跳表线段树B+ 树布隆过滤器树状数组
经典排序算法冒泡排序鸡尾酒排序双调排序选择排序插入排序二分插入排序希尔排序归并排序自顶向下归并快速排序三路快排双轴快排堆排序计数排序基数排序桶排序
图算法Dijkstra 最短路Kruskal 最小生成树Prim 最小生成树Bellman-Ford 最短路拓扑排序Floyd 多源最短路强连通分量2-SAT最大流二分图匹配LCA 倍增欧拉路径
动态规划编辑距离0-1 背包完全背包最长公共子序列最长递增子序列硬币找零方案数石子合并旅行商 TSP树形 DP数位 DP换根 DP
回溯与搜索N 皇后子集生成全排列组合总和迷宫寻路岛屿数量单词搜索数独A* 寻路
字符串KMP 字符串匹配Rabin-KarpBoyer-MooreManacher后缀数组LCP / height 数组AC 自动机Z 函数
数学与数论埃氏筛线性筛欧几里得算法快速幂扩展欧几里得中国剩余定理欧拉函数米勒-拉宾FFTPollard's Rho
计算几何凸包旋转卡壳最近点对线段相交扫描线求交
查找二分查找二分边界旋转数组搜索二分答案三分查找

硬币找零方案数

动态规划 · 计数

不求最优,只数方案

前几页 DP 求的都是「最优值」——最少代价、最大价值。还有一大类 DP 求的是方案数:给若干面额的硬币、每种无限枚,问凑出目标金额有多少种不同组合。这就是计数 DP。

把「取 max」换成「相加」

设 dp[i][a] = 只用前 i 种硬币凑出金额 a 的方案数。它和 完全背包长得几乎一样——同样是「无限次取」的二维表,「用一枚」的来源也在本行dp[i][a-面额]。差别只在算子:

完全背包在「不取」和「取」之间取 max;硬币找零则把两条路的方案数相加——dp[i][a] = dp[i-1][a](不用这种硬币的方案数) + dp[i][a-面额](至少用一枚这种硬币的方案数)。边界也不同:dp[0][0] = 1——凑出 0 元有 1 种方案(什么都不选),这个「1」是所有方案数的种子。

下面固定硬币 [1, 2, 5]、金额 5。点「下一步」逐格看:当前格琥珀环,「不用」的上一行格和「用一枚」的本行左侧格都黄色高亮,两个方案数相加填入即变绿。走到底,右下角 = 4(四种凑法:1×5、1×3+2、1+2×2、5)。右侧代码随每一步同步高亮。

012345
∅100000
1
2
5

边界:凑出金额 0 只有 1 种方案(什么硬币都不选)→ dp[0][0]=1;第 0 行其余金额都凑不出,为 0

1function change(amount: number, coins: number[]): number {
2 const m = coins.length;
3 const dp = Array.from({ length: m + 1 },
4 () => Array(amount + 1).fill(0));
5 dp[0][0] = 1; // 凑 0 元有 1 种:什么都不选
6 for (let i = 1; i <= m; i++) {
7 const c = coins[i - 1];
8 for (let a = 0; a <= amount; a++) {
9 dp[i][a] = dp[i - 1][a]; // 不用第 i 种硬币
10 if (a >= c) dp[i][a] += dp[i][a - c]; // 用一枚(本行,可再用)
11 }
12 }
13 return dp[m][amount];
14}
硬币1,2,5
金额5
1 / 20

计数 DP 在哪里用

组合计数:找零方案、爬楼梯方法数、路径条数、可达状态数。
概率 / 期望:把「方案数」换成「概率累加」,同一套递推。
对照最优 DP:把递推里的 max/min 换成加法、边界 1 而非 0——同一张表,问的问题不同。

建议和 完全背包对照着看:同样一张「无限次取」的二维表、同样的本行来源,只把「取 max」改成「方案数相加」,求的就从最大价值变成了组合总数。