SDUT 1008最长公共子序列

题目链接

https://acm.sdut.edu.cn/onlinejudge2/index.php/Home/Index/problemdetail/pid/1008.html

分析

题目类型:变维DP

状态定义

         对于动态规划而言,最难的地方就在于状态的定义,定义一个良好的状态可以极大的帮助我们解决问题,反之,如果定义一个糟糕的状态尽管这个状态是正确的,也很有可能会因为状态转移方程难以确定从而导致无法解决问题。对于这个题目,我们先从普通的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;
}

 

你可能感兴趣的:(解题报告)