https://acm.sdut.edu.cn/onlinejudge2/index.php/Home/Index/problemdetail/pid/1008.html
对于动态规划而言,最难的地方就在于状态的定义,定义一个良好的状态可以极大的帮助我们解决问题,反之,如果定义一个糟糕的状态尽管这个状态是正确的,也很有可能会因为状态转移方程难以确定从而导致无法解决问题。对于这个题目,我们先从普通的LCS入手。
这里假设读者都掌握了LCS的相关知识。在计算两个字符串的最长公共子序列是,显然我们可以定义转态为dp[i][j],用于表示第一个串的前i,第二个串的前j个字符的LCS。并有状态转移方程
if (s[i] == s[j]) dp[i][j] = max(dp[i - 1][j - 1] + 1, max(dp[i][j - 1], dp[i - 1][j]));
else dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
同样的道理我们可以定义三个字符串的状态为dp[i][j][k],n个字符串的状态为dp[x1][x2]```[xn]。不难发现 空间复杂度是O(M^N),M表示字符的大小,N表示字符串的个数,所以单从空间复杂度上考虑这种方式显然不可取。其次,即便是空间没有任何限制,我们也会发现,由于状态转移方程与N相关,所以根本就没有办法进行状态转移(如果OJ对于代码长度没有任何限制,对空间没有任何限制,并且最重要的是你的耐心足够的好,你可以试着写一下)
分析N个串的状态转移方程,不难注意到我们按照普通的方式进行实现需要使用n重for循环,而for循环遍历对于各个位置x1, x2```xn会产生一个固定的次序,以二重for循环为例
void test() {
int cnt = 0;
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 2; ++j) {
printf("%d\n", cnt++);
}
}
}
执行此函数,我们可以看到程序依次输出了0 、1、2、3、4、5
对于不同的二元组(i, j),cnt的值各不相同,因此我们可以借鉴这种思想,定义一个从n元组(x1, x2, ```, xn)到一个整数的哈希函数,从而将状态映射到一个一维数组中去
同样for循环程序为例,我们可以发现,对于二元组(i,j),cnt的值为 i * j + j,对于三元组(i, j, k)我们可以发现cnt = i * j * k + j * k + k。因此我们可以大胆的猜测对于n元组(x1, x2, ```, xn)cnt = x1 * x2 *``` * xn + x2 * x3 *```*xn +``` + xn,所以我们可以递归的计算这个唯一的值cnt,哈希函数递归实现如下
int id(int deep, int idx) {
if (deep < 0) return 0;
return id(deep - 1, idx * len[deep]) + idx * _end[deep];
}
通过上面的散列函数我们可以把状态用一个一维数组保存,即实现记忆化
经过上面的努力,我们成功的定义了状态。这个状态定义的极妙,通过这个状态我们可以比较轻松的进行状态转移
对于LCS问题,我们易知,如果两个串当前位置的字符相同,我们有状态转移方程
dp[i][j] = max(dp[i - 1][j - 1] + 1, max(dp[i][j - 1], dp[i - 1][j]));
同样的道理,对于n个字符串的LCS问题,当当前位置的字符全部相同是,我们可以让位置向前移动,最后的结果是ans = max(ans, dp() + 1),dp()代表子问题最优解,ans代表当前问题最优解;对于不相等的问题,普通的LCS是向后移动,也就是暴力枚举,同样的道理,对于n个字符串的LCS 问题,我们同样的进行暴力枚举
为了实现散列函数我们必须知道两个东西,一是每个字符串的长度,二是当前下标位置,在我的程序中,分别用len和_end表示
为了实现记忆化,我们需要开一个一维数组dp
为了实现比较,我们必须把字符串的信息保存下来,因此我们需要定义二维的字符数组
以上即是程序中涉及的主要的数据结构
#include
#include
#include
using namespace std;
#define N 110
#define M 33000
int n;
int dp[M];
char s[N][M];
int len[N], _end[M];
//通过一个散列函数将状态映射到一个一维数组之中
int id(int deep, int idx) {
if (deep < 0) return 0;
return id(deep - 1, idx * len[deep]) + idx * _end[deep];
}
int dfs() {
int i; //作用域为整个函数,方便以后的使用
int idx = id(n - 1, 1);
if (dp[idx] == -1) { //该子问题没有处理过
//分为匹配和不匹配两种情况
dp[idx] = 0;
for (i = 0; i < n - 1; ++i)
if (s[i][_end[i]] != s[i + 1][_end[i + 1]]) break;
if (i >= n - 1) { //匹配时
for (i = 0; i < n; ++i) {
if (_end[i]) --_end[i]; else break;
}
//状态转移
if (i < n) dp[idx] = max(dp[idx], 1);
else dp[idx] = max(dp[idx], dfs() + 1);
while ((--i) >= 0) _end[i]++; //恢复初始状态
} else { //不匹配时
for (i = 0; i < n; ++i) {
if (_end[i]) {
_end[i]--;
dp[idx] = max(dp[idx], dfs());
_end[i]++;
}
}
}
}
return dp[idx]; //返回最优解
}
int main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%d", &n);
for (int i = 0; i < n; ++i) {
scanf("%s", s[i]);
len[i] = strlen(s[i]); _end[i] = len[i] - 1;
}
memset(dp, -1, sizeof(dp));
printf("%d\n", dfs());
}
return 0;
}