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;
}
相关推荐
一切皆是因缘际会1 小时前
下一代 AI 架构:基于记忆演化与单向投影的安全智能系统
大数据·人工智能·深度学习·算法·安全·架构
falldeep2 小时前
五分钟了解OpenClaw底层架构
人工智能·算法·机器学习·架构
m0_629494732 小时前
LeetCode 热题 100-----16.除了自身以外数组的乘积
数据结构·算法·leetcode
weixin_446260852 小时前
模型能力深度对决:GPT-4o、Claude 3.5和DeepSeek V系列模型的横向评测与未来趋势洞察
人工智能·算法·机器学习
迷途之人不知返2 小时前
优先级队列:priority_queue
数据结构·c++
jieyucx2 小时前
Go 零基础数据结构:顺序表(像「排抽屉」一样学增删改查)
java·数据结构·golang
想唱rap2 小时前
应用层协议与序列化
linux·运维·服务器·网络·数据结构·c++·算法
重生之我是Java开发战士2 小时前
【笔试强训】Week3:重排字符串,分组,DNA序列
算法
We་ct2 小时前
LeetCode 97. 交错字符串:动态规划详解
前端·算法·leetcode·typescript·动态规划