动态规划1——线性动态规划

一、线性动态规划的核心概念

1.1 什么是线性动态规划

线性动态规划(Linear DP)是动态规划的一种基本形式,其特点是状态沿着线性序列(如数组或字符串)单向推进。这类问题通常具有以下特征:

  • 单维/双维状态空间:状态通常表示为 dp[i] 或 dp[i][j]

  • 线性依赖关系:当前状态仅依赖于序列中前一个或多个位置的状态

  • 无后效性:当前状态确定后,后续状态发展不受之前状态影响

1.2 基本解题框架

// 1. 初始化状态数组
vector<int> dp(n, 0); 

// 2. 设置边界条件
dp[0] = base_case; 

// 3. 状态转移(核心)
for(int i = 1; i < n; i++) {
    dp[i] = transition(dp[...]); // 状态转移方程
}

// 4. 获取最终结果
return dp[n-1] or max(dp);

二、经典例题精讲

2.1 最大子段和(Maximum Subarray)

问题描述:给定整数数组,求连续子数组的最大和。
输入格式:第一行是一个正整数N,表示了序列的长度。(N≤200000)
                第二行是用空格分开的N个整数,表示这个序列
输出格式:一个整数,为最大的子段和是多少,子段的最小长度为1
输入样例:7
2  -4   3   -1   2  -4  3
输出样例:4

状态定义

  • dp[i]:以第 i 个元素结尾的最大子数组和

状态转移方程

dp[i] = max(dp[i-1] + nums[i], nums[i])

推导过程

  • 选项1:将当前元素加入前一个子数组(dp[i-1] + nums[i]

  • 选项2:从当前元素开始新子数组(nums[i]

  • 取两者最大值作为新状态

代码实现

int maxSubarraySum(int nums[], int n) {
    int dp[100];
    dp[0] = nums[0];
    int maxSum = dp[0];
    
    for (int i = 1; i < n; i++) {
        dp[i] = max(dp[i-1] + nums[i], nums[i]); // 状态转移
        maxSum = max(maxSum, dp[i]); // 更新全局最大值
    }
    return maxSum;
}

时间复杂度:O(n)
空间复杂度:O(n)(可优化为O(1))

2.2 最长上升子序列(LIS)

问题描述:求序列中最长的递增子序列长度,

输入格式:第1行是序列的长度N(1 <= N <= 1000)。
                  第2行给出序列中的N个整数,这些整数的取值范围都在0到10000之间
输出格式:一个整数,表示最长上升子序列的长度
输入样例:

7

2  7  3  1  9  4  2
输出样例:

3

状态定义

  • dp[i]:以第 i 个元素结尾的最长上升子序列长度

状态转移方程

dp[i] = max(dp[j]) + 1 (0 ≤ j < i 且 nums[j] < nums[i])

推导过程

  • 遍历所有 j < i

  • 若 nums[j] < nums[i],则 i 可以接在 j 后面

  • 在所有符合条件的 j 中,取 dp[j] 的最大值加1

代码实现

int longestIncreasingSubsequence(int a[], int n) {
    vector<int> dp(n, 1); // 初始化为1(每个元素自身是长度为1的序列)
    int maxV = 1;
    
    for (int i = 1; i < n; i++) {
        for (int j = 0; j < i; j++) {
            if (a[j] < a[i]) {
                dp[i] = max(dp[i], dp[j] + 1);
            }
        }
        maxV = max(maxV, dp[i]);
    }
    return maxV;
}

时间复杂度:O(n²)
空间复杂度:O(n)

2.3 最长公共子序列(LCS)

问题描述:求两个序列的最长公共子序列长度。

输入格式:
两行。每行为由大写字母构成的长度不超过1000的字符串,表示序列X和Y。
输出格式:
第一行为一个非负整数。表示所求得的最长公共子序列的长度。若不存在公共子
序列,则输出文件一个整数0。

输入样例:
ABCBDAB
BDCABA


输出样例:

4

状态定义

  • dp[i][j]X[0..i-1] 和 Y[0..j-1] 的LCS长度

状态转移方程

dp[i][j] = {
    dp[i-1][j-1] + 1,             if X[i-1] == Y[j-1]
    max(dp[i-1][j], dp[i][j-1]),  otherwise
}

推导过程

  • 末尾字符相同:LCS长度+1

  • 末尾字符不同:取两个子问题的最大值

    • 忽略X的末尾(dp[i-1][j]

    • 忽略Y的末尾(dp[i][j-1]

代码实现

int longestCommonSubsequence(string a, string b) {
    int m = a.size(), n = b.size();
    vector<vector<int>> dp(m+1, vector<int>(n+1, 0));
    
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            if (a[i-1] == b[j-1]) {
                dp[i][j] = dp[i-1][j-1] + 1;
            } else {
                dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
            }
        }
    }
    return dp[m][n];
}

时间复杂度:O(mn)
空间复杂度:O(mn)

2.4 编辑距离(Edit Distance)

问题描述:设A和B是两个字符串。我们要用最少的字符操作次数,将字符串A转换为字符串B。这里所说的字符操作共有三种:
1、删除一个字符;
2、插入一个字符
3、将一个字符改为另一个字符
对任意的两个字符串A和B,计算出将字符串A变换为字符串B所用的最少字符操作次数。
输入格式:
第一行为字符串A;第二行为字符串B;字符串A和B的长度均小于2000。
输出格式:
只有一个正整数,为最少字符操作次数。

输入样例:
sfdqxbw
gfdgw
输出样例:

4

状态定义

  • dp[i][j]:将 A[0..i-1] 转换为 B[0..j-1] 的最小操作数

状态转移方程

dp[i][j] = {
    dp[i-1][j-1],                  if A[i-1] == B[j-1]
    min(
        dp[i-1][j] + 1,    // 删除A[i]
        dp[i][j-1] + 1,    // 插入B[j]
        dp[i-1][j-1] + 1   // 替换A[i]为B[j]
    ), otherwise
}

推导过程

  • 字符相同:无需操作,继承前状态

  • 字符不同:取三种操作的最小值

    • 删除:消耗1次操作,转化为 dp[i-1][j]

    • 插入:消耗1次操作,转化为 dp[i][j-1]

    • 替换:消耗1次操作,转化为 dp[i-1][j-1]

代码实现

int editDistance(string a, string b) {
    int m = a.size(), n = b.size();
    vector<vector<int>> dp(m+1, vector<int>(n+1, 0));
    
    // 初始化边界条件
    for (int i = 0; i <= m; i++) dp[i][0] = i;
    for (int j = 0; j <= n; j++) dp[0][j] = j;
    
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            if (a[i-1] == b[j-1]) {
                dp[i][j] = dp[i-1][j-1];
            } else {
                dp[i][j] = min({
                    dp[i-1][j] + 1,   // 删除
                    dp[i][j-1] + 1,   // 插入
                    dp[i-1][j-1] + 1  // 替换
                });
            }
        }
    }
    return dp[m][n];
}

时间复杂度:O(mn)
空间复杂度:O(mn)

三、线性DP的优化技巧

3.1 空间优化:滚动数组

当状态只依赖前有限个状态时,可降维存储:

// 最长上升子序列优化(O(n)空间)
int lisOptimized(int a[], int n) {
    vector<int> tail; // 存储潜在LIS的末尾元素
    
    for (int x : a) {
        auto it = lower_bound(tail.begin(), tail.end(), x);
        if (it == tail.end()) {
            tail.push_back(x); // 可扩展LIS
        } else {
            *it = x; // 优化潜在序列
        }
    }
    return tail.size();
}

3.2 时间优化:预处理与剪枝

  • 前缀和/后缀和:用于子数组问题

  • 单调队列/栈:优化滑动窗口类问题

  • 二分查找:将O(n)查找优化为O(logn)

四、练习题库(难度递进)

4.1 基础练习

  1. 爬楼梯问题:每次爬1或2阶,求到n阶的方法数
    状态方程dp[i] = dp[i-1] + dp[i-2]

  2. 打家劫舍:不能偷相邻房屋,求最大收益
    状态方程dp[i] = max(dp[i-1], dp[i-2] + nums[i])

  3. 硬币找零:求组成金额的最少硬币数
    状态方程dp[i] = min(dp[i], dp[i-coin]+1)

4.2 进阶挑战

  1. 最长回文子序列

    dp[i][j] = {
        2 + dp[i+1][j-1],            if s[i]==s[j]
        max(dp[i+1][j], dp[i][j-1]), otherwise
    }
  2. 乘积最大子数组

    maxDP[i] = max(nums[i], maxDP[i-1]*nums[i], minDP[i-1]*nums[i])
    minDP[i] = min(nums[i], maxDP[i-1]*nums[i], minDP[i-1]*nums[i])

五、总结与学习建议

5.1 线性DP解题四步法

  1. 定义状态:明确 dp[i] 或 dp[i][j] 的含义

  2. 建立方程:找出状态间的递推关系

  3. 初始化:设置边界条件

  4. 确定顺序:选择正确的计算方向

5.2 核心思维训练

  • 分解思想:将大问题拆解为线性子问题

  • 状态设计:"以...结尾"是最常用状态定义

  • 方程推导:思考最后一步操作的决策点

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值