📌  相关文章
📜  1到N的两个不同排列中的公共子数组的计数(1)

📅  最后修改于: 2023-12-03 15:13:06.303000             🧑  作者: Mango

1到N的两个不同排列中的公共子数组的计数

介绍

给定1到N的两个不同排列P和Q,求它们的公共子数组的个数。

例如,当N为4时,排列P为[1,2,3,4],排列Q为[3,1,4,2],它们的公共子数组为[1]、[2]、[3]、[4]、[1,2]、[3,4],共6个。

这是一个经典的计算问题,有多种解法,本文将会介绍其中两种常见的解法。

解法一:动态规划法

首先,我们来看看动态规划的解法。

设f[i][j]表示P中以i结尾、Q中以j结尾的公共子数组个数。

当P[i] != Q[j]时,f[i][j] = 0,因为P[i]和Q[j]不相等,它们之间没有公共部分。

当P[i] == Q[j]时,f[i][j]可以由f[i-1][j-1]转移而来(因为以i结尾和以j结尾的公共子数组最后一个元素都是P[i])。

因此,我们可以得到如下的状态转移方程:

f[i][j] = 0 (P[i] != Q[j]) f[i][j] = f[i-1][j-1] + 1 (P[i] == Q[j])

要注意的是,我们需要将f[i][j]初始化为0。同时,由于我们只需要知道f[N][N]的值,可以将f降成一维。

最终,我们可以得到如下的动态规划代码片段:

int dp[MAXN];
memset(dp, 0, sizeof(dp));

int ans = 0;
for (int i = 1; i <= N; i++) {
    int last = 0;
    for (int j = 1; j <= N; j++) {
        int tmp = dp[j];
        if (P[i] == Q[j]) {
            dp[j] = last + 1;
            ans += dp[j];
        } else {
            dp[j] = 0;
        }
        last = tmp;
    }
}

其中,变量ans表示公共子数组的个数,dp数组表示以Q[j]为结尾的公共子数组长度。

解法二:后缀数组法

除了动态规划法外,我们还可以使用后缀数组来解决这个问题。

首先,我们将两个排列拼接起来,并在它们的中间插入一个分隔符,如下所示:

P[1] P[2] P[3] ... P[N] | Q[1] Q[2] Q[3] ... Q[N]

然后,我们可以将该字符串的所有后缀按字典序排序,得到一个后缀数组SA,SA[i]表示排名为i的后缀的起始位置。

接下来,我们只需要在SA中找到相邻的两个排名a和b,它们所对应的后缀分别来自于P和Q,就可以计算它们的公共长度了。

具体地,我们可以使用RMQ算法来维护SA中相邻两个排名所对应的后缀的最长公共前缀长度LCP。最终,公共子数组的个数就是所有LCP的和。

最终,我们可以得到如下的后缀数组代码片段:

// 将P和Q拼接起来
for (int i = 1; i <= N; i++) {
    s[i] = P[i];
    s[i+N+1] = Q[i];
}
s[N+1] = '|';

// 建立后缀数组
build_SA();

// 计算LCP数组
for (int i = 1; i <= n; i++) {
    rk[SA[i]] = i;
}
int h = 0;
for (int i = 1; i <= n; i++) {
    if (rk[i] == 1) {
        continue;
    }
    if (h > 0) {
        h--;
    }
    int j = SA[rk[i]-1];
    while (i+h <= n && j+h <= n && s[i+h] == s[j+h]) {
        h++;
    }
    LCP[rk[i]] = h;
}
LCP[1] = 0;

// 计算公共子数组个数
ll ans = 0;
for (int i = 2; i <= n; i++) {
    if ((SA[i-1] < N+1 && SA[i] > N+1) || (SA[i-1] > N+1 && SA[i] < N+1)) {
        ans += LCP[i];
    }
}

其中,变量n表示字符串长度,rk数组表示每个位置所对应的排名,h表示当前LCP的长度,对应build_SA函数和RMQ算法的实现略去不表。