最长递增子序列
最长递增子序列的状态如何定义?
- 问题: 求数组中最长严格递增子序列的长度 (300);
- 阶段: 按子序列末端索引划分;
- 状态:
dp[i]为以nums[i]结尾的最长递增子序列长度; - 初值: 每个元素自身构成长度 1, 故
dp全填 1; - 答案:
max(dp), 不一定是dp[n-1];
如何写出转移方程?
- 推导: 对每个
0 <= j < i, 若nums[i] > nums[j], 可以把nums[i]续在dp[j]之后; - 方程:
dp[i] = max(dp[i], dp[j] + 1), 条件nums[j] < nums[i]; - 复杂度: 时间
O(n^2), 空间O(n);
const lengthOfLIS = (nums) => {
const dp = new Array(nums.length).fill(1);
for (let i = 1; i < nums.length; i++) {
for (let j = 0; j < i; j++) {
if (nums[i] > nums[j]) dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
return Math.max(...dp);
};
如何同时统计最长递增子序列的个数?
- 问题: 统计最长递增子序列的方案数 (673);
- 状态: 在
dp[i]之外再维护count[i], 表示以nums[i]结尾的最长递增子序列个数; - 转移分三种情况:
dp[j] + 1 > dp[i]: 首次得到更长长度,dp[i] = dp[j] + 1,count[i] = count[j];dp[j] + 1 === dp[i]: 长度相同, 方案累加count[i] += count[j];dp[j] + 1 < dp[i]: 不更新;
- 初值:
dp全 1,count全 1; - 答案: 先取
max = max(dp), 再把所有dp[i] === max的count[i]相加;
const findNumberOfLIS = (nums) => {
const dp = new Array(nums.length).fill(1);
const count = new Array(nums.length).fill(1);
for (let i = 1; i < nums.length; i++) {
for (let j = 0; j < i; j++) {
if (nums[i] <= nums[j]) continue;
if (dp[j] + 1 > dp[i]) {
dp[i] = dp[j] + 1;
count[i] = count[j];
} else if (dp[j] + 1 === dp[i]) {
count[i] += count[j];
}
}
}
const max = Math.max(...dp);
return dp.reduce((sum, len, i) => (len === max ? sum + count[i] : sum), 0);
};
俄罗斯套娃信封问题怎么转化为最长递增子序列?
- 问题: 信封有宽高, 宽高都严格更小时才能套入, 求最多层数 (354);
- 排序: 宽度升序; 宽度相同时高度降序;
- 原因: 同宽信封不能互相套入, 高度降序保证同宽内不会形成递增关系, 从而只按高度取递增即可;
- 转化: 对排序后的高度序列求最长递增子序列;
- 注意: 直接套
O(n^2)会超时, 需要二分贪心的O(n log n)写法;
为什么贪心加二分可以把复杂度降到 O(n log n)?
- 维护: 数组
tails,tails[k]为长度k+1的递增子序列的最小结尾值; - 贪心: 结尾越小, 后续越容易延长, 所以只保留每种长度的最小结尾;
- 二分: 新元素
x大于tails末尾就追加; 否则找到第一个大于等于x的位置并替换它; - 结果:
tails.length就是最长递增子序列长度; - 代价: 只得到长度, 无法还原具体子序列;
const lengthOfLISFast = (nums) => {
const tails = [];
for (const x of nums) {
let left = 0;
let right = tails.length;
while (left < right) {
const mid = (left + right) >> 1;
if (tails[mid] < x) left = mid + 1;
else right = mid;
}
tails[left] = x;
}
return tails.length;
};