LeetCode 1621 大小为 K 的不重叠线段的数目是计数动态规划的经典标杆题,也是面试中考察状态定义、前缀和优化以及组合计数能力的核心考点。这道题的核心是破解 "在数轴上选出恰好 k 条不重叠线段,统计全部合法方案数量" 的问题,题目要求对结果取模,涉及大数下的计数运算。
针对这道题目存在三条递进的解题路径:二维 DP + 前缀和优化、一维滚动数组 DP 空间压缩直接计算。我们会从最直观的二维动态规划入手,一步步理清状态定义与状态转移的推导逻辑;随后基于二维 DP 进一步做空间压缩,推出一维滚动数组写法。本文覆盖两种解法,由浅入深,适合入门计数 DP,并且给出可直接运行的 Java 代码,帮你从原理到实现彻底啃下这道题

解法一:二维DP + 前缀和优化
1. 状态定义
定义:
dpijdpijdpij
含义:在区间 0,j0,j0,j 的点中,选出恰好 iii 条合法不重叠线段的方案总数。
最终答案:dpkn−1dpkn-1dpkn−1,也就是完整区间 0,n−10,n-10,n−1 选出 kkk 条线段的总方案。
2. 状态转移推导
dpijdpijdpij 拆分为两种互斥情况:
- 第 jjj 号点不作为任何线段右端点 :方案等价于 0,j−10,j-10,j−1 选 iii 条线段,即 dpij−1dpij-1dpij−1。
- 第 jjj 号点作为某条线段右端点 :线段为 p,jp,jp,j,p∈0,j−1p\in0,j-1p∈0,j−1;前面区间 0,p0,p0,p 需要选 i−1i-1i−1 条线段,所有p的方案求和:
∑p=0j−1dpi−1p\sum_{p=0}^{j-1} dpi-1pp=0∑j−1dpi−1p
合并得到转移方程:
dpij=dpij−1+∑p=0j−1dpi−1pdpij = dpij-1 + \sum_{p=0}^{j-1}dpi-1pdpij=dpij−1+p=0∑j−1dpi−1p
直接循环求和复杂度 O(kn2)O(kn^2)O(kn2)。引入
sum维护前缀和,将内层求和降为 O(1)O(1)O(1),整体复杂度 O(kn)O(kn)O(kn)。
3. 初始化
i=0i=0i=0,选取0条线段:无论区间多长,不选线段只有1种方案。
dp0j=1,j∈0,n−1dp0j=1,\quad j\in0,n-1dp0j=1,j∈0,n−1
边界:j=0j=0j=0 只有单个点,无法构成线段,选大于0条线段方案数为0。Java数组默认值为0,无需额外赋值。
4. Java完整代码
java
class Solution {
// dp[i][j]表示[0,j]中i个线段的答案
int[][] dp;
// 当答案特别大的时候,存不下会直接溢出变成负数,所以一般会要求算出一个中间结果
static final int MOD = 1000000007;
public int numberOfSets(int n, int k) {
// dp[k][n-1]表示[0,n-1]中k个线段的答案
dp = new int[k + 1][n];
// 当i = 0时,表示 [0 , j]中0个线段的答案,这个答案一定是1
for (int j = 0 ; j < n ; j++){
dp[0][j] = 1;
}
// dp[i][j] = dp[i][j - 1]([0 ,j - 1]中i个线段的答案,也就是 [j - 1 , j]无线段)
// + dp[i - 1][0] + dp[i - 1][1] + ... + dp[i - 1][j - 1]
//在前半段[0 , p]寻找i - 1个线段,加上后半段[p , j]一个线段
//可以看到dp[i][j]的计算必须要先等到dp[i - 1]完全计算好,然后dp[i][0]往dp[i][j]计算
// i 从1开始,0已经在上面初始化过了
for (int i = 1 ; i <= k ; i++){
// 这里的sum时dp[i - 1][0] + ... dp[i - 1][j - 1]的和,这样就不需要一直重复相加
int sum = 0;
// j = 0 不用计算,在[0 , 0]中寻找大于0的线段肯定是0
for (int j = 1 ; j < n ; j++){
sum = (sum + dp[i - 1][j - 1]) % MOD;
dp[i][j] = (dp[i][j - 1] + sum) % MOD;
}
}
return dp[k][n - 1];
}
}
5. 取模与溢出说明
计数DP持续累加方案数,int存在数值上限,累加过大时会发生整型溢出,得到负数错误答案。
所以每一步加法后,都对 MOD=109+7MOD=10^9+7MOD=109+7 取模,将数值限制在int安全范围内。
6. 复杂度
- 时间:O(k×n)O(k\times n)O(k×n)
- 空间:O(k×n)O(k\times n)O(k×n),二维数组。下面一维DP会做空间压缩。
解法二:一维滚动数组 DP(空间压缩)
优化思路回顾
在上一节二维DP方案中,我们回顾状态转移方程:
dpij=dpij−1+∑p=0j−1dpi−1pdpij = dpij-1+\sum_{p=0}^{j-1}dpi-1pdpij=dpij−1+p=0∑j−1dpi−1p
观察依赖关系:计算 KaTeX parse error: Expected 'EOF', got '}' at position 8: dpij}̲ 时,**仅需要 iii 这一层前面的 dp\[ij−1dpij-1dpij−1,以及上一层 i−1i-1i−1 的全部 pre...pre...pre... 数据** ,更早的 i−2,i−3i-2,i-3i−2,i−3 等历史层不会再参与后续计算。
因此我们不再需要完整的二维数组,只维护两个一维数组:
pre[]:保存上一层,代表区间 0,j0,j0,j 选出 i−1i-1i−1 条线段的方案数;dp[]:保存当前层,代表区间 0,j0,j0,j 选出 iii 条线段的方案数。
循环计算完当前层dp之后,将dp赋值给pre,滚动迭代,以此把空间复杂度由二维的 O(kn)O(kn)O(kn) 压缩到 O(n)O(n)O(n)。
状态定义
pre[j]:区间 0,j0,j0,j 选出恰好 i−1i-1i−1 条不重叠线段的方案总数dp[j]:区间 0,j0,j0,j 选出恰好 iii 条不重叠线段的方案总数
转移方程:
dpj=dpj−1+∑p=0j−1prepdpj = dpj-1+\sum_{p=0}^{j-1}prepdpj=dpj−1+p=0∑j−1prep
使用变量sum维护pre数组的前缀和,避免每次内层循环重复求和,保证时间复杂度不变。
初始化逻辑
i=0i=0i=0 代表选取0条线段。无论区间 0,j0,j0,j 包含多少个点,不选线段只有唯一1种方案。因此初始时将pre数组全部置1。
Java代码实现
java
class Solution {
// dp[i][j] 只和dp[i - 1][...]和dp[i - 1][j - 1]有关
// 所以可以压缩成两个数组,不再使用二维数组
int[] dp;
int[] pre;
// 当答案特别大的时候,存不下会直接溢出变成负数,所以一般会要求算出一个中间结果
static final int MOD = 1000000007;
public int numberOfSets(int n, int k) {
// pre[j] 表示[0 , j]中i - 1个线段的答案
pre = new int[n];
// 当i = 0时,表示 [0 , j]中0个线段的答案,这个答案一定是1
for (int j = 0 ; j < n ; j++){
pre[j] = 1;
}
// dp[j] = dp[j - 1]([0 ,j - 1]中i个线段的答案,也就是 [j - 1 , j]无线段)
// + pre[0] + pre[1] + ... + pre[j - 1]
// 在前半段[0 , p]寻找i - 1个线段,加上后半段[p , j]一个线段
// i 从1开始,0已经在上面初始化过了
for (int i = 1 ; i <= k ; i++){
// dp[j]表示[0,j]中i个线段的答案
dp = new int[n];
// 这里的sum时pre[0] + ... pre[j - 1]的和,这样就不需要一直重复相加
int sum = 0;
// j = 0 不用计算,在[0 , 0]中寻找大于0的线段肯定是0
for (int j = 1 ; j < n ; j++){
sum = (sum + pre[j - 1]) % MOD;
dp[j] = (dp[j - 1] + sum) % MOD;
}
pre = dp;
}
return pre[n - 1];
}
}
二维DP与一维滚动数组对比
- 底层数学逻辑完全一致:状态转移、前缀和累加、取模规则都没有改动,最终输出答案完全相同。一维只是对二维DP做空间压缩优化,没有改变计数思路;
- 空间开销大幅下降 :二维 O(kn)O(kn)O(kn) → 一维 O(n)O(n)O(n),当 kkk 取值较大时优化效果明显;
- 二维数组状态直观,适合新手用来理解DP推导;一维滚动数组牺牲了一部分可读性,换来更低的内存占用,适合工程提交。
复杂度分析
- 时间复杂度:O(k×n)O(k\times n)O(k×n),和二维DP保持不变,两层循环,前缀和优化消除内层求和循环;
- 空间复杂度:O(n)O(n)O(n),只维护
dp与pre两个长度为n的一维数组。