题目描述
思路解析
一句话答案: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,漏掉别处的结尾。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
核心两条:更长则继承、等长则累加。下面每格都在套它。
上行是原数组 nums(固定),下面两行 len、cnt 待填。每个数结尾至少能自成长度 1、且就 1 条。
算 dp[0](数 1):往前看,没有比它小的数能接,只能自己起头,len=1、cnt=1。
dp[0] 落子:len[0]=1、cnt[0]=1(1 自成一条)。
算 dp[1](数 2):往前找到比它小的 1(蓝格)。逐个比 len[j]+1:更长就把 len 换大、cnt 抄过来;等长就把 cnt 累加上去。
dp[1] 落子:len[1]=2,cnt[1]=1(接在 1 后面,方案数继承自它)。
算 dp[2](数 4):往前找到比它小的 1、2(蓝格)。逐个比 len[j]+1:更长就把 len 换大、cnt 抄过来;等长就把 cnt 累加上去。
dp[2] 落子:len[2]=3,cnt[2]=1(接在 2 后面,方案数继承自它)。
算 dp[3](数 3):往前找到比它小的 1、2(蓝格)。逐个比 len[j]+1:更长就把 len 换大、cnt 抄过来;等长就把 cnt 累加上去。
dp[3] 落子:len[3]=3,cnt[3]=1(接在 2 后面,方案数继承自它)。
算 dp[4](数 5):往前找到比它小的 1、2、4、3(蓝格)。逐个比 len[j]+1:更长就把 len 换大、cnt 抄过来;等长就把 cnt 累加上去。
dp[4] 落子:len[4]=4。有 2 个来路都给出长度 4(4、3),方案数累加为 cnt[4]=2。
算 dp[5](数 7):往前找到比它小的 1、2、4、3、5(蓝格)。逐个比 len[j]+1:更长就把 len 换大、cnt 抄过来;等长就把 cnt 累加上去。
dp[5] 落子:len[5]=5,cnt[5]=2(接在 5 后面,方案数继承自它)。
算 dp[6](数 6):往前找到比它小的 1、2、4、3、5(蓝格)。逐个比 len[j]+1:更长就把 len 换大、cnt 抄过来;等长就把 cnt 累加上去。
dp[6] 落子:len[6]=5,cnt[6]=2(接在 5 后面,方案数继承自它)。
算 dp[7](数 8):往前找到比它小的 1、2、4、3、5、7、6(蓝格)。逐个比 len[j]+1:更长就把 len 换大、cnt 抄过来;等长就把 cnt 累加上去。
dp[7] 落子:len[7]=6。有 2 个来路都给出长度 6(7、6),方案数累加为 cnt[7]=4。
两行都填好了。下一步:先在 len 行里找出最大值,再数有哪些格子达到它。
扫一遍 len 行(蓝格),最大值是 6——这就是最长递增子序列的长度。
len 等于 6 的格子有 1 个(绿色 cnt),把它们的 cnt 相加:4=4。这就是最长递增子序列的条数。
边界先想清,尤其全相等时答案是元素个数。
两个高频追问。
参考代码
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)复杂度
- 时间:O(n²),每个 i 往前扫一遍
- 空间:O(n),len、cnt 两个数组
易错点
面试追问把动画讲成自己的话
追问为什么 cnt[i] 初始化为 1?
追问能优化到 O(n log n) 吗?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
两个字符串的最小 ASCII 删除和
LeetCode 712 · 中等 · 沿着 动态规划套路 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题