最长上升子序列(LIS)
前言
¶最长上升子序列(LIS)是讲解动态规划算法的经典例题,使用朴素的动态规划算法可以在
一些定义:
上升子序列:对于数列
,若其某一子序列 满足 且 ,则称 为 的一个上升子序列。最长上升子序列: 上升子序列中元素个数最多的那些。
字典序最小的最长上升子序列: 最长上升子序列中(不妨设元素个数为
),元素下标组成的 维向量最小的那个。
最长上升子序列的长度
¶动态规划
¶记
- 时间复杂度:
- 额外空间复杂度:
1234567891011121314151617export function findLengthOfLIS( N: number, cmp: (i: number, j: number) => -1 | 0 | 1 | number,): number { if (N <= 0) return 0
const dp: number[] = new Array(N) for (let j = 0, i: number, count: number; j < N; ++j) { for (i = 0, count = 0; i < j; ++i) { if (cmp(i, j) < 0) { if (count < dp[i]) count = dp[i] } } dp[j] = count + 1 } return dp[N - 1]}
单调栈优化
¶维护一个单调栈 lower_bound) 快速找到
由于对数组进行了一次
- 时间复杂度:
- 额外空间复杂度:
, 为最长上升子序列的长度
1234567891011121314151617import { lowerBound } from '@algorithm.ts/binary-search'
type ICompare<T> = (x: number, y: number) => number
export function findLengthOfLIS(N: number, cmp: ICompare<number>): number { if (N <= 0) return 0
const stack: number[] = [0] for (let i = 1; i < N; ++i) { const idx: number = lowerBound(0, stack.length, x => cmp(stack[x], i))
// Update the monotonous stack. if (idx < stack.length) stack[idx] = i else stack.push(i) } return stack.length}
字典序最小的最长上升子序列
¶在上一个问题中,我们可以获得以
- 令
- 若
,则停止算法 - 找到一个
满足 且 - 令
,执行 2.
不难证明,收集上述算法中所有出现过的
接下来考虑如何获得一个字典序最小的最长上升子序列。为方便叙述,不妨记字典序最小的最长上升子序列为
不难想到,
维护一个长度为
的数列 ,数列中所有元素的初始值均为 ;从
到 遍历 ,对于每个 均执行 3;记
,若 ,则直接将 更新为 ,因为它参与构成的最长上升子序列的字典序肯定比之前找到的 更优。否则, ,此时需要检查一下是否已经找到了一个合法的 ,即以 作为结尾的最长子序列长度为 :- 若存在且
,则将 更新为 ,因为它肯定比之前找到的 更优(从字典序上来看); - 否则,抛弃
,不做任何操作。
- 若存在且
综上所述,求最小字典序的最长上升子序列的算法为求最长上升子序列的长度的算法基础上加一次
下面两个算法中给出的代码实现都是求
1234567function getMinimumLexicographicOrderLIS(nums: number[]): number[] { const idx: number = findMinimumLexicographicOrderLIS( nums, (x, y) => x - y ) return idx.map(id => nums[id])}
动态规划
¶1234567891011121314151617181920212223242526export function findMinimumLexicographicOrderLIS( N: number, cmp: (i: number, j: number) => -1 | 0 | 1 | number,): number[] { if (N <= 0) return []
const dp: number[] = new Array(N) for (let j = 0, i: number, count: number; j < N; ++j) { for (i = 0, count = 0; i < j; ++i) { if (cmp(i, j) < 0) { if (count < dp[i]) count = dp[i] } } dp[j] = count + 1 }
const results: number[] = new Array(dp[N - 1]).fill(-1) for (let i = N - 1; i >= 0; --i) { const idx = dp[i] - 1 if (idx + 1 < results.length) { if (results[idx + 1] === -1 || cmp(i, results[idx + 1]) >= 0) continue } results[idx] = i } return results}
单调栈优化
¶需要额外维护一个数组(对应下面代码中的 rank 数组),其含义与前面动态规划解法的
dp 数组类似:
12345678910111213141516171819202122232425262728import { lowerBound } from '@algorithm.ts/binary-search'
type ICompare<T> = (x: number, y: number) => number
export function findMinimumLexicographicOrderLIS(N: number, cmp: ICompare<number>): number[] { if (N <= 0) return []
const rank: number[] = new Array(N) const stack: number[] = [] for (let i = 0; i < N; ++i) { const idx: number = lowerBound(0, stack.length, x => cmp(stack[x], i)) rank[i] = idx
// Update the monotonous stack. if (idx < stack.length) stack[idx] = i else stack.push(i) }
stack.fill(-1) for (let i = N - 1; i >= 0; --i) { const idx = rank[i] if (idx + 1 < stack.length) { if (stack[idx + 1] === -1 || cmp(i, stack[idx + 1]) >= 0) continue } stack[idx] = i } return stack}
最长非严格递增子序列
¶和上升子序列相比只改变了一个条件,即目标序列中相邻元素不再是严格小于,而是小于等于。不难得到新的状态转移方程:
观察转移方程的变化,不难想到可以通过修改 cmp 函数的定义(也就是把“小于等于”都视作小于)来支持上文提到的算法求解最长严格非递增子序列问题,如:
123456export function findLengthOfLNIS( N: number, cmp: (i: number, j: number) => -1 | 0 | 1 | number,): number { return findLengthOfLIS(nums, (i, j) => cmp(i, j) <= 0 ? -1 : 1) }
动态规划
¶1234567891011121314151617export function findLengthOfLNIS( N: number, cmp: (i: number, j: number) => -1 | 0 | 1 | number,): number { if (N <= 0) return 0
const dp: number[] = new Array(N) for (let j = 0, i: number, count: number; j < N; ++j) { for (i = 0, count = 0; i < j; ++i) { if (cmp(i, j) <= 0) { if (count < dp[i]) count = dp[i] } } dp[j] = count + 1 } return dp[N - 1]}
单调栈优化
¶只要把 lowerBound 改成 upperBound 就好了。
1234567891011121314151617import { upperBound } from '@algorithm.ts/binary-search'
type ICompare<T> = (x: number, y: number) => number
export function findLengthOfLNIS(N: number, cmp: ICompare<number>): number { if (N <= 0) return 0
const stack: number[] = [0] for (let i = 1; i < N; ++i) { const idx: number = upperBound(0, stack.length, x => cmp(stack[x], i))
// Update the monotonous stack. if (idx < stack.length) stack[idx] = i else stack.push(i) } return stack.length}
Related
¶