洛谷 P8020 Badania naukowe 题解

前言

依然是模拟赛的 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 分成三个部分:

  1. A A A B B B 前半部分的公共子序列,这个部分可以用经典的最长公共子序列算法来预处理。
  2. C C C 的子串,这个部分可以遍历匹配。
  3. 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 r1r1 r 2 ′ ≥ r 2 r_2' \ge r_2 r2r2 的方案 ( 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 部分的长度都没有增加。

所以我们可以预处理出两个数组 nextAnextB 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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值