树状数组优化最长公共子序列(LCS)

树状数组优化 LCS

众所周知,树状数组可以将 LIS 优化到 O ( n log ⁡ n ) O(n \log n) O(nlogn) 的时间复杂度(见往期:树状数组优化 LIS)。你可能不知道的是,树状数组还能优化 LCS,在 a i a_i ai 两两不同的时候可以达到 O ( n log ⁡ n ) O(n \log n) O(nlogn),但是在构造数据的情况下可以退化到 O ( n 2 log ⁡ n ) O(n^2 \log n) O(n2logn)。不过因为树状数组基于位运算,常数很小,仍能通过。

Luogu 1439 最长公共子序列(特殊版)

给出 1 , 2 , … , n 1,2,\ldots,n 1,2,,n 的两个排列 P 1 P_1 P1 P 2 P_2 P2,求它们的最长公共子序列。

记顺序处理完 P 2 P_2 P2 的前 ( i − 1 ) (i-1) (i1) 位后的 f ( x ) f(x) f(x) 为: P 1 P_1 P1 x x x 位结尾 的子序列,与 P 2 P_2 P2 的前 ( i − 1 ) (i-1) (i1) 位的最大公共部分的长度。

注意: f ( x ) f(x) f(x) 对应的 P 1 P_1 P1 需要以第 x x x 位结尾,而 P 2 P_2 P2 不需要以第 ( i − 1 ) (i-1) (i1) 位结尾。

那么当处理到第 i i i 位时,我们知道只有 P 1 P_1 P1 中数值等于 P 2 , i P_{2, i} P2,i 的位置,它的 f ( x ) f(x) f(x) 才会发生改变。

而根据定义,显然 f ( x ) = max ⁡ 1 ≤ y < x { f ( y ) } + 1 \displaystyle f(x) = \max_{1 \le y < x} \{f(y)\} + 1 f(x)=1y<xmax{f(y)}+1

分析可知,只需要记录在 P 1 P_1 P1 中每个数值出现的位置 x x x,用树状数组即可维护 max ⁡ 1 ≤ y < x { f ( y ) } \displaystyle\max_{1 \le y < x} \{f(y)\} 1y<xmax{f(y)}

最终的答案就是 [ 1 , n ] [1, n] [1,n] f ( x ) f(x) f(x) 的最大值。

时间复杂度 O ( n log ⁡ n ) O(n \log n) O(nlogn)

核心代码:

// 树状数组略
int query(int);
int update(int, int);
int n, id[MAXN];

scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
    int x;
    scanf("%d", &x);
    id[x] = i;
}
for (int i = 1; i <= n; ++i) {
    int x;
    scanf("%d", &x);
    update(id[x], query(id[x]-1) + 1);
}
printf("%d\n", query(n));

普通版

给定序列 a 1 , a 2 , … , a n a_1, a_2, \dots, a_n a1,a2,,an b 1 , b 2 , … , b m b_1, b_2, \dots, b_m b1,b2,,bm,可能存在 i ≠ j i \ne j i=j a i = a j a_i = a_j ai=aj 的情况,求两个序列的 LCS。

每个数并不恰好只出现一次,那么应该怎么办?

开个 vector 记录每个数每次出现的位置,更新的时候对 b i b_i bi a a a 中出现过的每一个位置倒序更新。

倒序更新使得当前 query() 到的都是上一阶段的结果,进而这一阶段处理过的状态,不会对决策产生干扰。

// 输入及树状数组略。
int query(int);
int update(int, int);
int n, m, a[MAXN], b[MAXM];
vector<int> idx[1005];

for (int i = 1; i <= n; ++i) {
    idx[a[i]].push_back(i);
}
for (int i = 1; i <= m; ++i) {
    for (int j = idx[b[i]].size()-1; j >= 0; --j) {
        update(idx[b[i]][j], query(idx[b[i]][j] - 1) + 1);
    }
}
printf("%d\n", query(n));

c x c_x cx x x x a a a 中出现的次数。时间复杂度为 O ( ∑ i = 1 m c b i log ⁡ n ) O(\sum_{i=1}^m c_{b_i} \log n) O(i=1mcbilogn)

∀ 1 ≤ i < j ≤ n \forall 1 \le i < j \le n ∀1i<jn a i = a j a_i = a_j ai=aj,且 ∀ 1 ≤ i < j ≤ m \forall 1 \le i < j \le m ∀1i<jm b i = b j b_i = b_j bi=bj,可以退化到 O ( n m log ⁡ n ) O(nm \log n) O(nmlogn)

a i a_i ai b j b_j bj 分别两两不同,就是特殊版的情况,时间复杂度为 O ( n log ⁡ n ) O(n \log n) O(nlogn)

Luogu 2516 [HAOI2010] 最长公共子序列(求方案数)

给树状数组加个计算方案数的功能就可以了。dijkstra 最短路求方案数怎么求,这里也可以怎么求。注意累加的时候要取模。传引用也行,如果你写成返回 pair<int, int> 也行。另外本人亲测,就算卡到 O ( n 2 log ⁡ n ) O(n^2 \log n) O(n2logn),也就 100~300ms 的样子。今天随手交到了最优解第一?

#include <cstdio>
#include <cstring>
#include <vector>

using namespace std;

const int MAXN = 5e3+5;
const int mod = 1e8;

char buf[MAXN];
int n, m;
vector<int> idx[26];
int f[MAXN], c[MAXN]; // 长度,方案数

void update(int p, int val, int cnt) {
    for (; p <= n; p += p & -p) {
        if (val > f[p]) {
            f[p] = val, c[p] = cnt;
        } else if (val == f[p]) {
            c[p] = (c[p] + cnt) % mod;
        }
    }
}

void query(int p, int& val, int& cnt) {
    val = 0;
    for (; p; p -= p & -p) {
        if (f[p] > val) {
            val = f[p], cnt = c[p];
        } else if (f[p] == val) {
            cnt = (cnt + c[p]) % mod;
        }
    }
}

int main() {
    #ifndef ONLINE_JUDGE
    freopen("lcs.in", "r", stdin);
    freopen("lcs.out", "w", stdout);
    #endif
    scanf("%s", buf + 1);
    n = strlen(buf + 1) - 1;
    for (int i = 1; i <= n; ++i) {
        idx[buf[i] - 'A'].push_back(i);
    }
    scanf("%s", buf + 1);
    m = strlen(buf + 1) - 1;
    for (int i = 1; i <= m; ++i) {
        int pos = buf[i] - 'A';
        for (int j = idx[pos].size()-1; j >= 0; --j) {
            int val, cnt;
            query(idx[pos][j] - 1, val, cnt);
            if (!val) cnt = 1; // 特判一下
            ++val;
            update(idx[pos][j], val, cnt);
        }
    }
    int ans1, ans2;
    query(n, ans1, ans2);
    printf("%d\n%d\n", ans1, ans2);
    return 0;
}

LCS 与 LIS 的关系

// LIS
// a[i] 是离散化后的序列, 最大值为 n, 长度为 m.
for (int i = 1; i <= m; ++i) {
    update(a[i], query(a[i] - 1) + 1);
}
printf("%d\n", query(n));

// LCS
for (int i = 1; i <= m; ++i) {
    for (int j = idx[b[i]].size()-1; j >= 0; --j) {
        update(idx[b[i]][j], query(idx[b[i]][j] - 1) + 1);
    }
}
printf("%d\n", query(n));

对比两段核心代码,不难发现它们的相似之处。

LCS 的 idx[b[i]][j],恰恰是 LIS 中的 a[i]。求 LCS 的过程,就等价于把 idx[b[i]][j] 依次排成一个新的序列,然后对这个新的序列求 LIS。

从某种角度上说,优化的过程,就是把它转化成 LIS,并利用 LIS 的 O ( n log ⁡ n ) O(n \log n) O(nlogn) 进行计算的过程。当然转化后的序列,长度并不一定还是原来的 n n n,极端情况下甚至可以变成 n 2 n^2 n2。所以:看题目的情况,小心负优化。

  • 13
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是使用动态规划空间优化的 C 代码: ```c #include <stdio.h> #include <string.h> #define MAX_N 1000 int dp[2][MAX_N+1]; int lcs(char* s1, char* s2, int len1, int len2) { int i, j; for (i = 0; i <= len1; i++) { for (j = 0; j <= len2; j++) { if (i == 0 || j == 0) { dp[i%2][j] = 0; } else if (s1[i-1] == s2[j-1]) { dp[i%2][j] = dp[(i-1)%2][j-1] + 1; } else { dp[i%2][j] = (dp[(i-1)%2][j] > dp[i%2][j-1]) ? dp[(i-1)%2][j] : dp[i%2][j-1]; } } } return dp[len1%2][len2]; } int main() { char s1[MAX_N], s2[MAX_N]; scanf("%s%s", s1, s2); int len1 = strlen(s1); int len2 = strlen(s2); printf("%d\n", lcs(s1, s2, len1, len2)); return 0; } ``` 以上代码将原来的二维数组 `dp` 改为了两个一维数组,使用滚动数组的方式实现状态转移。这样可以将空间复杂度从 O(n²) 优化为 O(n)。 以下是使用树状数组优化的 C 代码: ```c #include <stdio.h> #include <string.h> #define MAX_N 1000 int c[MAX_N+1]; int dp[MAX_N+1]; int lowbit(int x) { return x & (-x); } void update(int x, int v, int n) { while (x <= n) { if (dp[x] < v) { dp[x] = v; } x += lowbit(x); } } int query(int x) { int res = 0; while (x > 0) { if (dp[x] > res) { res = dp[x]; } x -= lowbit(x); } return res; } int lcs(char* s1, char* s2, int len1, int len2) { int i, j; for (i = 1; i <= len1; i++) { memset(c, 0, sizeof(c)); for (j = 1; j <= len2; j++) { if (s1[i-1] == s2[j-1]) { int v = query(j-1) + 1; update(j, v, len2); } } } return query(len2); } int main() { char s1[MAX_N], s2[MAX_N]; scanf("%s%s", s1, s2); int len1 = strlen(s1); int len2 = strlen(s2); printf("%d\n", lcs(s1, s2, len1, len2)); return 0; } ``` 以上代码使用了树状数组来维护前缀最长公共子序列的长度。时间复杂度为 O(nlogn)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值