前言
依然是模拟赛的 T3,依然寄了,所以写篇题解……
题目分析
简化题意
原题面可能给的不太清晰(至少我个人看了好久才看懂)。
有三个数字串 A A A 和 B B B 和 C C C,你需要找到一个 A A A 和 B B B 的公共子序列 D D D,使得它在满足 C C C 是 D D D 的子串的情况下尽可能地长。
思路
设所求的公共子序列为 D D D,考虑将 D D D 分成三个部分:
- A A A 与 B B B 前半部分的公共子序列,这个部分可以用经典的最长公共子序列算法来预处理。
- 为 C C C 的子串,这个部分可以遍历匹配。
- A A A 与 B B B 后半部分的公共子序列,与部分 1 1 1 相似,这个部分可以用经典的最长公共子序列算法来预处理,但注意为方便枚举,要倒着 dp(即 d p i , j dp_{i, j} dpi,j 表示 A A A 的第 i i i 位到第 n n n 位与 B B B 的第 j j j 位到第 m m m 位的最长公共子序列)。
考虑在 A A A 和 B B B 中分别枚举其子序列的三个部分的两个分割点,然后求这三个部分长度之和,答案即所有的情况中长度之和的最大值,时间复杂度 O ( n 4 ) O(n ^ 4) O(n4)。
考虑优化,发现若存在分割点的方案 ( l 1 , r 1 , l 2 , r 2 ) (l_1, r_1, l_2, r_2) (l1,r1,l2,r2) 可以使得 A A A 的 [ l 1 , r 1 ] [l_1, r_1] [l1,r1] 与 B B B 的 [ l 2 , r 2 ] [l_2, r_2] [l2,r2] 的公共子序列为 C C C,另一种满足 r 1 ′ ≥ r 1 r_1' \ge r_1 r1′≥r1 且 r 2 ′ ≥ r 2 r_2' \ge r_2 r2′≥r2 的方案 ( l 1 , r 1 ′ , l 2 , r 2 ′ ) (l_1, r_1', l_2, r_2') (l1,r1′,l2,r2′) 的答案一定没有 ( l 1 , r 1 , l 2 , r 2 ) (l_1, r_1, l_2, r_2) (l1,r1,l2,r2) 优。因为相较于第一种方案来说,第二种第 3 3 3 部分的长度短了而且第 1 1 1 第 2 2 2 部分的长度都没有增加。
所以我们可以预处理出两个数组 nextA
和 nextB
。
n
e
x
t
A
i
nextA_i
nextAi 表示在
A
A
A 中,使区间
[
i
,
r
]
[i, r]
[i,r] 存在为
C
C
C 的子序列的最小的
r
r
r,
n
e
x
t
B
i
nextB_i
nextBi 同理。枚举分割点时,我们就可以只枚举
A
A
A 和
B
B
B 中第
1
1
1 部分与第
2
2
2 部分的分割点,从而
O
(
1
)
O(1)
O(1) 得到
A
A
A 和
B
B
B 中最优的第
2
2
2 部分与第
3
3
3 部分的分割点,时间复杂度
O
(
n
2
)
O(n ^ 2)
O(n2),可以通过本题的数据。
参考代码
#include<bits/stdc++.h>
using namespace std;
const int NR = 3e3;
int dp[2][NR + 10][NR + 10]; // 最长公共子序列 dp 数组
int a[NR + 10]; // A 数列
int b[NR + 10]; // B 数列
int c[NR + 10]; // C 数列
int nxt[2][NR + 10]; // nextA 数组和 nextB 数组
void init(int n, int m, int len){ // 预处理函数
for(int i = 1;i <= n;i++){ // A 与 B 前半部分的公共子序列, 用经典的最长公共子序列算法来计算
for(int j = 1;j <= m;j++){
if(a[i] == b[j]) dp[0][i][j] = max(dp[0][i][j], dp[0][i - 1][j - 1] + 1);
dp[0][i][j] = max(dp[0][i][j], max(dp[0][i - 1][j], dp[0][i][j - 1]));
}
}
for(int i = n;i >= 1;i--){ // A 与 B 后半部分的公共子序列, 注意要倒着 dp
for(int j = m;j >= 1;j--){
if(a[i] == b[j]) dp[1][i][j] = max(dp[1][i][j], dp[1][i + 1][j + 1] + 1);
dp[1][i][j] = max(dp[1][i][j], max(dp[1][i + 1][j], dp[1][i][j + 1]));
}
}
for(int i = 1;i <= n;i++){ // 预处理上文提到的 nextA 数组
int pos = 1, j;
for(j = i;j <= n && pos <= len;j++){ // 遍历匹配
if(a[j] == c[pos]) pos++;
}
if(pos > len) nxt[0][i] = j - 1; // 存在
else nxt[0][i] = 0x3f3f3f3f; // 不存在, 注意要设为无穷大
}
for(int i = 1;i <= m;i++){ // 预处理上文提到的 nextB 数组, 原理与 nextA 相同
int pos = 1, j;
for(j = i;j <= m && pos <= len;j++){
if(b[j] == c[pos]) pos++;
}
if(pos > len) nxt[1][i] = j - 1;
else nxt[1][i] = 0x3f3f3f3f;
}
}
int main(){
int n, m, len;
scanf("%d", &n); // 读入
for(int i = 1;i <= n;i++){
scanf("%d", &a[i]);
}
scanf("%d", &m);
for(int i = 1;i <= m;i++){
scanf("%d", &b[i]);
}
scanf("%d", &len);
for(int i = 1;i <= len;i++){
scanf("%d", &c[i]);
}
init(n, m, len); // 预处理上文提到的两段最长公共子序列和 next 数组
int Max = 0;
for(int l1 = 1;l1 <= n;l1++){ // 枚举 A 中第 1/第 2 部分的分界线
for(int l2 = 1;l2 <= m;l2++){ // 枚举 B 中第 1/第 2 部分的分界线
int r1 = nxt[0][l1]; // A 中第 2/第 3 部分最优的的分界线
int r2 = nxt[1][l2]; // B 中第 2/第 3 部分最优的的分界线
if(r1 > n || r2 > m) continue; // 若不能使第二部分中的子序列为 C, 不满足要求, 直接跳过
Max = max(Max, dp[0][l1 - 1][l2 - 1] + len + dp[1][r1 + 1][r2 + 1]); // 计算答案, 答案 = 第一部分长 + 第二部分长 + 第三部分长
}
}
printf("%d\n", Max == 0 ? -1 : Max); // 注意判断 -1 的情况
return 0;
}