代码随想录——动态规划之序列问题

300.最长递增子序列

300. 最长递增子序列

问题描述

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

思路:dp
  1. 状态定义
    dp[i] 表示以 nums[i] 结尾的最长递增子序列的长度。
    关键点:必须以 nums[i] 结尾,确保问题可分解。
  2. 初始化
    每个元素自身至少可以组成长度为 1 的子序列,因此初始化 dp[i] = 1
  3. 状态转移方程
    对于每个 i,遍历其之前的所有元素 j0 ≤ j < i):
    • 如果 nums[j] < nums[i],说明 nums[i] 可以接在 nums[j] 构成的递增子序列后面,形成更长的子序列。
    • 此时更新 dp[i] = max(dp[i], dp[j] + 1),即选择所有满足条件的 j 中的最大值。
  4. 结果获取
    最终结果不是 dp[n-1],而是 dp 数组中的最大值,因为最长子序列可能以任意位置结尾。
代码一
// 求解最长递增子序列(Longest Increasing Subsequence, LIS)长度的函数
int lengthOfLIS(int* nums, int numsSize) {
    // 定义 dp 数组,其中 dp[i] 表示以 nums[i] 结尾的最长递增子序列的长度
    int dp[numsSize];
    
    // 初始化 dp 数组,每个位置至少可以构成长度为 1 的递增子序列(即该元素自身)
    for (int i = 0; i < numsSize; i++) {
        dp[i] = 1;
    }

    // 遍历数组,计算每个位置的 dp 值
    for (int i = 1; i < numsSize; i++) {
        // 对于每个 nums[i],遍历 i 之前的所有元素 nums[j]
        for (int j = 0; j < i; j++) {
            // 如果当前元素 nums[i] 大于之前的元素 nums[j],说明可以在 nums[j] 后面接上 nums[i]
            if (nums[j] < nums[i])
                // 更新 dp[i]:取当前 dp[i] 和 dp[j] + 1(接上 nums[i] 后的序列长度)中的较大值
                dp[i] = fmax(dp[i], dp[j] + 1);
        }
    }

    // 找出 dp 数组中的最大值,即为整个数组中的最长递增子序列的长度
    int ans = 0;
    for (int i = 0; i < numsSize; i++) {
        ans = fmax(ans, dp[i]);
    }
    return ans;
}

代码二:贪心+二分查找

利用贪心+二分查找的方法,将时间复杂度从 O(n²) 降低到 O(n log n)。这种方法通常被称为“耐心排序”算法,其核心思想是维护一个数组 tails,其中 tails[i] 表示所有长度为 i+1 的递增子序列中,结尾元素的最小值。

具体思路

1.维护 tails 数组:
tails 数组用于存储不同长度递增子序列的最后一个元素,且这个最后一个元素是所有可能中最小的。这样可以尽可能为后续构造更长的子序列留出更大的空间。

2.二分查找插入位置:
对于每个新的数字 num,通过二分查找在 tails 数组中找到第一个大于或等于 num 的位置。

  • 如果 num 比 tails 中所有元素都大,则将其追加到 tails 数组末尾,表示找到了一条更长的递增子序列。
  • 否则,用 num 替换 tails 数组中第一个大于或等于它的元素,这一步操作不会改变序列长度,但会使 tails 数组中该长度序列的最后一个元素更小,从而有利于未来构造更长的序列。

3.返回 tails 长度:
tails 数组的长度即为最长递增子序列的长度。

#include <stdio.h>

// 辅助函数:在 tails 数组中利用二分查找找到第一个大于或等于 target 的位置
int binarySearch(int tails[], int size, int target) {
    int left = 0, right = size;
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (tails[mid] < target)
            left = mid + 1;
        else
            right = mid;
    }
    return left;
}

// 优化后的求最长递增子序列长度函数,时间复杂度为 O(n log n)
int lengthOfLIS(int* nums, int numsSize) {
    if (numsSize == 0) return 0;
    
    int tails[numsSize];  // tails 数组存储各长度递增子序列的最小尾值
    int size = 0;         // tails 数组当前的有效长度
    
    // 遍历每个数字,更新 tails 数组
    for (int i = 0; i < numsSize; i++) {
        // 对当前数字 nums[i],在 tails 中找到合适的位置
        int pos = binarySearch(tails, size, nums[i]);
        tails[pos] = nums[i];  // 替换或扩展 tails 数组
        if (pos == size) {     // 如果 pos 在 tails 尾部,说明找到了一条更长的序列
            size++;
        }
    }
    // tails 数组的长度就是最长递增子序列的长度
    return size;
}

674.最长连续递增序列

674. 最长连续递增序列

思路:dp

本题比较简单

  • 状态定义:

    dp[i]表示以nums[i]结尾的最长连续递增子序列的长度

  • 初始状态:
    d p [ 0 ] = 1 dp[0]=1 dp[0]=1

  • 状态转移公式

    对于 i ≥ 1 i \geq 1 i1有:
    d p [ i ] = { d p [ i − 1 ] + 1 , if  n u m s [ i ] > n u m s [ i − 1 ] , 1 , otherwise. dp[i] = \begin{cases} dp[i-1] + 1, & \text{if } nums[i] > nums[i-1], \\ 1, & \text{otherwise.} \end{cases} dp[i]={dp[i1]+1,1,if nums[i]>nums[i1],otherwise.

代码
// 求解最长连续递增子序列(Longest Continuous Increasing Subsequence, LCIS)长度的函数
int findLengthOfLCIS(int* nums, int numsSize) {
    // 定义 dp 数组,dp[i] 表示以 nums[i] 结尾的最长连续递增子序列的长度
    int dp[numsSize];
    
    // 初始化 dp 数组,每个位置至少构成长度为 1 的子序列(单个元素)
    for (int i = 0; i < numsSize; i++) {
        dp[i] = 1;
    }
    
    // 从第二个元素开始遍历
    for (int i = 1; i < numsSize; i++) {
        // 如果当前元素比前一个元素大,则当前的连续递增子序列长度在前一个基础上加 1
        if (nums[i] > nums[i - 1])
            dp[i] = dp[i - 1] + 1;
        else
            // 否则,当前元素不能与前面的连续序列连接,重新开始一个新的序列,长度为 1
            dp[i] = 1;
    }
    
    // 遍历 dp 数组,找到最大值,即为整个数组中的最长连续递增子序列长度
    int ans = 0;
    for (int i = 0; i < numsSize; i++) {
        ans = fmax(dp[i], ans);
    }
    return ans;
}

718.最长重复子数组

718. 最长重复子数组

注:本题的重复子数组是连续的

思路:dp

1.状态定义

定义二维动态规划数组 d p dp dp,其中:

  • d p [ i ] [ j ] dp[i][j] dp[i][j] 表示以 n u m s 1 [ i − 1 ] nums1[i-1] nums1[i1] n u m s 2 [ j − 1 ] nums2[j-1] nums2[j1] 结尾的公共连续子数组的长度。

2.初始状态

对于数组的边界条件,当其中一个数组为空时,公共子数组的长度为 0 0 0。因此:

  • i = 0 i=0 i=0 j = 0 j=0 j=0 时, d p [ i ] [ 0 ] = 0 dp[i][0] = 0 dp[i][0]=0 d p [ 0 ] [ j ] = 0 dp[0][j] = 0 dp[0][j]=0

3.状态转移公式

  • 对于 i ≥ 1 i \geq 1 i1 j ≥ 1 j \geq 1 j1,状态转移公式为:

d p [ i ] [ j ] = { d p [ i − 1 ] [ j − 1 ] + 1 , if  n u m s 1 [ i − 1 ] = n u m s 2 [ j − 1 ] , 0 , otherwise. dp[i][j] = \begin{cases} dp[i-1][j-1] + 1, & \text{if } nums1[i-1] = nums2[j-1], \\ 0, & \text{otherwise.} \end{cases} dp[i][j]={dp[i1][j1]+1,0,if nums1[i1]=nums2[j1],otherwise.

  • 解释:

    • 如果 n u m s 1 [ i − 1 ] = n u m s 2 [ j − 1 ] nums1[i-1] = nums2[j-1] nums1[i1]=nums2[j1],说明可以将前面匹配到的公共连续子数组延长 1 1 1,因此 d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] + 1 dp[i][j] = dp[i-1][j-1] + 1 dp[i][j]=dp[i1][j1]+1

    • 如果不相等,则连续性中断,公共子数组长度重置为 0 0 0

4.最终结果

遍历整个 d p dp dp 数组,找出所有元素中的最大值,该最大值即为两个数组的最长公共连续子数组的长度。

代码
// 求两个数组的最长公共连续子数组的长度
int findLength(int* nums1, int nums1Size, int* nums2, int nums2Size) {
    // 定义 dp 数组,其尺寸为 (nums1Size+1) x (nums2Size+1)
    // dp[i][j] 表示以 nums1[i-1] 和 nums2[j-1] 结尾的公共连续子数组的长度
    int dp[nums1Size+1][nums2Size+1];
    
    // 将 dp 数组全部初始化为 0
    // 当其中一个数组为空时,公共子数组长度为 0
    memset(dp, 0, sizeof(dp));
    
    // 遍历两个数组,从 1 开始,避免下标越界,同时便于利用前一个状态 dp[i-1][j-1]
    for (int i = 1; i <= nums1Size; i++) {
        for (int j = 1; j <= nums2Size; j++) {
            // 如果 nums1 中第 i-1 个元素和 nums2 中第 j-1 个元素相等,
            // 则说明可以将前面匹配的公共连续子数组延长1
            if (nums1[i-1] == nums2[j-1])
                dp[i][j] = dp[i-1][j-1] + 1;
            else
                // 否则,当前位置不构成连续公共子数组,将 dp[i][j] 置为 0
                dp[i][j] = 0;
        }
    }
    
    // 遍历 dp 数组,寻找最大值,即为最长公共连续子数组的长度
    int ans = 0;
    for (int i = 1; i <= nums1Size; i++) {
        for (int j = 1; j <= nums2Size; j++) {
            if (dp[i][j] > ans)
                ans = dp[i][j];
        }
    }
    return ans;
}

1143.最长公共子序列

1143. 最长公共子序列

思路:dp

1. 状态定义

  • d p [ i ] [ j ] dp[i][j] dp[i][j] 表示 字符串 t e x t 1 text1 text1 的前 i i i 个字符字符串 t e x t 2 text2 text2 的前 j j j 个字符 之间的最长公共子序列长度。

2. 初始状态

  • 任何字符串和空字符串的最长公共子序列长度均为 0 0 0,即:
    d p [ i ] [ 0 ] = 0 , d p [ 0 ] [ j ] = 0 dp[i][0] = 0, \quad dp[0][j] = 0 dp[i][0]=0,dp[0][j]=0
  • 代码中通过 memset(dp, 0, sizeof(dp)) 进行初始化。

3. 状态转移方程

对于 i ≥ 1 i \geq 1 i1 j ≥ 1 j \geq 1 j1,状态转移公式如下:
d p [ i ] [ j ] = { d p [ i − 1 ] [ j − 1 ] + 1 , if  t e x t 1 [ i − 1 ] = t e x t 2 [ j − 1 ] max ⁡ ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − 1 ] ) , if  t e x t 1 [ i − 1 ] ≠ t e x t 2 [ j − 1 ] dp[i][j] = \begin{cases} dp[i-1][j-1] + 1, & \text{if } text1[i-1] = text2[j-1] \\ \max(dp[i-1][j], dp[i][j-1]), & \text{if } text1[i-1] \neq text2[j-1] \end{cases} dp[i][j]={dp[i1][j1]+1,max(dp[i1][j],dp[i][j1]),if text1[i1]=text2[j1]if text1[i1]=text2[j1]

解释:

  • 匹配情况:如果 t e x t 1 [ i − 1 ] = t e x t 2 [ j − 1 ] text1[i-1] = text2[j-1] text1[i1]=text2[j1],说明当前字符匹配成功,
    • 则可以在之前的匹配基础上加 1 1 1,即 d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] + 1 dp[i][j] = dp[i-1][j-1] + 1 dp[i][j]=dp[i1][j1]+1
  • 不匹配情况:如果 t e x t 1 [ i − 1 ] ≠ t e x t 2 [ j − 1 ] text1[i-1] \neq text2[j-1] text1[i1]=text2[j1],则当前字符不匹配,
    • 需要取去掉当前字符后的最大值,即 d p [ i ] [ j ] = max ⁡ ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − 1 ] ) dp[i][j] = \max(dp[i-1][j], dp[i][j-1]) dp[i][j]=max(dp[i1][j],dp[i][j1])
代码
int longestCommonSubsequence(char* text1, char* text2) {
    // 获取字符串 text1 和 text2 的长度
    int m = strlen(text1), n = strlen(text2);
    
    // 定义一个二维 DP 数组,大小为 (m+1) x (n+1)
    // dp[i][j] 表示 text1 的前 i 个字符与 text2 的前 j 个字符之间的 LCS 长度
    int dp[m+1][n+1];
    
    // 将 dp 数组所有元素初始化为 0
    memset(dp, 0, sizeof(dp));
    
    // 遍历 text1 和 text2 的所有字符组合
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            // 如果当前字符相等,则可以将前面的 LCS 长度加 1
            if (text1[i-1] == text2[j-1])
                dp[i][j] = dp[i-1][j-1] + 1;
            else
                // 如果不相等,则选择删除其中一个字符,取较大值
                dp[i][j] = fmax(dp[i-1][j], dp[i][j-1]);
        }
    }
    
    // 返回两个字符串的 LCS 长度
    return dp[m][n];
}

1035.不相交的线

1035. 不相交的线

思路:dp

本题 “不相交的线” 可以抽象为 最长公共子序列(LCS) 问题:

  • 线段的连接规则要求 不相交,这意味着找到的序列必须 保持相对顺序
  • 只要两个数组中的相同元素按照相对顺序进行匹配,就能保证它们的线不会相交。

1. 状态定义

dp[i][j] 表示:

  • 数组 nums1i 个元素数组 nums2j 个元素 之间的 最多不相交线数(即最长公共子序列长度)

2. 状态转移方程

情况 1:当前元素匹配

  • nums1[i-1] == nums2[j-1] 时:
    • 说明可以在 i-1 个元素与前 j-1 个元素的最长匹配基础上增加一条线
    • 因此:
      d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] + 1 dp[i][j] = dp[i-1][j-1] + 1 dp[i][j]=dp[i1][j1]+1

情况 2:当前元素不匹配

  • nums1[i-1] ≠ nums2[j-1] 时:
    • 只能选择 不使用 nums1[i-1]不使用 nums2[j-1],即:
      d p [ i ] [ j ] = max ⁡ ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − 1 ] ) dp[i][j] = \max(dp[i-1][j], dp[i][j-1]) dp[i][j]=max(dp[i1][j],dp[i][j1])

3. 边界初始化

  • dp[i][0] = 0(当 nums2 为空时,匹配结果为 0)
  • dp[0][j] = 0(当 nums1 为空时,匹配结果为 0)
  • 在代码中通过 memset(dp, 0, sizeof(dp)); 初始化。
代码实现(C 语言)
#include <stdio.h>
#include <string.h>
#include <math.h>

int maxUncrossedLines(int* nums1, int nums1Size, int* nums2, int nums2Size) {
    // 定义DP数组,dp[i][j] 表示 nums1 前 i 个元素 与 nums2 前 j 个元素的最长公共子序列长度
    int dp[nums1Size+1][nums2Size+1]; 

    // 初始化DP数组,将所有值设为0(代表空序列)
    memset(dp, 0, sizeof(dp)); 

    // 遍历 nums1 的所有前缀
    for (int i = 1; i <= nums1Size; i++) {
        // 遍历 nums2 的所有前缀
        for (int j = 1; j <= nums2Size; j++) {
            if (nums1[i-1] == nums2[j-1]) { 
                // 若当前元素匹配,则在 dp[i-1][j-1] 基础上 +1
                dp[i][j] = dp[i-1][j-1] + 1;
            } else {
                // 若当前元素不匹配,则取“去掉 nums1[i] 或 nums2[j]”时的较大值
                dp[i][j] = fmax(dp[i-1][j], dp[i][j-1]);
            }
        }
    }

    // 返回最终计算出的最长公共子序列(LCS)长度,即最大不相交连线数
    return dp[nums1Size][nums2Size];
}

53.最大子数组和

53. 最大子数组和

思路一:贪心

1.局部最优解:

  • 维护一个当前子数组的最大和currentSum
  • 遍历nums时,如果currentSum变成负数,就直接抛弃,从当前元素重新开始计算子数组。

2.全局最优解:

  • 在遍历过程中,维护一个全局最大子数组和maxSum,记录所有可能的currentSum中的最大值。

贪心策略

  • currentSum 为负数,则 currentSum = nums[i](重新开始)。
  • currentSum 为非负数,则 currentSum += nums[i](继续扩展)。
  • 更新 maxSum = max(maxSum, currentSum)
代码
int maxSubArray(int* nums, int numsSize) {
    int maxSum = nums[0];   // 记录全局最大子数组和
    int currentSum = nums[0]; // 记录当前子数组和

    for (int i = 1; i < numsSize; i++) {
        currentSum = fmax(nums[i], currentSum + nums[i]); // 贪心选择当前最大子数组和
        maxSum = fmax(maxSum, currentSum); // 更新最大子数组和
    }

    return maxSum;
}

思路二:dp

1.状态定义:

  • dp[i]表示nums[i]结尾的最大子数组和

2.状态转移方程:
d p [ i ] = { d p [ i − 1 ] + 1 , if  d p [ i − 1 ] > 0 n u m s [ i ] , otherwise dp[i] = \begin{cases} dp[i-1] + 1, & \text{if } dp[i-1]>0 \\ nums[i], & \text{otherwise} \end{cases} dp[i]={dp[i1]+1,nums[i],if dp[i1]>0otherwise
3.初始状态:

  • dp[0]=nums[0]

4.返回结果:

  • max(dp[i]),i=0,1,2……numsSize-1
代码
int maxSubArray(int* nums, int numsSize) {
    int dp[numsSize]; 
    dp[0] = nums[0]; // 初始化

    int maxSum = dp[0]; // 记录最大子数组和

    for (int i = 1; i < numsSize; i++) {
        dp[i] = fmax(dp[i - 1] + nums[i], nums[i]); // 状态转移
        maxSum = fmax(maxSum, dp[i]); // 更新最大和
    }

    return maxSum;
}

总结
解法思路状态转移方程时间复杂度空间复杂度
贪心算法维护 currentSum,若为负则重置 c u r r e n t S u m = max ⁡ ( c u r r e n t S u m + n u m s [ i ] , n u m s [ i ] ) currentSum = \max(currentSum + nums[i], nums[i]) currentSum=max(currentSum+nums[i],nums[i]) O ( n ) O(n) O(n) O ( 1 ) O(1) O(1)
动态规划dp[i] 为以 nums[i] 结尾的最大子数组和 d p [ i ] = max ⁡ ( d p [ i − 1 ] + n u m s [ i ] , n u m s [ i ] ) dp[i] = \max(dp[i-1] + nums[i], nums[i]) dp[i]=max(dp[i1]+nums[i],nums[i]) O ( n ) O(n) O(n) O ( n ) O(n) O(n)

392.判断子序列

392. 判断子序列

给定字符串 st,判断 s 是否为 t子序列子序列 指的是可以删除 t 中的若干字符,但不改变剩余字符相对顺序后,得到 s

思路一:贪心+双指针

维护两个指针 ij

  • i 指向 s,表示当前匹配到 s[i]
  • j 指向 t,遍历 t 查找 s 的字符。

遍历 t

  • 如果 s[i] == t[j],说明匹配成功,i++,继续匹配下一个字符。
  • 否则j++,跳过 t[j],继续寻找 s[i]

终止条件

  • i == m:说明 s 的所有字符都匹配成功,返回 true
  • j == n:说明 t 遍历完毕,但 s 仍有未匹配的字符,返回 false
代码
bool isSubsequence(char* s, char* t) {
    int i = 0, j = 0;
    int m = strlen(s), n = strlen(t);
    
    while (i < m && j < n) { // 遍历 t,查找 s 的字符
        if (s[i] == t[j]) i++; // 匹配成功,s 指针前进
        j++; // t 指针前进
    }
    
    return i == m; // 若 s 全部匹配,返回 true,否则 false
}

思路二:dp
  • 预处理 t 的每个字符在后续出现的位置,加速 s 的匹配。

  • 维护一个 dp[i][c],表示 t[i] 及其后面最近的字符 c 位置。

    1. 初始化 dp

    由于 t[n:] 为空,因此对于 t 之外的部分:$ dp[n][c]=−1 ,∀c∈[a,z] $表示 t 结束后,任何字符都找不到。

    2.递推计算 dp[i][c]

    我们从 后往前 计算 dp

    • 如果 t[i] == c,说明 ct[i:] 的第一个位置就是 i,所以: d p [ i ] [ c ] = i dp[i][c]=i dp[i][c]=i
    • 否则ct[i+1:] 里的最近位置和 dp[i+1][c] 一样: d p [ i ] [ c ] = d p [ i + 1 ] [ c ] dp[i][c]=dp[i+1][c] dp[i][c]=dp[i+1][c]

    综上:
    d p [ i ] [ c ] = { i , if  t [ i ] = c d p [ i + 1 ] [ c ] , otherwise dp[i][c] = \begin{cases} i, & \text{if } t[i]=c \\ dp[i+1][c], & \text{otherwise} \end{cases} dp[i][c]={i,dp[i+1][c],if t[i]=cotherwise

  • 处理 s 时,按照 dp 跳跃,快速找到匹配字符。

代码
bool isSubsequence(char* s, char* t) {
    int m = strlen(s), n = strlen(t);
    int dp[n+1][26]; // 26 个字母
    
    // 初始化 dp[n],表示 t 结尾之后的所有字符都不存在
    for (int c = 0; c < 26; c++) dp[n][c] = -1;

    // 从后往前填充 dp
    for (int i = n - 1; i >= 0; i--) {
        for (int c = 0; c < 26; c++) dp[i][c] = dp[i + 1][c];
        dp[i][t[i] - 'a'] = i; // 记录 t[i] 的位置
    }

    // 使用 dp 进行跳跃匹配 s
    int j = 0; // 当前 t 位置
    for (int i = 0; i < m; i++) {
        if (j == -1) return false; // t 没有剩余字符
        j = dp[j][s[i] - 'a']; // 找到 s[i] 在 t 的下一个匹配位置
        if (j != -1) j++; // 跳到下一个匹配位置
    }

    return j != -1;
}

总结比较
方法适用场景时间复杂度额外空间
贪心双指针单次查询 s O ( n ) O(n) O(n) O ( 1 ) O(1) O(1)
DP 跳跃表多次查询 s O ( n + m ) O(n + m) O(n+m) O ( n ) O(n) O(n)

115.不同的子序列

115. 不同的子序列

题目描述:计算字符串 s 中有多少个不同的子序列等于字符串 t

思路:dp

1.状态定义

dp[i][j]表示 s 的前 i 个字符中,出现 t 的前 j 个字符的子序列个数

2.状态转移方程

  • s[i-1]==t[j-1],那么有情况:

    1.选用s[i-1],使其匹配t[j-1],有dp[i-1][j-1]个子序列

    2.不选用s[t-1],有dp[i-1][j]个子序列

    所以有:
    d p [ i ] [ j ] = ( d p [ i − 1 ] [ j − 1 ] + d p [ i − 1 ] [ j ] ) m o d ( 1 0 9 + 7 ) dp[i][j]=(dp[i-1][j-1]+dp[i-1][j])mod (10^9+7) dp[i][j]=(dp[i1][j1]+dp[i1][j])mod(109+7)

  • 如果 s[i-1] != t[j-1],那么 s[i-1] 不能匹配 t[j-1],只能继承 dp[i-1][j]

​ 有:
d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j]=dp[i-1][j] dp[i][j]=dp[i1][j]
综上:
d p [ i ] [ j ] = { ( d p [ i − 1 ] [ j − 1 ] + d p [ i − 1 ] [ j ] ) m o d ( 1 0 9 + 7 ) , if  s [ i − 1 ] = t [ j − 1 ] d p [ i − 1 ] [ j ] , otherwise dp[i][j] = \begin{cases} (dp[i-1][j-1]+dp[i-1][j])mod(10^9+7), & \text{if } s[i-1]=t[j-1] \\ dp[i-1][j], & \text{otherwise} \end{cases} dp[i][j]={(dp[i1][j1]+dp[i1][j])mod(109+7),dp[i1][j],if s[i1]=t[j1]otherwise
3.初始化

空字符串 t 是任何 s 的子序列,因此:

$ dp[i][0]=1$

非空 t 不能由空 s 匹配,所以:

d p [ 0 ] [ j ] = 0 ( j > 0 ) dp[0][j]=0 (j > 0) dp[0][j]=0(j>0)

代码
int mod = 1e9 + 7;

int numDistinct(char* s, char* t) {
    int m = strlen(s), n = strlen(t);
    int dp[m+1][n+1];  // 定义 DP 数组,dp[i][j] 代表 s 前 i 个字符包含 t 前 j 个字符的子序列个数

    // 初始化边界条件
    // 空字符串 t 是任何 s 的子序列
    for(int i = 0; i <= m; i++) {
        dp[i][0] = 1;
    }
    // 非空 t 无法由空 s 组成
    for(int i = 1; i <= n; i++) {
        dp[0][i] = 0;
    }

    // 状态转移方程填充 DP 表
    for(int i = 1; i <= m; i++) {
        for(int j = 1; j <= n; j++) {
            if (s[i-1] == t[j-1]) {  // s[i-1] 和 t[j-1] 匹配,有两种选择
                dp[i][j] = (dp[i-1][j-1] + dp[i-1][j]) % mod;
            } else {  // s[i-1] 和 t[j-1] 不匹配,继承之前的值
                dp[i][j] = dp[i-1][j];
            }
        }
    }

    return dp[m][n];  // 返回最终答案
}

583. 两个字符串的删除操作

583. 两个字符串的删除操作

问题描述

给定两个字符串 word1 和 word2,我们只能执行 删除 操作,将 word1 变成 word2,求最少删除的字符数量。

思路:dp

1.状态定义

dp[i][j] 表示 word1 的前 i 个字符转换为 word2 的前 j 个字符所需的最少删除操作数

2.状态转移方程

  • 如果 word1[i-1] == word2[j-1]
    • 说明当前字符匹配,不需要删除,继承前一个状态:

d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] dp[i][j]=dp[i-1][j-1] dp[i][j]=dp[i1][j1]

  • 如果 word1[i-1] ≠ word2[j-1]
    • 由于只能进行 删除 操作,所以word1[i-1]word2[j-1] 必须被删掉:

      • 删除 word1[i-1]dp[i-1][j] + 1
      • 删除 word2[j-1]dp[i][j-1] + 1
    • 取最小值:
      d p [ i ] [ j ] = m i n ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − 1 ] ) + 1 dp[i][j]=min(dp[i-1][j],dp[i][j-1])+1 dp[i][j]=min(dp[i1][j],dp[i][j1])+1

初始化(边界条件)

  • word1 为空(i = 0)时,只能通过删除 word2 的全部字符得到 word1
    d p [ 0 ] [ j ] = j dp[0][j]=j dp[0][j]=j

  • word2 为空(j = 0)时,只能通过删除 word1 的全部字符得到 word2
    d p [ i ] [ 0 ] = i dp[i][0]=i dp[i][0]=i

代码
int minDistance(char* word1, char* word2) {
    int m = strlen(word1);
    int n = strlen(word2);
    int dp[m+1][n+1];

    // 初始化边界条件
    for (int i = 0; i <= m; i++) {
        dp[i][0] = i;  // word2 为空,删除 word1 的全部字符
    }
    for (int j = 0; j <= n; j++) {
        dp[0][j] = j;  // word1 为空,删除 word2 的全部字符
    }

    // 计算 dp[i][j] 状态
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            if (word1[i-1] == word2[j-1]) {  // 字符匹配,无需删除
                dp[i][j] = dp[i-1][j-1];
            } else {  // 选择删除 word1[i-1] 或 word2[j-1]
                dp[i][j] = fmin(dp[i-1][j] + 1, dp[i][j-1] + 1);
            }
        }
    }

    return dp[m][n];  // 返回最少删除次数
}

72.编辑距离

72. 编辑距离

问题描述

给你两个单词 word1word2请返回将 word1 转换成 word2 所使用的最少操作数

你可以对一个单词进行如下三种操作:

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符
思路:dp

与上一题(583. 两个字符串的删除操作)不同的是添加了插入和替换操作,其实插入和删除操作的效果是一样的,因此可以忽略添加的插入操作,只用删除和替换操作。

思路与上题相比,只是略有不同:

如果 word1[i-1] ≠ word2[j-1](当前字符不匹配),则有 3 种操作方式:

  • 删除 word1[i-1](让 word1 变短):dp[i-1][j] + 1

  • 删除 word2[j-1](使 word2 变短):dp[i][j-1] + 1

  • 替换 word1[i-1]word2[j-1]dp[i-1][j-1] + 1

  • 取最小值:
    d p [ i ] [ j ] = m i n ⁡ ( d p [ i − 1 ] [ j ] + 1 , d p [ i ] [ j − 1 ] + 1 , d p [ i − 1 ] [ j − 1 ] + 1 ) dp[i][j]=min⁡(dp[i−1][j]+1,dp[i][j−1]+1,dp[i−1][j−1]+1) dp[i][j]=min(dp[i1][j]+1,dp[i][j1]+1,dp[i1][j1]+1)

其他都是相同的:

  • 动态规划定义dp[i][j] 表示 word1i 个字符变为 word2j 个字符的最小操作次数。

  • 状态转移方程
    d p [ i ] [ j ] = { d p [ i − 1 ] [ j − 1 ] , 如果  w o r d 1 [ i − 1 ] = w o r d 2 [ j − 1 ] min ⁡ ( d p [ i − 1 ] [ j − 1 ] + 1 , d p [ i − 1 ] [ j ] + 1 , d p [ i ] [ j − 1 ] + 1 ) , 否则 dp[i][j] = \begin{cases} dp[i-1][j-1], & \text{如果 } word1[i-1] = word2[j-1] \\ \min(dp[i-1][j-1] + 1, dp[i-1][j] + 1, dp[i][j-1] + 1), & \text{否则} \end{cases} dp[i][j]={dp[i1][j1],min(dp[i1][j1]+1,dp[i1][j]+1,dp[i][j1]+1),如果 word1[i1]=word2[j1]否则

  • 边界处理

    • dp[i][0] = i(删除 word1
    • dp[0][j] = j(插入 word2
代码
#include <stdio.h>
#include <string.h>
#include <math.h>

int minDistance(char* word1, char* word2) {
    int m = strlen(word1);
    int n = strlen(word2);
    int dp[m+1][n+1];

    // 初始化边界条件
    for (int i = 0; i <= m; i++) {
        dp[i][0] = i;  // 需要删除 i 个字符
    }
    for (int j = 0; j <= n; j++) {
        dp[0][j] = j;  // 需要插入 j 个字符
    }

    // 计算 dp[i][j] 状态
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            if (word1[i-1] == word2[j-1]) {  // 字符匹配,无需编辑
                dp[i][j] = dp[i-1][j-1];
            } else {  // 选择删除、插入或替换
                dp[i][j] = fmin(dp[i-1][j-1] + 1,   // 替换
                                fmin(dp[i-1][j] + 1,  // 删除
                                     dp[i][j-1] + 1)); // 删除
            }
        }
    }

    return dp[m][n];  // 返回最少编辑距离
}

674.回文子串

647. 回文子串

题目描述

给定一个字符串 s,统计其中 回文子串 的个数。回文子串是指 从前往后读和从后往前读都相同的连续子串

思路:dp

1. 定义状态

dp[i][j] 表示 子串 s[i:j] 是否为回文串,即 s[i]s[j] 之间的子串是否是回文。

2. 状态转移方程

  • i == j,即单个字符,一定是回文串:

    dp[i][i]=true

  • s[i] == s[j] 时,需要检查 s[i:j] 是否是回文:

    • j == i + 1(即长度为 2),如 "aa",那么 s[i:j] 一定是回文: dp[i][j]=true
    • j > i + 1(长度大于 2),则需要 s[i+1:j-1] 也是回文: dp[i][j]=dp[i+1][j−1]
  • 如果 s[i] != s[j],则 s[i:j] 不是回文:

    dp[i][j]=false

3.遍历顺序

根据状态转移方程可知:dp[i][j]依赖于dp[i+1][j-1],所以i由大向小遍历,j由小向大遍历

代码
int countSubstrings(char* s) {
    int n = strlen(s);
    bool dp[n][n];
    memset(dp, false, sizeof(dp));
    int ans = 0;

    // 逆序遍历 i,正序遍历 j,保证 dp[i+1][j-1] 已经被计算
    for (int i = n - 1; i >= 0; i--) {
        for (int j = i; j < n; j++) {
            if (s[i] == s[j]) {
                if (j - i <= 1) dp[i][j] = true;  // 单字符或双字符情况
                else dp[i][j] = dp[i + 1][j - 1]; // 长度大于 2 时,依赖内部区间
            } else {
                dp[i][j] = false;
            }
            
            if (dp[i][j]) ans++; // 统计回文子串数量
        }
    }
    return ans;
}

516.最长回文子序列

516. 最长回文子序列

题目描述

给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。

子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。

思路:dp

1. 定义状态

dp[i][j] 表示 s[i:j] 范围内的最长回文子序列的长度

2. 状态转移方程

  • s[i] == s[j]

    • 如果 i == j,单个字符自身是回文,长度为 1

    • 如果 j == i+1,两个字符相同,如 "aa",最长回文子序列长度 2

    • 其它情况下,s[i]s[j] 可作为子序列的两端,长度增加 2
      d p [ i ] [ j ] = d p [ i + 1 ] [ j − 1 ] + 2 dp[i][j]=dp[i+1][j-1]+2 dp[i][j]=dp[i+1][j1]+2

  • s[i] != s[j]

    • 需要舍弃 s[i]s[j],即:
      d p [ i ] [ j ] = m a x ( d p [ i + 1 ] [ j ] , d p [ i ] [ j − 1 ] ) dp[i][j]=max(dp[i+1][j],dp[i][j-1]) dp[i][j]=max(dp[i+1][j],dp[i][j1])

3.初始化

  • dp[i][i] = 1(单个字符的最长回文子序列长度为 1)。

4.遍历顺序

  • dp[i][j] 依赖于 dp[i+1][j-1],所以需要 倒序遍历 i,顺序遍历 j
代码
int longestPalindromeSubseq(char* s) {
    int n = strlen(s);
    int dp[n][n];

    // 初始化单个字符的回文长度
    for (int i = 0; i < n; i++) dp[i][i] = 1;

    // 逆序遍历 i,保证计算 dp[i][j] 时 dp[i+1][j-1] 已计算
    for (int i = n - 1; i >= 0; i--) {
        for (int j = i + 1; j < n; j++) {
            if (s[i] == s[j]) {
                if (j == i + 1) dp[i][j] = 2;  
                else dp[i][j] = dp[i + 1][j - 1] + 2;
            } else {
                dp[i][j] = fmax(dp[i + 1][j], dp[i][j - 1]);
            }
        }
    }
    
    return dp[0][n - 1]; // 结果在 dp[0][n-1]
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值