跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++算法

子序列 DP 实战:从基础 LIS 到二维状态转移

梳理了最长递增子序列(LIS)模型及其衍生题型,包括摆动序列、计数、数对链、定差子序列、斐波那契子序列和等差数列。重点展示了从一维 dp[i] 向二维 dp[i][j] 的状态升维技巧,以及用哈希表优化定差场景的方法。所有思路均用 C++ 实现,适合巩固线性 DP 的建模能力,并为后续回文串等区间 DP 做铺垫。

禅心发布于 2026/6/7更新于 2026/9/427 浏览
子序列 DP 实战:从基础 LIS 到二维状态转移

子序列问题不像子数组那样要求连续,所以我们选数时得带着'往回看'的思维。LIS(最长递增子序列)是最经典的模板,很多变种都是由它衍生出来的。状态定义往往是 dp[i] 表示以 i 位置结尾的最优解;转移的时候因为允许跳过元素,通常需要一个双层循环 for i { for j < i }。但如果遇到一个数确定不了规律(比如等差、斐波那契),就可以直接升维到 dp[i][j] 表示以 i 和 j 结尾的状态。


最长递增子序列 (LIS)

题目链接:300. 最长递增子序列,找最长严格递增子序列的长度,比如 [10,9,2,5,3,7,101,18] 答案是 4([2,3,7,101])。

dp[i] 的含义就是必须以 nums[i] 结尾的最长递增子序列长度。怎么算?对于每个 i,往前扫一遍 j,只要 nums[j] < nums[i],就可以接在 j 后面,我们就拿 dp[j] + 1 去更新 dp[i]。初始值都是 1,因为单独一个元素也算。

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int n = nums.size();
        vector<int> dp(n, 1);
        int ret = 1;
        for (int i = 1; i < n; i++) {
            for (int j = 0; j < i; j++) {
                if (nums[j] < nums[i]) {
                    dp[i] = max(dp[i], dp[j] + 1);
                }
            }
            ret = max(ret, dp[i]);
        }
        return ret;
    }
};

这道题有更高效的 O(n log n) 贪心+二分法,但那个留到贪心专题再聊;先用 DP 把模型搞透,后面很多变种还要靠它。


摆动序列

题目链接:376. 摆动序列。给定序列,要找最长子序列,使得相邻两数之差正负交替,比如 [1,7,4,9,2,5] 可以得到长度 6。

只记录长度还不够,我们需要知道结尾的"姿势"——最后一步是涨还是跌,因为这会决定下一个数能不能接上。于是拆成两个数组:f[i] 表示以 i 结尾且最后一步是上升的最长摆动序列长度,g[i] 对应最后一步是下降的。

转移时,对于每个 j < i,如果 nums[j] < nums[i],说明现在上升,可以接在 g[j] 后面,f[i] = max(f[i], g[j] + 1);反过来如果 nums[j] > nums[i],现在下降,接在 f[j] 后面,g[i] = max(g[i], f[j] + 1)。

class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        int n = nums.size();
        vector<int> f(n, 1), g(n, 1);
        int ret = 1;
        for (int i = 1; i < n; i++) {
            for (int j = 0; j < i; j++) {
                if (nums[j] < nums[i]) {
                    f[i] = max(f[i], g[j] + 1);
                } else if (nums[j] > nums[i]) {
                    g[i] = max(g[i], f[j] + 1);
                }
            }
            ret = max(ret, max(f[i], g[i]));
        }
        return ret;
    }
};

这题也有 O(n) 的贪心,不过 DP 版本更容易套到其他类似于"需要记住上一个状态是涨还是跌"的问题里。


最长递增子序列的个数

题目链接:673. 最长递增子序列的个数。不仅要最长长度,还要问有多少个最长子序列。

显然只用一个 dp 数组不够了,需要再带一个计数数组。len[i] 还是以 i 结尾的最长长度,count[i] 是以 i 结尾的最长长度的方案数。遍历 j 时,如果 len[j] + 1 > len[i],表示找到了更长的序列,那就更新 len[i],并把 count[i] 置为 count[j](替代);如果 len[j] + 1 == len[i],表示发现了等长的不同序列,count[i] += count[j](累加)。最后把所有 len 等于全局最长的 count 加起来就是答案。

class Solution {
public:
    int findNumberOfLIS(vector<int>& nums) {
        int n = nums.size();
        vector<int> len(n, 1), count(n, 1);
        int maxLen = 1;
        for (int i = 1; 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;
                        count[i] = count[j];
                    } else if (len[j] + 1 == len[i]) {
                        count[i] += count[j];
                    }
                }
            }
            maxLen = max(maxLen, len[i]);
        }
        int ret = 0;
        for (int i = 0; i < n; i++) {
            if (len[i] == maxLen) ret += count[i];
        }
        return ret;
    }
};

最长数对链

题目链接:646. 最长数对链。输入是一对对 [a,b],链的条件是 b < c 才能接。这题几乎就是 LIS 的翻版,唯一不同是初始顺序可能乱。

先按数对的第一个元素排个序,然后就和 LIS 一模一样了:if pairs[j][1] < pairs[i][0] 则 dp[i] = max(dp[i], dp[j] + 1)。

class Solution {
public:
    int findLongestChain(vector<vector<int>>& pairs) {
        sort(pairs.begin(), pairs.end());
        int n = pairs.size();
        vector<int> dp(n, 1);
        int ret = 1;
        for (int i = 1; i < n; i++) {
            for (int j = 0; j < i; j++) {
                if (pairs[j][1] < pairs[i][0]) {
                    dp[i] = max(dp[i], dp[j] + 1);
                }
            }
            ret = max(ret, dp[i]);
        }
        return ret;
    }
};

最长定差子序列

题目链接:1218. 最长定差子序列。这题数据量到了 10^5,再用双层循环 O(n²) 会超时。但题目给的差值 difference 是固定的,这就好办了。对于当前数字 x,我们只关心 x - difference 结尾的最长长度是多少。用一个哈希表 hash[x] 表示以 x 结尾的最长长度,那么 hash[x] = hash[x - difference] + 1,一次遍历就结束。

class Solution {
public:
    int longestSubsequence(vector<int>& arr, int difference) {
        unordered_map<int, int> hash;
        int ret = 1;
        for (int x : arr) {
            hash[x] = hash[x - difference] + 1;
            ret = max(ret, hash[x]);
        }
        return ret;
    }
};

定差这个优化很实用,把原本 O(n²) 的 for-for 直接降成 O(n)。


最长的斐波那契子序列的长度

题目链接:873. 最长的斐波那契子序列的长度。要求子序列满足 x_i + x_{i+1} = x_{i+2}。一个数没法确定斐波那契数列,两个数才行。所以把状态升维:dp[i][j] 表示以 i 和 j(i < j)结尾的斐波那契子序列长度,初始值至少是 2。

固定 j,枚举 i,我们要找 target = arr[j] - arr[i] 且它必须在 i 之前。如果存在这样的下标 k,那么 dp[i][j] = dp[k][i] + 1。为了快速查找,预先建一个值到下标的映射。注意 target < arr[i] 这个剪枝能跳过一些不可能的数值(因为数列肯定递增)。

class Solution {
public:
    int lenLongestFibSubseq(vector<int>& arr) {
        int n = arr.size();
        unordered_map<int, int> idxMap;
        for (int i = 0; i < n; i++) idxMap[arr[i]] = i;
        
        vector<vector<int>> dp(n, vector<int>(n, 2));
        int ret = 0;
        
        for (int j = 2; j < n; j++) {
            for (int i = 1; i < j; i++) {
                int target = arr[j] - arr[i];
                if (target < arr[i] && idxMap.count(target)) {
                    int k = idxMap[target];
                    dp[i][j] = dp[k][i] + 1;
                    ret = max(ret, dp[i][j]);
                }
            }
        }
        return ret < 3 ? 0 : ret;
    }
};

最长等差数列

题目链接:1027. 最长等差数列。和斐波那契一样,两个数才能定公差。dp[i][j] 表示以 i、j 结尾的等差子序列长度,初始为 2。公差 diff = nums[j] - nums[i],前一个数应该是 target = nums[i] - diff。如果 target 出现过且下标 k 在 i 之前,就 dp[i][j] = dp[k][i] + 1。

为了避免重复元素干扰,我们可以在外循环 i 时动态维护一个哈希表,把遍历完的 i 才放进表里,这样保证取到的 k 一定在 i 之前。

class Solution {
public:
    int longestArithSeqLength(vector<int>& nums) {
        int n = nums.size();
        vector<vector<int>> dp(n, vector<int>(n, 2));
        int ret = 2;
        unordered_map<int, int> hash;
        
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                int target = 2 * nums[i] - nums[j]; // nums[i] - (nums[j] - nums[i])
                if (hash.count(target)) {
                    int k = hash[target];
                    dp[i][j] = dp[k][i] + 1;
                }
                ret = max(ret, dp[i][j]);
            }
            hash[nums[i]] = i;
        }
        return ret;
    }
};

等差数列划分 II - 子序列(Hard)

题目链接:446. 等差数列划分 II - 子序列。这次要求所有等差子序列的个数,不是长度。仍然用 dp[i][j],但表示以 i、j 结尾的等差数列个数。找到 k 之后,dp[i][j] += dp[k][i] + 1(那个 +1 是 [k,i,j] 这一组新序列)。因为可能存在多个相同的 target 值,所以哈希表要存下标列表,遍历所有满足 k < i 的 k 累加。

class Solution {
public:
    int numberOfArithmeticSlices(vector<int>& nums) {
        int n = nums.size();
        long long ans = 0;
        vector<vector<int>> dp(n, vector<int>(n, 0));
        
        unordered_map<long long, vector<int>> map;
        for (int i = 0; i < n; i++) map[nums[i]].push_back(i);
        
        for (int j = 1; j < n; j++) {
            for (int i = 0; i < j; i++) {
                long long target = 2LL * nums[i] - nums[j];
                if (map.count(target)) {
                    for (int k : map[target]) {
                        if (k < i) {
                            dp[i][j] += dp[k][i] + 1;
                        } else {
                            break;
                        }
                    }
                }
                ans += dp[i][j];
            }
        }
        return (int)ans;
    }
};

最后这题有点难,但思路还是前面二维 DP 的延伸,只不过从求长度变成了求计数。


总结

子序列 DP 的核心就是'不连续',因此经典套路是 dp[i] 回顾前面所有的 j。当单个下标定不住规律时,升维到 dp[i][j] 用两个位置来确定状态(比如等差需要确定公差,斐波那契需要确定前两个数)。遇到定差这类问题,利用哈希表直接映射可以把复杂度压到线性。这些技巧组合起来,能解决大部分线性 DP 的子序列问题。下一篇我们会进入回文串专题,那是区间 DP 的起点。

目录

  1. 最长递增子序列 (LIS)
  2. 摆动序列
  3. 最长递增子序列的个数
  4. 最长数对链
  5. 最长定差子序列
  6. 最长的斐波那契子序列的长度
  7. 最长等差数列
  8. 等差数列划分 II - 子序列(Hard)
  9. 总结

更多推荐文章

查看全部
  • 数据结构核心:树与二叉树详解
  • 从命令行到自动诊断:构建 AI 驱动的故障树与交互式排障系统
  • Trae 安装与上手指南
  • Arduino BLDC 四足仿生穿越机器人设计与实现
  • ARIS 开源:基于 Claude Code 的全自动科研与论文工作流
  • 检索增强生成(RAG)技术原理与核心范式详解
  • Python 基础入门:数据存储与运算
  • RabbitMQ 安装与运维指南:从部署到高级配置
  • OpenClaw 助手 20 个精选 Skills 配置与实战指南
  • 7 种大模型微调方法详解:LoRA、QLoRA 及适配器技术
  • 大疆无人机开发入门:MSDK、PSDK 与上云 API 技术解析
  • Agent 的九种设计模式详解:原理、图解与代码实现
  • 多旋翼物流无人机节能轨迹规划及 Python 实现
  • 基于 Coze 抓取小红书视频并同步至飞书多维表实战
  • 如何用AI生成带文字的海报?Ideogram v3 2026最强文字渲染教程
  • 在 VSCode 中配置并使用 Clang-Format 进行 C++ 代码自动格式化
  • MCP 协议详解:与 Function Call 的区别及实战用法
  • 文心一言 4.5 开源模型深度解析:轻量化部署与中文场景优化
  • OpenCode:开源免费的 AI 编程智能体介绍
  • Visual Studio 中基于 libmodbus 的 C++ Modbus RTU 主站示例

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online