LeetCode 97. 交错字符串 - 二维DP经典题解(C语言实现)

这道题是经典的二维 DP 题,非常适合用来练习"前缀 + 坐标路径"的思路。leetcode+1​

题目与交错定义

给定三个字符串 s1、s2、s3,判断 s3 是否可以由 s1 和 s2 交错组成。leetcode

交错的含义是:保持 s1、s2 各自字符相对顺序不变,把它们按某种顺序"插"在一起,形成 s3。leetcode

例如:s1 = "aabcc",s2 = "dbbca",可以拆成 "aa" + "bc" + "c" 和 "dbbc" + "a",交错后得到 "aadbbcbcac",所以返回 true。leetcode

DP 状态设计:坐标视角

定义 dpij:是否可以用 s1 的前 i 个字符和 s2 的前 j 个字符,组成 s3 的前 i + j 个字符。leetcode

因为 i、j 都要包括"前 0 个字符"的情况,所以 i ∈ 0, m,j ∈ 0, n,需要开一个 (m+1) x (n+1) 的 dp 数组。leetcode+1​

可以把 (i, j) 看成一张网格上的坐标:

  • 向下走一步 (i-1, j) -> (i, j) 表示多用一个 s1 的字符。
  • 向右走一步 (i, j-1) -> (i, j) 表示多用一个 s2 的字符。

这样,一条从 (0,0) 走到 (m,n) 的路径,就对应着一种"取字符顺序"的交错方式。

边界与初始化细节

长度剪枝非常重要:如果 len(s1) + len(s2) != len(s3),直接返回 false,无需 DP。leetcode+1​

dp00 = true:用空串 s1 的前 0 个 + 空串 s2 的前 0 个,自然可以组成 s3 的前 0 个(空串)。leetcode+1​

第一行 dp0j:只使用 s2 的前 j 个字符,判断能否等于 s3 的前 j 个:

  • 递推关系:dp0j = dp0j-1 && (s2j-1 == s3j-1)。leetcode

第一列 dpi0:只使用 s1 的前 i 个字符,判断能否等于 s3 的前 i 个:

  • 递推关系:dpi0 = dpi-10 && (s1i-1 == s3i-1)。leetcode

在代码中,这部分写成了边界分支:

c 复制代码
dp[0][0] = true;
for (i = 0; i <= s1_len; i++) {
    for (j = 0; j <= s2_len; j++) {
        if (i == 0 && j == 0) continue;
        else if (i == 0) {
            if (s2[j - 1] == s3[i + j - 1])
                dp[i][j] = dp[i][j - 1];
        } else if (j == 0) {
            if (s1[i - 1] == s3[i + j - 1])
                dp[i][j] = dp[i - 1][j];
        } ...
    }
}

可以看到,i == 0 的时候只从左边来,j == 0 的时候只从上边来,语义完全对应"只用一个字符串"的情况。leetcode

核心转移:如何体现"连续多字符交错"

很多人一开始会有疑惑:转移看起来只是在比较一个字符:

d p i j = ( d p i − 1 j ∧ s 1 i − 1 = = s 3 i + j − 1 ) ∨ ( d p i j − 1 ∧ s 2 j − 1 = = s 3 i + j − 1 ) dpij = (dpi-1j \wedge s1i-1 == s3i+j-1) \vee (dpij-1 \wedge s2j-1 == s3i+j-1) dpij=(dpi−1j∧s1i−1==s3i+j−1)∨(dpij−1∧s2j−1==s3i+j−1)

那像 "aa" + "dbbc" + "bc" 这种中间连续好几个字符来自同一个串的情况,怎么体现呢?

关键在于

dpi-1j 或 dpij-1 本身就已经包含了"一整段连续来自某个字符串"的历史信息。

例如连续从 s2 取 "dbbc",在网格上就是从 (2,0) 走到 (2,4) 的四步右移,每一步都用:

  • dp21 依赖 dp20
  • dp22 依赖 dp21
  • dp23 依赖 dp22
  • dp24 依赖 dp23

每次只比较一个字符,但多次叠加,最终就形成"连续几个字符都从 s2 取"的效果。

代码对应实现

c 复制代码
else {
    if (s3[i + j - 1] == s1[i - 1] && dp[i - 1][j])
        dp[i][j] = true;
    else if (s3[i + j - 1] == s2[j - 1] && dp[i][j - 1])
        dp[i][j] = true;
    else
        dp[i][j] = false;
}

这里:

  • 从上方来:说明当前字符来自 s1,并且之前 (i-1, j) 这条路径是合法的交错。
  • 从左边来:说明当前字符来自 s2,并且之前 (i, j-1) 这条路径是合法的交错。

任意一种成立,就能把一条合法路径扩展一位。

完整 C 代码与复杂度

完整通过 LeetCode 的 C 代码如下(二维 DP 版):leetcode

c 复制代码
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>

bool isInterleave(char* s1, char* s2, char* s3) {
    int s1_len, s2_len, s3_len;
    bool **dp, result;
    int i, j;

    // 题目输入在 LeetCode 环境下不会是 NULL,这段可以有也可以去掉
    if (s1 == NULL || s2 == NULL || s3 == NULL)
        return false;

    s1_len = strlen(s1);
    s2_len = strlen(s2);
    s3_len = strlen(s3);

    // 长度剪枝
    if (s1_len + s2_len != s3_len)
        return false;

    // 分配 (m+1) x (n+1) 的 DP 数组
    dp = (bool **)malloc((s1_len + 1) * sizeof(bool *));
    for (i = 0; i <= s1_len; i++) {
        dp[i] = (bool *)malloc((s2_len + 1) * sizeof(bool));
        for (j = 0; j <= s2_len; j++) {
            dp[i][j] = false;
        }
    }

    dp[0][0] = true;

    for (i = 0; i <= s1_len; i++) {
        for (j = 0; j <= s2_len; j++) {
            if (i == 0 && j == 0)
                continue;
            else if (i == 0) {
                if (s2[j - 1] == s3[i + j - 1])
                    dp[i][j] = dp[i][j - 1];
            } else if (j == 0) {
                if (s1[i - 1] == s3[i + j - 1])
                    dp[i][j] = dp[i - 1][j];
            } else {
                if (s3[i + j - 1] == s1[i - 1] && dp[i - 1][j])
                    dp[i][j] = true;
                else if (s3[i + j - 1] == s2[j - 1] && dp[i][j - 1])
                    dp[i][j] = true;
                else
                    dp[i][j] = false;
            }
        }
    }

    result = dp[s1_len][s2_len];

    for (i = 0; i <= s1_len; i++) {
        free(dp[i]);
    }
    free(dp);

    return result;
}

时间复杂度 :遍历所有 i ∈ 0,m、j ∈ 0,n,时间为 O ( m n ) O(mn) O(mn)。leetcode

空间复杂度 :dp 为 (m+1) x (n+1),空间为 O ( m n ) O(mn) O(mn)。leetcode

如果之后想写进阶版本,可以在这个转移基础上把 dp 压缩成一维 dpj,把空间降到 O ( n ) O(n) O(n),思路是"当前行只依赖当前行的左边和上一行同一列",循环时注意 j 的遍历顺序即可。leetcode


https://leetcode.com/problems/interleaving-string/submissions/1872425894/?envType=study-plan-v2&envId=top-interview-150

https://leetcode.com/problems/interleaving-string/?envType=study-plan-v2&envId=top-interview-150

相关推荐
benben04414 分钟前
强化学习之DQN算法族(基于gymnasium开发)
算法
Luminous.1 小时前
C语言--day30
c语言·开发语言
玖玥拾1 小时前
C/C++ 数据结构(七)栈、容器适配器
c语言·数据结构·c++··容器适配器
何以解忧,唯有..1 小时前
Go语言循环语句详解:for、range与循环控制
开发语言·算法·golang
謓泽2 小时前
C语言不是语法,是通往机器的地图。
c语言·开发语言
不会C语言的男孩2 小时前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言
想吃火锅10052 小时前
【leetcode】88.合并两个有序数组js
算法
生成论实验室3 小时前
机器人:一个自主运动的系统
人工智能·算法·语言模型·机器人·自动驾驶·agi·安全架构
Qres8213 小时前
算法复键——树状数组
数据结构·算法