从一个给定的串中删去(不一定连续地删去)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
三个串:
- ab
- bc
- 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;
}