最长递增子序列的个数 图解题解
这道题到底在问什么
- 输入
- nums=[1,2,4,3,5,7,6,8]
- 输出
- 4
最优解:为什么这么做
一句话答案:LeetCode 673 最长递增子序列的个数在 LIS 的 O(n²) 动态规划上再挂计数数组 cnt:len[i] 记以 i 结尾的最长长度,cnt[i] 记达该长度的方案数,更长重置、等长累加,最后把长度最大处的 cnt 相加。
673 到底在数什么
给整数数组 nums,在它所有严格递增的子序列里挑最长的,问有几条。子序列可跳着挑但保持原顺序,严格递增指后一个比前一个大。以 nums=[1,2,4,3,5,7,6,8] 为例,最长长度 6,凑出它的走法有 4 条,答案就是 4。
普通 LIS 只算长度,个数从哪来
普通最长递增子序列(LIS)用一维动态规划:len[i] 表示以下标 i 结尾的最长上升子序列长度,往前找所有比 nums[i] 小的 nums[j],取 len[i]=max(len[j]+1)。只答多长、不答条数。
于是再开 cnt 数组,cnt[i] 记:以 i 结尾、长度恰等于 len[i] 的子序列有几条。cnt 全初始化为 1,每个数自成长度 1 的子序列。
cnt 更新为什么分「重置」和「累加」两种
递推 cnt[i] 时,每遇到能接的 nums[j](nums[j] 小于 nums[i]),看 len[j]+1 与 len[i] 谁大,这一步是重置还是累加,全看它。
len[j]+1 大于 len[i]:经 j 接出更长的子序列,原先较短方案作废。于是 len[i]=len[j]+1,cnt[i] 改写成 cnt[j]——是覆盖不是累加,新长度的条数只由 j 提供。
len[j]+1 等于 len[i]:经 j 接出的长度与已记最长相等,于是 cnt[i]+=cnt[j]。一句话:更长重置、等长累加。len[j]+1 小于 len[i] 更短,跳过。
拿 [1,2,4,3,5,7,6,8] 亲手算一遍
下标 0 到 7。前几格顺推:起手 len、cnt 都是 1;2 接 1,len=2、cnt=1;4 接 2,len=3、cnt=1;3 接 2,len=3、cnt=1。这几步都是长度加一、cnt 继承 1,还没碰上抉择。
下标 4 的数 5 最能看清两种更新。接 4(len=3)得 len[4]=4,cnt[4] 先改写成 cnt[2]=1;再接 3(len 也 3)也得 4,与 len[4] 相等,累加 cnt[4]+=cnt[3]=1,得 cnt[4]=2。
往后:7 接 5,len=5、cnt=2;6 接 5,len=5、cnt=2。8 接 7、6(都 len=5)得 len=6:先由 7 定 len[7]=6、cnt[7]=2,再遇等长的 6 累加 cnt[7]+=cnt[6]=2,得 cnt[7]=4。填完 len=[1,2,3,3,4,5,5,6]、cnt=[1,1,1,1,2,2,2,4]。
答案为什么要把多个 cnt 加起来
最长长度是 len 的最大值 6,本例只在下标 7,把该处 cnt 相加即答案 4。之所以相加:最长子序列可在不同位置结尾,凡 len[i] 等于最大值的下标都要计入 cnt;数据一变常散几处,只读末格就漏。
复杂度:外层每个 i、内层往前扫 j,O(n²);只用 len、cnt 两个数组,空间 O(n)。两处最容易写反:更长时把 cnt[i]=cnt[j] 误写成 +=,混入作废方案;只返回末格 cnt,漏掉别处的结尾。
▶ 动画逐步走查(共 21 步)——想跟着动画一帧帧对照就展开
- 3核心两条:更长则继承、等长则累加。下面每格都在套它。
- 4上行是原数组 nums(固定),下面两行 len、cnt 待填。每个数结尾至少能自成长度 1、且就 1 条。
- 5算 dp[0](数 1):往前看,没有比它小的数能接,只能自己起头,len=1、cnt=1。
- 6dp[0] 落子:len[0]=1、cnt[0]=1(1 自成一条)。
- 7算 dp[1](数 2):往前找到比它小的 1(蓝格)。逐个比 len[j]+1:更长就把 len 换大、cnt 抄过来;等长就把 cnt 累加上去。
- 8dp[1] 落子:len[1]=2,cnt[1]=1(接在 1 后面,方案数继承自它)。
- 9算 dp[2](数 4):往前找到比它小的 1、2(蓝格)。逐个比 len[j]+1:更长就把 len 换大、cnt 抄过来;等长就把 cnt 累加上去。
- 10dp[2] 落子:len[2]=3,cnt[2]=1(接在 2 后面,方案数继承自它)。
- 11算 dp[3](数 3):往前找到比它小的 1、2(蓝格)。逐个比 len[j]+1:更长就把 len 换大、cnt 抄过来;等长就把 cnt 累加上去。
- 12dp[3] 落子:len[3]=3,cnt[3]=1(接在 2 后面,方案数继承自它)。
- 13算 dp[4](数 5):往前找到比它小的 1、2、4、3(蓝格)。逐个比 len[j]+1:更长就把 len 换大、cnt 抄过来;等长就把 cnt 累加上去。
- 14dp[4] 落子:len[4]=4。有 2 个来路都给出长度 4(4、3),方案数累加为 cnt[4]=2。
- 15算 dp[5](数 7):往前找到比它小的 1、2、4、3、5(蓝格)。逐个比 len[j]+1:更长就把 len 换大、cnt 抄过来;等长就把 cnt 累加上去。
- 16dp[5] 落子:len[5]=5,cnt[5]=2(接在 5 后面,方案数继承自它)。
- 17算 dp[6](数 6):往前找到比它小的 1、2、4、3、5(蓝格)。逐个比 len[j]+1:更长就把 len 换大、cnt 抄过来;等长就把 cnt 累加上去。
- 18dp[6] 落子:len[6]=5,cnt[6]=2(接在 5 后面,方案数继承自它)。
- 19算 dp[7](数 8):往前找到比它小的 1、2、4、3、5、7、6(蓝格)。逐个比 len[j]+1:更长就把 len 换大、cnt 抄过来;等长就把 cnt 累加上去。
- 20dp[7] 落子:len[7]=6。有 2 个来路都给出长度 6(7、6),方案数累加为 cnt[7]=4。
- 21两行都填好了。下一步:先在 len 行里找出最大值,再数有哪些格子达到它。
- 22扫一遍 len 行(蓝格),最大值是 6——这就是最长递增子序列的长度。
- 23len 等于 6 的格子有 1 个(绿色 cnt),把它们的 cnt 相加:4=4。这就是最长递增子序列的条数。
⚠️ 容易写错的地方
✗ 错:只数最后一格 cnt
✓ 对:把所有 len 等于最大值的 cnt 都加上
最长子序列可能在多个位置结尾
✗ 错:更长时忘了重置 cnt
✓ 对:更长要 cnt[i]=cnt[j](覆盖,不是累加)
旧的较短方案已作废
✗ 错:等长时写成 cnt[i]=cnt[j]
✓ 对:等长要 cnt[i]+=cnt[j]
多条同长路线都要计入
完整代码(Python / C++ / Java)
Python
def findNumberOfLIS(nums):
n = len(nums)
length = [1] * n
count = [1] * n
for i in range(n):
for j in range(i):
if nums[j] < nums[i]:
if length[j] + 1 > length[i]:
length[i] = length[j] + 1
count[i] = count[j]
elif length[j] + 1 == length[i]:
count[i] += count[j]
mx = max(length)
return sum(c for l, c in zip(length, count) if l == mx)C++
int findNumberOfLIS(vector<int>& nums){
int n = nums.size();
vector<int> len(n, 1), cnt(n, 1);
int mx = 0;
for(int i = 0; i < n; i++){
for(int j = 0; j < i; j++) if(nums[j] < nums[i]){
if(len[j] + 1 > len[i]){ len[i] = len[j] + 1; cnt[i] = cnt[j]; }
else if(len[j] + 1 == len[i]) cnt[i] += cnt[j];
}
mx = max(mx, len[i]);
}
int ans = 0;
for(int i = 0; i < n; i++) if(len[i] == mx) ans += cnt[i];
return ans;
}Java
int findNumberOfLIS(int[] nums){
int n = nums.length, mx = 0, ans = 0;
int[] len = new int[n], cnt = new int[n];
for(int i = 0; i < n; i++){
len[i] = 1; cnt[i] = 1;
for(int j = 0; j < i; j++) if(nums[j] < nums[i]){
if(len[j] + 1 > len[i]){ len[i] = len[j] + 1; cnt[i] = cnt[j]; }
else if(len[j] + 1 == len[i]) cnt[i] += cnt[j];
}
mx = Math.max(mx, len[i]);
}
for(int i = 0; i < n; i++) if(len[i] == mx) ans += cnt[i];
return ans;
}复杂度
时间
O(n²)
每个 i 往前扫一遍
空间
O(n)
len、cnt 两个数组
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 最长递增子序列的个数 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么发现更长时 cnt 要覆盖而不是累加?+
更长意味着原先记在 cnt[i] 里的都是较短方案,长度被刷新、对新长度没有贡献,必须整批丢掉,只留来自 j 的条数,所以写 cnt[i]=cnt[j]。若这里也累加,会把作废的短方案混进新长度,结果偏大;等长才累加。
它和最长递增子序列 LeetCode 300 是什么关系?+
300 只问最长长度,本题在它之上多维护一个 cnt 数组数方案。len 的递推和 300 一样,cnt 是加装的一层。会了 300,只需想清「更长重置、等长累加」,再把长度等于最大值处的 cnt 汇总即可。
能优化到 O(n log n) 吗?+
能。用树状数组或线段树按数值维护「某长度对应的方案数」,把内层线性往前扫降到 O(log n),整体 O(n log n)。代价是实现更复杂,面试里 O(n²) 版通常够用,先把两数组递推写清再谈优化。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 最长递增子序列的个数 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。