N 个字符串最长公共子序列(LCS)求解问题

从一个给定的串中删去(不一定连续地删去)0个或0个以上的字符,剩下地字符按原来顺序组成的串。例如:" ","a","xb","aaa","bbb","xabb","xaaabbb"都是串"xaaabbb"的子序列。(例子中的串不包含引号。)

编程求N个非空串的最长公共子序列的长度。限制:2<=N<=100;N个串中的字符只会是数字0,1,...,9或小写英文字母a,b,...,z;每个串非空且最多含100个字符;N个串的长度的乘积不会超过30000。

输入格式:

文件第1行是一个整数T,表示测试数据的个数(1<=T<=10)。接下来有T组测试数据。各组测试数据的第1行是一个整数Ni,表示第i组数据中串的个数。各组测试数据的第2到N+1行中,每行一个串,串中不会有空格,但行首和行末可能有空格,这些空格当然不算作串的一部分。

输出格式:

输出T行,每行一个数,第i行的数表示第i组测试数据中Ni个非空串的最长公共子序列的长度。

输入样例:

复制代码
1

3

ab

bc

cd

输出样例:

复制代码
0

解决方案:

多维状态 + 记忆化搜索(DFS + 缓存)

简单说:

  • 状态 = 每个字符串当前看到第几个字符
  • 缓存 = 算过的状态存起来,不再重复算
  • 枚举 = 一个个试所有字符,看是不是所有串都有

一、整体结构(先看大框架)

复制代码
// 1. 头文件 + 宏定义
// 2. 全局变量(方便递归访问)
// 3. trim():去掉字符串首尾空格
// 4. init_mul():状态映射(多维转一维)
// 5. get_state():计算状态编号
// 6. dfs():记忆化搜索(核心算法)
// 7. main():输入输出 + 调用函数

二、逐行逐模块精讲

1. 头文件 + 宏定义

复制代码
#include <stdio.h>   // 输入输出
#include <string.h>  // 字符串处理、memset
#include <ctype.h>   // isalnum() 判断字母/数字

#define MAX_N 105      // 最多 100 个字符串
#define MAX_LEN 105    // 每个串最长 100 字符
#define MAX_STATE 30005// 状态总数上限(题目说乘积 ≤30000)
  • 这些是安全上限,保证不越界。

2. 全局变量(非常关键)运行

复制代码
int n;                  // 当前这组数据有几个字符串
char str[MAX_N][MAX_LEN];// 存所有字符串
int len[MAX_N];         // 每个字符串的长度
int dp[MAX_STATE];      // 记忆化缓存:dp[状态] = 这个状态的LCS长度
int mul[MAX_N];         // 状态映射用的乘数数组

为什么用全局? 因为递归函数 dfs 要反复访问这些数据,全局最方便、不麻烦传参。


3. trim () 函数:去掉字符串首尾空格

复制代码
void trim(char *s) {
    int i, j;
    int l = strlen(s);

    // 从前跳过空格
    for (i = 0; i < l && isspace(s[i]); i++);

    // 从后跳过空格
    for (j = l - 1; j >= 0 && isspace(s[j]); j--);

    // 如果全是空格,清空
    if (i > j) {
        s[0] = '\0';
        return;
    }

    // 把中间有效字符复制到开头
    int idx = 0;
    for (int k = i; k <= j; k++)
        s[idx++] = s[k];
    s[idx] = '\0';
}

作用 :题目说输入行首行末可能有空格,不算串内容,必须删掉。


4. init_mul ():状态映射(核心工具)

先懂一个问题:

N 个字符串,每个有一个下标:i1, i2, i3 ... iN这是 N 维坐标,C 语言不能直接开 N 维数组。

解决方法:

把 N 个下标 压成一个数字(状态编号),用乘法。

复制代码
void init_mul() {
    mul[n-1] = 1;  // 最后一维的乘数是 1
    for (int i = n-2; i >= 0; i--) {
        mul[i] = mul[i+1] * len[i+1];
    }
}

例子:3 个串,长度分别是 2,2,2

  • mul[2] = 1
  • mul[1] = 1 * 2 = 2
  • mul[0] = 2 * 2 = 4

下标 (0,1,0) → 0×4 + 1×2 + 0×1 = 2 下标 (1,1,1) → 1×4 +1×2 +1×1 = 7

作用:把 N 维坐标 → 1 个数字,就能用一维数组存。


5. get_state ():计算状态编号

复制代码
int get_state(int idx[]) {
    int state = 0;
    for (int i = 0; i < n; i++) {
        state += idx[i] * mul[i];
    }
    return state;
}

输入:idx[0], idx[1] ... idx[n-1](每个串当前位置)输出:一个唯一的数字,代表这个状态。


🔥 最核心:dfs () 递归函数(算法灵魂)

复制代码
int dfs(int idx[]) {

输入:idx[] = 每个字符串现在看到第几个字符功能:返回从这个状态开始,能拿到的最长公共子序列长度


第一步:边界条件 ------ 有字符串看完了

复制代码
for (int i = 0; i < n; i++) {
    if (idx[i] >= len[i])
        return 0;
}

✅ 任何一个串走到头了 → 不可能再找到公共字符 → 返回 0。


第二步:查缓存 ------ 算过就直接拿结果

复制代码
int state = get_state(idx);
if (dp[state] != -1)
    return dp[state];

✅ 记忆化搜索的精髓:同一个状态绝不算第二次


第三步:枚举所有可能字符(0-9、a-z)

复制代码
int maxl = 0;
for (char c = '0'; c <= 'z'; c++) {
    if (!isalnum(c)) continue;

遍历所有合法字符,一个个试:这个字符 c 是不是所有串都有?


第四步:每个串都找到当前位置后第一个 c

复制代码
int ok = 1;
int ni[MAX_N]; // 下一个状态的下标

for (int i = 0; i < n; i++) {
    int p;
    // 从 idx[i] 开始往后找 c
    for (p = idx[i]; p < len[i]; p++) {
        if (str[i][p] == c) break;
    }
    // 有一个串找不到 c → 这个字符不能用
    if (p >= len[i]) {
        ok = 0;
        break;
    }
    ni[i] = p + 1; // 下一个状态从 p+1 开始
}

第五步:能用这个字符 → 递归算下一个状态

复制代码
if (ok) {
    int now = 1 + dfs(ni);
    if (now > maxl) maxl = now;
}
  • 选了这个 c → 长度 +1
  • 剩下的长度 = 递归下一个状态
  • 记录最大的结果

第六步:缓存结果 + 返回

复制代码
dp[state] = maxl;
return maxl;

三、main 函数:流程控制运行

复制代码
int main() {
    int T;
    scanf("%d", &T); // 测试组数

    while (T--) {
        scanf("%d", &n); // 字符串个数
        getchar();       // 吃掉换行

        // 读取每个串 + 去空格
        for (int i = 0; i < n; i++) {
            fgets(str[i], MAX_LEN, stdin);
            trim(str[i]);
            len[i] = strlen(str[i]);
        }

        memset(dp, -1, sizeof(dp)); // 缓存初始化:-1=未计算
        init_mul(); // 初始化状态映射

        int st[MAX_N] = {0}; // 初始状态:所有串从 0 开始
        printf("%d\n", dfs(st));
    }
    return 0;
}

四、用样例走一遍

样例输入

复制代码
1
3
ab
bc
cd

三个串:

  1. ab
  2. bc
  3. cd

初始状态:idx = [0,0,0]

开始枚举字符:

  • a:串 2、3 没有 → 不行
  • b:串 3 没有 → 不行
  • c:串 1 没有 → 不行
  • d:串 1、2 没有 → 不行其他字符都没有。

所以 maxl = 0 → 输出 0。

完整代码:

复制代码
#include <stdio.h>
#include <string.h>
#include <ctype.h>

#define MAX_N 105
#define MAX_LEN 105
#define MAX_STATE 30005

int n;
char str[MAX_N][MAX_LEN];
int len[MAX_N];
int dp[MAX_STATE];
int mul[MAX_N];

// 去掉字符串首尾空格
void trim(char *s) {
    int i, j;
    int l = strlen(s);
    for (i = 0; i < l && isspace(s[i]); i++);
    for (j = l - 1; j >= 0 && isspace(s[j]); j--);
    if (i > j) {
        s[0] = '\0';
        return;
    }
    int idx = 0;
    for (int k = i; k <= j; k++) s[idx++] = s[k];
    s[idx] = '\0';
}

// 初始化状态映射乘数
void init_mul() {
    mul[n-1] = 1;
    for (int i = n-2; i >= 0; i--) {
        mul[i] = mul[i+1] * len[i+1];
    }
}

// N维下标 → 一维状态
int get_state(int idx[]) {
    int state = 0;
    for (int i = 0; i < n; i++) {
        state += idx[i] * mul[i];
    }
    return state;
}

// 记忆化搜索求 LCS
int dfs(int idx[]) {
    // 任意串走完,返回0
    for (int i = 0; i < n; i++) {
        if (idx[i] >= len[i]) return 0;
    }

    int state = get_state(idx);
    if (dp[state] != -1) return dp[state];

    int maxl = 0;
    // 枚举所有可能字符:0-9, a-z
    for (char c = '0'; c <= 'z'; c++) {
        if (!isalnum(c)) continue;

        int ok = 1;
        int ni[MAX_N];
        for (int i = 0; i < n; i++) {
            int p;
            for (p = idx[i]; p < len[i]; p++) {
                if (str[i][p] == c) break;
            }
            if (p >= len[i]) {
                ok = 0;
                break;
            }
            ni[i] = p + 1;
        }

        if (ok) {
            int now = 1 + dfs(ni);
            if (now > maxl) maxl = now;
        }
    }

    dp[state] = maxl;
    return maxl;
}

int main() {
    int T;
    scanf("%d", &T);
    while (T--) {
        scanf("%d", &n);
        getchar(); // 吞换行

        for (int i = 0; i < n; i++) {
            fgets(str[i], MAX_LEN, stdin);
            trim(str[i]);
            len[i] = strlen(str[i]);
        }

        memset(dp, -1, sizeof(dp));
        init_mul();

        int st[MAX_N] = {0};
        printf("%d\n", dfs(st));
    }
    return 0;
}
相关推荐
代码中介商13 分钟前
排序算法完全指南(六):希尔排序深度详解
java·算法·排序算法
Lumbrologist27 分钟前
【C++】零基础入门 · 第 3 节:条件判断(if、switch)
开发语言·c++·算法
sukioe27 分钟前
深入理解 MySQL 索引:底层数据结构与 B+ 树设计原理
数据结构·mysql·oracle
TDengine (老段)31 分钟前
TDengine MemTable 深度解析 — 内存写入缓冲区的数据结构与生命周期
大数据·数据结构·数据库·物联网·时序数据库·tdengine·涛思数据
codealy33 分钟前
Rust 核心理论: 高并发与异步(四)
算法·rust
yh弓长38 分钟前
算法积累笔记
java·算法
-To be number.wan43 分钟前
算法日记 | C++ 结构体
数据结构·学习·算法
xier_ran1 小时前
【infra之路】从“三堵叹息之墙”到异构计算的狂飙
开发语言·c++·算法
头歌实践平台1 小时前
LL(1)文法分析
算法
计算机安禾1 小时前
【算法分析与设计】第6篇:动态规划的原理:最优子结构与重叠子问题
算法