[CF83E] Two Subsequences 题解

[CF83E] Two Subsequences 题解

思路

定义 overlap(a,b) 为字符串 a 的后缀与 b 的前缀的最大相等的长度,有 |f(a,b)|=|a|+|b|overlap(a,b),下文称匹配为相邻两串的 overlap

观察到每次操作之后,一定有一个序列是以 ai 为结尾的。

所以根据这个性质设计状态:fi,j 表示考虑到 ai,另一个序列以 aj 结尾的最少长度。

但是这个状态设计并不好,因为没法进一步优化,发现 |ai|20,所以尝试把另一个序列的结尾状压进状态表示,另外为了方便,可以做一步转化(不做也可以):最后序列的最短长度就等于总长度减去最多能匹配的长度。

所以可以列出这个状态表示:fi,s 表示考虑到 ai,另一个序列结尾为 s 的最大匹配数量。

推出状态转移方程为:(为了方便书写,这里省略和状态原值取 max 的步骤)

fi,sfi1,s+overlap(ai1,ai)fi,ai1fi1,s+overlap(s,ai)

  • 第一个转移代表把 ai1ai 放到同一个序列里面;

  • 第二个转移代表 ai1ai 不在同一个序列里面,这种情况下要枚举另一个序列结尾本来是什么来转移。

这样是 O(2|a|n) 的,会超时,考虑优化:

首先发现第一个转移是对所有的 fi,s 都加上一个值之后取 max,因为是对所有状态的操作,所以每次加上的这个值可以使用一个标记 tag 维护。

那么瓶颈就到了第二个转移上面,发现第二个转移的特点是只有一个状态发生了改变,并且每次加上的值都介于 0|a| 之间,非常小,考虑枚举加上的值,也就是上一个串 s 的后缀和 ai 前缀匹配数。

这样就可以把第二个转移写成:

fi,ai1maxk=0|a|(maxs|a|k+1|a|=ai1k(fi1,s)+k)

进一步发现第二个 max 可以预处理出来,具体而言,定义 gk,xi 表示 maxs|a|k+1|a|=x(fi1,s)

g 代入原式:

fi,ai1maxk=0|a|(gk,ai1ki+k)

这样如果得到了 g 数组,我们就可以实现对 fO(|a|) 转移。

如何得到 g 呢,我们发现对于 gi 而言它的大部分转移都和 gi1 相似,只会相差 Δtag,而唯一一个会改变的是第二维为 ai1 的状态,这个每次更新完 f 之后对 g 进行更新即可。

之后进行滚动数组优化即可通过此题。

实现

注意一些细节,所有的 f 状态是默认会加上 tag 的,所以转移的时候要减掉 Δtag 来转移。

时间复杂度:O(n|a|n|a|2),这个波浪号取决于你怎么实现 overlap,时间允许当然暴力好写,追求极致可以写 Exkmp。

// Problem: Two Subsequences
// Author: Moyou
// Copyright (c) 2023 Moyou All rights reserved.
// Date: 2023-12-06 22:07:10

#include <algorithm>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 2e5 + 10;

string a[N];
int n, m, f[(1 << 20) + 5], g[22][(1 << 20) + 5], tag;
int overlap(string a, string b) {
    for(int i = m; ~i; i --) {
        bool flg = 1;
        for(int j = 0; j < i && flg; j ++)
            if(a[m - i + j] != b[j]) flg = 0;
        if(flg) return i;
    }
    return 0;
}
int change(string x) {
    int t = 0;
    for(int i = 0; i < m; i ++)
        t = t * 2 + (x[i] - '0');
    return t;
}

signed main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    cin >> n;
    for(int i = 1; i <= n; i ++) cin >> a[i];
    m = a[1].size();
    memset(f, -0x3f, sizeof f), memset(g, -0x3f, sizeof g);
    for(int i = 2, delta = 0; i <= n; i ++) {
        delta = overlap(a[i - 1], a[i]);
        int now = change(a[i - 1]);
        tag += delta;
        f[now] = max(-delta, f[now]); // 这个转移来自于那些本来第二个序列里面没有放东西的
        for(int j = 0, t = 0; j <= m; j ++) {
            f[now] = max(f[now], g[j][t] + j - delta);
            if(j < m) t = t * 2 + (a[i][j] - '0');
        }
        for(int j = 0, t = 0; j <= m; j ++) {
            g[j][t] = max(g[j][t], f[now]);
            if(j < m) t += (1 << j) * (a[i - 1][m - j - 1] - '0');
        }
    }
    int ans = tag;
    for(int i = 0; i < (1 << m); i ++)
        ans = max(ans, f[i] + tag);
    cout << n * m - ans << '\n';
    return 0;
}

posted @   MoyouSayuki  阅读(19)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
:name :name
点击右上角即可分享
微信分享提示