力扣 1621. 大小为K的不重叠线段的数目:动态规划(Java 实现)

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 拆分为两种互斥情况:

  1. 第 jjj 号点不作为任何线段右端点 :方案等价于 0,j−10,j-10,j−1 选 iii 条线段,即 dpij−1dpij-1dpij−1
  2. 第 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与一维滚动数组对比

  1. 底层数学逻辑完全一致:状态转移、前缀和累加、取模规则都没有改动,最终输出答案完全相同。一维只是对二维DP做空间压缩优化,没有改变计数思路;
  2. 空间开销大幅下降 :二维 O(kn)O(kn)O(kn) → 一维 O(n)O(n)O(n),当 kkk 取值较大时优化效果明显;
  3. 二维数组状态直观,适合新手用来理解DP推导;一维滚动数组牺牲了一部分可读性,换来更低的内存占用,适合工程提交。

复杂度分析

  • 时间复杂度:O(k×n)O(k\times n)O(k×n),和二维DP保持不变,两层循环,前缀和优化消除内层求和循环;
  • 空间复杂度:O(n)O(n)O(n),只维护dppre两个长度为n的一维数组。
相关推荐
君顾11 小时前
本地托管机构管理源码实战:需求拆解、数据建模与多端部署
java·开发语言·托管
青山木1 小时前
Hot 100 --- 最长有效括号
java·数据结构·算法·leetcode·动态规划
xcl09251 小时前
本地电竞服务交易系统架构设计与实战:从同城匹配到订单履约
java·spring boot
曹牧1 小时前
SVN 上查找文件
java
加贝哥|usun2 小时前
maven项目从外网搬迁至内网-本地仓库(不搭建私服)
java
paopaokaka_luck2 小时前
小学非遗科普平台(AI 非遗科普问答,ECharts学情数据、非遗资源分类与审核,资源收藏下载与学习记录,学习任务发布和作品评分,活动报名签到与统计)
java·人工智能·spring·信息可视化·数据分析·echarts
这料鬼有毒3 小时前
二刷hot100-1143.最长公共子序列
算法·leetcode·动态规划
Yize.3 小时前
Spring Task定时任务全解析:从入门到实战
java·spring
政企项目老覃3 小时前
边缘 AI 推理部署:安防零售场景下的模型裁剪与端侧落地实践
人工智能·程序人生·算法·性能优化·vllm