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

最长的一段「越来越大」

给一串数 [1, 3, 2, 4, 3, 5],找一个最长的子序列(按顺序挑,不要求连续),让它严格递增。这里的答案是 1 → 3 → 4 → 5(长度 4)。这仍是动态规划,但和前几页不同——它的 DP 表是一维的。

一维 DP:dp[i] 只回看前面

设 dp[i] 为以第 i 个数结尾的最长递增子序列长度。每个数单独成序列,所以 dp 全部从 1 起。填 dp[i] 时,回看前面每一个 j < i:只要 a[j] < a[i](能接在 a[j] 后面继续变大),就用 dp[j] + 1 更新 dp[i],取所有这样的 j 里最大的。前几页 DP 依赖「上/左/左上」的相邻格,这里 dp[i] 依赖前面所有 dp[j]——这就是一维 DP 的味道。

全部填完后,最大的 dp[i] 就是 LIS 的长度。想恢复出具体是哪几个数,和 LCS 一样从最大值处回溯:记住每个 dp[i] 是从哪个 j 接过来的(前驱),沿前驱倒着串起来即可。

下面是 [1, 3, 2, 4, 3, 5] 的 DP 两行表(上行是值、下行是 dp)。点「下一步」逐步看:当前 i(琥珀)逐个回看每个 j(黄色),能变长就把 dp[i] 增大(绿色);填完后从最大 dp 回溯,构成 LIS 的元素绿环标出。右侧代码随每一步同步高亮。

012345
值132435
dp111111

每个元素自身是长度 1 的递增子序列:dp 全部初始化为 1

1function longestIncreasingSubseq(a: number[]): number[] {
2 const n = a.length;
3 const dp = new Array(n).fill(1); // 每个元素自身长度 1
4 const pred = new Array(n).fill(-1);
5 for (let i = 1; i < n; i++) {
6 for (let j = 0; j < i; j++) {
7 if (a[j] < a[i] && dp[j] + 1 > dp[i]) { // 能接得更长
8 dp[i] = dp[j] + 1;
9 pred[i] = j;
10 }
11 }
12 }
13 let best = 0;
14 for (let i = 1; i < n; i++) if (dp[i] > dp[best]) best = i; // 最长在哪结尾
15 const lis: number[] = [];
16 for (let k = best; k !== -1; k = pred[k]) lis.unshift(a[k]); // 回溯恢复
17 return lis;
18}
输入1 3 2 4 3 5
dp1 1 1 1 1 1
1 / 18

一维 vs 二维

DP 的「表」有不同形状:
二维表:编辑距离 / 0-1 背包 / 最长公共子序列——两个维度(两串、或物品×容量)。
一维数组:最长递增子序列(本页)——dp[i] 沿一条线回看前面。

本页的 O(n²) 解法最直观(每个 i 回看所有 j);LIS 还有更快的 O(n log n) 解法(维护一个「各长度下最小结尾」的数组 + 二分),但一维 DP 已足够把「回看前面、取最优、再回溯恢复」讲清楚。它与LCS同属「子序列」家族,一个在两串间、一个在一串内。