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)

动态规划 · 区间由短及长 · 枚举分割点

贪心为什么会错

4 堆石子 [4, 1, 3, 2] 排成一行,每次只能合并相邻两堆、代价 = 两堆之和,求合成一堆的最小总代价。直觉贪心「每次挑最小的一对」:先合 1+3?可它们的合并结果会反复参与后续合并——早合的堆被「搬运」次数更多,局部最小不等于全局最小。

把区间当阶段

设 dp[i][j] = 合并第 i..j 堆的最小代价。最后一次合并一定把区间劈成两半:dp[i][j] = mink(dp[i][k] + dp[k+1][j]) + sum(i..j)——枚举分割点 k,再加上「最后把两半合起来」的代价(恰为区间和)。填表按区间长度由短及长:对角线 0 → 相邻直合 → 越来越长。下面的上三角表逐格点亮,黄格是当前格引用的最优拆分对。右侧代码随每一步同步高亮。

4132
40
10
30
20

4 堆石子 [4, 1, 3, 2],每次只能合并相邻两堆、代价 = 两堆之和,求合成一堆的最小总代价。贪心「每次挑最小」会错——用区间 DP:dp[i][j] = 合并第 i..j 堆的最小代价,对角线自己合自己 = 0

1function stoneMerge(a: number[]): number {
2 const n = a.length;
3 const pre = [0];
4 for (const x of a) pre.push(pre[pre.length - 1] + x);
5 const dp = Array.from({ length: n }, () => new Array(n).fill(0));
6 for (let len = 2; len <= n; len++) // 区间由短及长
7 for (let i = 0; i + len - 1 < n; i++) {
8 const j = i + len - 1;
9 dp[i][j] = Infinity;
10 for (let k = i; k < j; k++) // 枚举分割点
11 dp[i][j] = Math.min(dp[i][j], dp[i][k] + dp[k + 1][j]);
12 dp[i][j] += pre[j + 1] - pre[i]; // 本次合并代价 = 区间和
13 }
14 return dp[0][n - 1];
15}
石堆[4, 1, 3, 2]
1 / 8

区间 DP 范式

三层循环:len 由短及长 → 左端点 i → 分割点 k,O(n³)。
为什么由短及长:算 dp[i][j] 时它引用的都是更短区间——填表序保证「用到时已就绪」。
环形变体:石子围成圈 → 拆环成链(复制一倍),答案取 min。
更快:四边形不等式(决策单调性)可把 O(n³) 优化到 O(n²)。

同一范式的题排成一串:矩阵链乘法(怎么加括号乘法次数最少)、最优二叉搜索树、能量项链、多边形三角剖分——凡是「最后一步把区间劈成两半」的问题,都是石子合并换皮。与LCS、编辑距离同为二维表 DP,只是填表方向从「左上往右下」换成了「短区间往长区间」。