当前位置: 首页 > article >正文

力扣第115题:不同的子序列 — C语言解法

力扣第115题:不同的子序列 — C语言解法

题目描述

给定一个字符串 s 和一个字符串 t,返回 ts 中出现的不同子序列的个数。

子序列 是由字符串中删除一些字符(可以不删除任何字符)得到的一个新字符串。

说明:

  • 字符串 st 的长度均不超过 1000 1000 1000
  • 一个子序列是由删除零个或多个字符而得到的字符串。

解题思路

这道题目要求我们计算字符串 t 在字符串 s 中出现的不同子序列的个数。子序列的匹配可以是不连续的,但必须按顺序匹配。

动态规划(Dynamic Programming)

这是一道典型的动态规划问题。设 dp[i][j] 表示字符串 s 的前 i i i 个字符中,字符串 t 的前 j j j 个字符的不同子序列的个数。

状态转移方程
  1. 当字符匹配时( s [ i − 1 ] = = t [ j − 1 ] s[i-1] == t[j-1] s[i1]==t[j1]

    • 我们有两种选择:
      • 包含当前字符:即在当前字符 s[i-1] 匹配到 t[j-1] 后,继续计算剩余部分:dp[i-1][j-1]
      • 不包含当前字符:即忽略当前字符 s[i-1],继续计算:dp[i-1][j]
    • 所以状态转移方程为:
      d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] + d p [ i − 1 ] [ j ] dp[i][j] = dp[i-1][j-1] + dp[i-1][j] dp[i][j]=dp[i1][j1]+dp[i1][j]
  2. 当字符不匹配时( s [ i − 1 ] ≠ t [ j − 1 ] s[i-1] \neq t[j-1] s[i1]=t[j1]

    • 我们只能选择不包含当前字符 s[i-1],即:
      d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j] = dp[i-1][j] dp[i][j]=dp[i1][j]
边界条件
  • dp[i][0] = 1:对于任何 i i i,空字符串 t 总能在字符串 s 中找到(即不选任何字符)。
  • dp[0][j] = 0:对于任何 j > 0 j > 0 j>0,空字符串 s 无法包含任何非空字符串 t
模运算

由于子序列的个数可能非常大,我们需要对结果进行取模运算。通常使用 M O D = 1 0 9 + 7 MOD = 10^9 + 7 MOD=109+7,它是一个大素数,可以避免整数溢出,并且在算法竞赛中常见。


代码实现

#include <stdio.h>
#include <string.h>

#define MOD 1000000007  // 定义常用的模数

// 动态规划实现:计算 t 在 s 中的不同子序列个数
long long numDistinct(char* s, char* t) {
    int m = strlen(s);
    int n = strlen(t);

    // dp[i][j] 表示 s[0..i-1] 中 t[0..j-1] 的不同子序列个数
    long long dp[m + 1][n + 1];

    // 初始化 dp 数组
    for (int i = 0; i <= m; i++) {
        dp[i][0] = 1;  // 空字符串 t 总能在任何 s 中找到
    }
    for (int j = 1; j <= n; j++) {
        dp[0][j] = 0;  // 非空字符串 t 不能在空字符串 s 中找到
    }

    // 填充 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],我们可以选择包含 s[i-1] 或不包含
                dp[i][j] = (dp[i - 1][j - 1] + dp[i - 1][j]) % MOD;
            } else {
                // 如果 s[i-1] != t[j-1],我们只能选择不包含 s[i-1]
                dp[i][j] = dp[i - 1][j] % MOD;
            }
        }
    }

    // 返回 dp[m][n],即 s[0..m-1] 中 t[0..n-1] 的不同子序列个数
    return dp[m][n];
}

// 测试代码
int main() {
    char s[] = "rabbbit";
    char t[] = "rabbit";

    long long result = numDistinct(s, t);
    printf("Number of distinct subsequences: %lld\n", result);

    return 0;
}

代码解析

1. 定义模数

我们在代码中定义了常量 MOD = 1000000007,用于防止结果的溢出。每次更新 dp 数组时,我们都对当前值取模,确保它不会超出 long long 类型的最大值。

#define MOD 1000000007  // 定义常用的模数

2. 初始化 dp 数组

在动态规划中,我们使用一个二维数组 dp 来存储不同子序列的计数。dp[i][j] 表示在 s[0..i-1] 中找到 t[0..j-1] 的不同子序列的个数。我们初始化 dp[i][0] = 1,因为空字符串 t 总是能够在 s 中找到(即不选择任何字符)。同时,对于任何 j > 0dp[0][j] = 0,因为非空字符串 t 无法在空字符串 s 中找到。

for (int i = 0; i <= m; i++) {
    dp[i][0] = 1;  // 空字符串 t 总能在任何 s 中找到
}
for (int j = 1; j <= n; j++) {
    dp[0][j] = 0;  // 非空字符串 t 不能在空字符串 s 中找到
}

3. 填充 dp 数组

我们按照动态规划的思路,遍历 st 的每个字符,更新 dp[i][j]。如果 s[i-1] == t[j-1],我们可以选择包含当前字符或不包含它,更新为:
d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] + d p [ i − 1 ] [ j ] dp[i][j] = dp[i-1][j-1] + dp[i-1][j] dp[i][j]=dp[i1][j1]+dp[i1][j]
否则,我们只能选择不包含当前字符:
d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j] = dp[i-1][j] dp[i][j]=dp[i1][j]

每次更新时,我们都对结果取模,防止溢出。

for (int i = 1; i <= m; i++) {
    for (int j = 1; j <= n; j++) {
        if (s[i - 1] == t[j - 1]) {
            dp[i][j] = (dp[i - 1][j - 1] + dp[i - 1][j]) % MOD;
        } else {
            dp[i][j] = dp[i - 1][j] % MOD;
        }
    }
}

4. 返回结果

最终,dp[m][n] 存储的是在 s[0..m-1] 中,t[0..n-1] 的不同子序列的个数。

return dp[m][n];

5. 测试代码

main 函数中,我们测试了 s = "rabbbit"t = "rabbit" 的情况,输出了结果:

long long result = numDistinct(s, t);
printf("Number of distinct subsequences: %lld\n", result);

时间复杂度与空间复杂度

  • 时间复杂度 O ( m × n ) O(m \times n) O(m×n),其中 m m m n n n 分别是字符串 st 的长度。我们需要填充一个 m × n m \times n m×n 的动态规划数组。
  • 空间复杂度 O ( m × n ) O(m \times n) O(m×n),用于存储动态规划数组 dp

由于每次更新 dp[i][j] 时的计算都是常数时间,因此总时间复杂度为 O ( m × n ) O(m \times n) O(m×n)


http://www.kler.cn/a/450745.html

相关文章:

  • C语言初阶【13】——打印一个数的每一位(递归和非递归实现)
  • 微调大模型时,如何进行数据预处理? 将<input, output>转换为模型所需的<input_ids, labels, attention_mask>
  • springBoot Maven 剔除无用的jar引用
  • pyinstaller打包资源文件和ini配置文件怎么放
  • MySQL 8.0:explain analyze 分析 SQL 执行过程
  • 写给Pythoner的前端进阶指南(五):事件驱动模型
  • golang , chan学习
  • 62.基于SpringBoot + Vue实现的前后端分离-驾校预约学习系统(项目+论文)
  • Java面试题精选:MyBatis(一)
  • 使用RKNN进行YOLOv8人体姿态估计的实战教程:yolov8-pose.onnx转yolov8-pose.rknn+推理全流程
  • Excel生成DBC脚本源文件
  • 分布式 IO 模块:赋能造纸业,革新高速纸机主传动
  • 【MFC】如何修改多文档视图的标签
  • 深入解析Android Recovery系统
  • 代写软件标书哪里找:如何让标书撰写变得高效轻松
  • 自动驾驶---Parking端到端架构
  • 在 .NET Core 中使用 ActionBlock 实现高效率的多步骤数据处理
  • 阿里云ESC服务器一次性全部迁移到另一个ESC
  • 以“技”出圈,珈和科技农业典型案例 “盛放”2024湖北农博会
  • 问题小记-达梦数据库报错“字符串转换出错”处理
  • 深入理解C++23的Deducing this特性(上):基础概念与语法详解
  • curl 放弃对 Hyper Rust HTTP 后端的支持
  • 《Opencv》基础操作详解(3)
  • 全国硕士研究生入学考试(考研)考研时间线之大四
  • 24.12.25 AOP
  • CASA模型相关遥感数据及MODIS NDVI、FPAR遥感产品数据时序重建