标签: 字符串

Manacher算法原理与实现

Manacher算法原理与实现

概述

Manacher算法是一种用于快速计算字符串中回文串长的算法,复杂度为O(n)。下面介绍该算法的原理及实现细节。

原理

我们从朴素地查找回文串开始,我们枚举回文串的回文中心,并向两边扩展,这种算法是O(n^2)的。我们来想一个问题,如果有串abcbaabcbaabcbaabcba,它本身是回文的,而子串abcba也是回文的,在寻找过程中,abcba可能会被多次遍历到。我们能否把已经找到过的abcba信息利用起来,优化算法呢。Manacher就实现了这种优化。
首先我们要对字符串进行一步处理,因为奇回文abcba和偶回文abba存在不同的模式,需要分别考虑,我们把字符之间都插入一个特殊字符,如#,就可以把偶回文也转化为奇回文的情况了,如abba$#a#b#b#a#。其中,字符串0位置的字符$是用来防止越界的。
这里给一个对齐表方便观察。

manacher - Manacher算法原理与实现

我们考虑在上面的例子中,#a#b#c#b#a#a#b#c#b#a#存在子回文串#a#b#c#b#a#,在下一次遇到该回文串的回文中心时,我们可以考虑在整个大回文串找到这个中心的对称点,就像这样:

#a#b#c#b#a#a#b#c#b#a#
     ^         ^

并把对称点的答案直接拿过来用。如果我们用mx代表当前极大回文串右端点,id代表当前极大回文串中心位置,p数组代表某个位置为中心的最长回文串半径,那么代码写出来应该是这样的:

if(i < mx) p[i] = std::min(p[2 * id - i], mx - i);

然后我们再来扩展修正这个p值,动态地维护当前极大回文串即可。
在处理过的串中p数组代表的半径,而我们发现p数组的值实际上为原串中该位置为中心的最长回文串长+1,因此可以这样直接计算出答案。

实现

本代码可以通过【P3805】【模板】manacher算法 – 洛谷一题。

// Code by KSkun, 2018/6
#include <cstdio>
#include <cctype>
#include <cstring>

#include <algorithm>

const int MAXN = 22000005;

char s1[MAXN], s2[MAXN];
int n, tot, p[MAXN], mx;

inline int manacher() {
    s2[tot++] = '$'; s2[tot++] = '#';
    for(int i = 1; i <= n; i++) {
        s2[tot++] = s1[i]; s2[tot++] = '#';
    }
    int mxlen = 0, mx = 0, id;
    for(int i = 1; i < tot; i++) {
        if(i < mx) p[i] = std::min(p[(id << 1) - i], mx - i);
        else p[i] = 1;
        while(s2[i - p[i]] == s2[i + p[i]]) p[i]++;
        if(mx < i + p[i]) {
            id = i; mx = i + p[i];
        }
        mxlen = std::max(mxlen, p[i] - 1);
    }
    return mxlen;
}

int main() {
    scanf("%s", s1 + 1); n = strlen(s1 + 1);
    printf("%d", manacher());
    return 0;
}

参考资料

[NOI2016]优秀的拆分 题解

[NOI2016]优秀的拆分 题解

题目地址:洛谷:【P1117】[NOI2016]优秀的拆分 – 洛谷、BZOJ:Problem 4650. — [Noi2016]优秀的拆分

题目描述

如果一个字符串可以被拆分为 AABB 的形式,其中 A和 B是任意非空字符串,则我们称该字符串的这种拆分是优秀的。
例如,对于字符串 aabaabaa,如果令 A=aab,B=a,我们就找到了这个字符串拆分成 AABB的一种方式。
一个字符串可能没有优秀的拆分,也可能存在不止一种优秀的拆分。比如我们令 A=a,B=baa,也可以用 AABB表示出上述字符串;但是,字符串 abaabaa 就没有优秀的拆分。
现在给出一个长度为 n的字符串 S,我们需要求出,在它所有子串的所有拆分方式中,优秀拆分的总个数。这里的子串是指字符串中连续的一段。
以下事项需要注意:

  1. 出现在不同位置的相同子串,我们认为是不同的子串,它们的优秀拆分均会被记入答案。
  2. 在一个拆分中,允许出现 A=B。例如 cccc 存在拆分 A=B=c。
  3. 字符串本身也是它的一个子串。

题意简述

问一个字符串所有子串中,能够将子串拆分成AABB的拆分方案总数。其中A、B是任意非空串,A=B同样合法。

输入输出格式

输入格式:
每个输入文件包含多组数据。输入文件的第一行只有一个整数 T,表示数据的组数。保证 1≤T≤10。
接下来 T行,每行包含一个仅由英文小写字母构成的字符串 S,意义如题所述。

输出格式:
输出 T行,每行包含一个整数,表示字符串 S 所有子串的所有拆分中,总共有多少个是优秀的拆分。

输入输出样例

输入样例#1:

4
aabbbb
cccccc
aabaabaabaa
bbaabaababaaba

输出样例#1:

3
5
4
7

说明

我们用 S[i,j]表示字符串 S第 i 个字符到第 j个字符的子串(从 1开始计数)。
第一组数据中,共有 3个子串存在优秀的拆分:
S[1,4]=aabb,优秀的拆分为 A=a,B=b;
S[3,6]=bbbb,优秀的拆分为 A=b,B=b;
S[1,6]=aabbbb,优秀的拆分为 A=a,B=bb。
而剩下的子串不存在优秀的拆分,所以第一组数据的答案是 3。
第二组数据中,有两类,总共 4 个子串存在优秀的拆分:
对于子串 S[1,4]=S[2,5]=S[3,6]=cccc,它们优秀的拆分相同,均为 A=c,B=c,但由于这些子串位置不同,因此要计算 3 次;
对于子串 S[1,6]=cccccc,它优秀的拆分有 2 种:A=c,B=cc 和 A=cc,B=c,它们是相同子串的不同拆分,也都要计入答案。
所以第二组数据的答案是 3+2=5。
第三组数据中,S[1,8] 和 S[4,11] 各有 2 种优秀的拆分,其中 S[1,8] 是问题描述中的例子,所以答案是 2+2=4。
第四组数据中,S[1,4],S[6,11],S[7,12],S[2,11],S[1,8] 各有 1种优秀的拆分,S[3,14] 有 2 种优秀的拆分,所以答案是 5+2=7。
对于全部的测试点,保证 1≤T≤10。以下对数据的限制均是对于单组输入数据而言的,也就是说同一个测试点下的 T组数据均满足限制条件。
我们假定 n 为字符串 S 的长度,每个测试点的详细数据范围见下表:
All n≤30000

题解

参考资料:

我们考虑对于每个位置i,统计两个信息presuf,表示前缀i后缀为AA的个数与后缀i前缀为AA的个数。答案显然就是 \displaystyle \sum_{i=1}^n pre[i]suf[i+1]
后缀的前缀和前缀的后缀问题显然可以用SA解决(方法参见:后缀数组(倍增)算法原理及应用 | KSkun’s Blog),但是我们需要一个统计的思路。
考虑枚举AA模式中一段A的串长k,然后在原串中每k个字符设置一个关键点,对于任意一个A串长为k的AA串,一定恰好经过两个相邻的关键点kt、k(t+1)。我们考虑求出两关键点的前缀LCS长度l和后缀LCP长度r,如果 (l+r) \geq k,则我们就找到了一个符合条件的AA串。
noi16 string1 - [NOI2016]优秀的拆分 题解
考虑找到的每个AA串对presuf造成的影响。设相邻的两个关键点靠前的为i,靠后的为j。能够产生AA串作为前缀的位置,这一段位置应当是[j-l+k, j+r-1]。对应示例图,我们可以发现这段区间对应的是A’这个串第k个字符到末尾,这是由于A部分此时至少要保留[i-l+1, j-l-1]这一段,也就对应A’部分的[i-l+k+1, j-l+k-1]一段。而作为后缀的位置是[i-l+1, i+r-k],原理相同。那么实际上每次的操作就是对presuf中连续一段区间+1,可以将操作差分降低复杂度。
这样一通操作下来的复杂度实际上是O(Tn \log n)的,数据范围不是很卡。

代码

// Code by KSkun, 2018/5
#include <cstdio>
#include <cctype>
#include <cstring>

#include <algorithm>

typedef long long LL;

const int MAXN = 100005;

int Log2[MAXN];

inline void callog2() {
    for(int i = 2; i < MAXN; i++) {
        Log2[i] = Log2[i >> 1] + 1;
    }
}

struct SuffixArray {
    char s[MAXN];
    int n, m, sa[MAXN], t[MAXN], t1[MAXN], c[MAXN], rnk[MAXN], hei[MAXN], st[MAXN][25];
    inline void init() {
        memset(this, 0, sizeof(SuffixArray));
    }
    inline void calsa() {
        int *x = t, *y = t1;
        for(int i = 1; i <= m; i++) c[i] = 0;
        for(int i = 1; i <= n; i++) c[x[i] = s[i]]++;
        for(int i = 1; i <= m; i++) c[i] += c[i - 1];
        for(int i = n; i >= 1; i--) sa[c[x[i]]--] = i;
        for(int k = 1; k <= n; k <<= 1) {
            int p = 0;
            for(int i = n - k + 1; i <= n; i++) y[++p] = i;
            for(int i = 1; i <= n; i++) if(sa[i] > k) y[++p] = sa[i] - k;
            for(int i = 1; i <= m; i++) c[i] = 0;
            for(int i = 1; i <= n; i++) c[x[y[i]]]++;
            for(int i = 1; i <= m; i++) c[i] += c[i - 1];
            for(int i = n; i >= 1; i--) sa[c[x[y[i]]]--] = y[i];
            std::swap(x, y);
            p = x[sa[1]] = 1;
            for(int i = 2; i <= n; i++) {
                x[sa[i]] = (y[sa[i - 1]] == y[sa[i]] && y[sa[i - 1] + k] == y[sa[i] + k]) 
                    ? p : ++p;
            }
            if(p >= n) break;
            m = p;
        }
        memcpy(rnk, x, sizeof(rnk));
        int k = 0;
        for(int i = 1; i <= n; i++) {
            int j = sa[rnk[i] + 1];
            if(k) k--;
            if(!j) continue;
            while(s[i + k] == s[j + k]) k++;
            hei[rnk[i]] = k;
        }
    }
    inline void calst() {
        for(int i = 1; i <= n; i++) {
            st[i][0] = hei[i];
        }
        for(int i = 1; i <= 20; i++) {
            for(int j = 1; j + (1 << i) <= n; j++) {
                st[j][i] = std::min(st[j][i - 1], st[j + (1 << (i - 1))][i - 1]);
            }
        }
    }
    inline int querylcp(int x, int y) {
        x = rnk[x]; y = rnk[y];
        if(x > y) std::swap(x, y);
        int t = Log2[y - x];
        return std::min(st[x][t], st[y - (1 << t)][t]);
    }
} saa, sab;

int T, n, pre[MAXN], suf[MAXN];

inline int lcp(int x, int y) {
    return saa.querylcp(x, y);
}

inline int lcs(int x, int y) {
    return sab.querylcp(n - x + 1, n - y + 1);
}

int main() {
    callog2();
    scanf("%d", &T);
    while(T--) {
        saa.init(); sab.init();
        memset(pre, 0, sizeof(pre));
        memset(suf, 0, sizeof(suf));
        saa.m = sab.m = 256;
        scanf("%s", saa.s + 1);
        saa.n = sab.n = n = strlen(saa.s + 1);
        for(int i = 1; i <= n; i++) {
            sab.s[i] = saa.s[n - i + 1];
        }
        saa.calsa(); saa.calst();
        sab.calsa(); sab.calst();
        for(int k = 1; k <= n >> 1; k++) {
            for(int i = k, j = i + k; j <= n; i += k, j += k) {
                int l = std::min(lcs(i - 1, j - 1), k - 1), r = std::min(lcp(i, j), k);
                if(l + r >= k) {
                    pre[j - l + k - 1]++; pre[j + r]--;
                    suf[i - l]++; suf[i + r - k + 1]--;
                }
            }
        }
        for(int i = 1; i <= n; i++) {
            pre[i] += pre[i - 1];
            suf[i] += suf[i - 1];
        }
        LL ans = 0;
        for(int i = 1; i <= n; i++) {
            ans += 1ll * pre[i] * suf[i + 1];
        }
        printf("%lld\n", ans);
    }
    return 0;
}
Codeforces Round #476 (Div. 2) [Thanks, Telegram!] 赛后总结

Codeforces Round #476 (Div. 2) [Thanks, Telegram!] 赛后总结

965A Paper Airplanes

题意简述

一张纸可以做s个纸飞机,现在有k个人,每个人要做n个纸飞机,而一包纸有p张,他们想买若干包然后把纸分给每个人让他们做纸飞机,求他们应该买多少包纸才能满足条件。

思路

没什么难度,按照题目的意思求即可。答案是
[eq display=”1″] \lceil \frac{\lceil \frac{n}{s} \rceil k}{p} \rceil [/eq]
打CFR的时候看一眼1A的那种题。

代码

// Code by KSkun, 2018/4
#include <cstdio>
#include <cmath>

int k, n, s, p;

int main() {
    scanf("%d%d%d%d", &k, &n, &s, &p);
    printf("%d", int(ceil(ceil(double(n) / s) * k / p)));
    return 0;
}

965B Battleship

题意简述

给你一个n*n的地图,有的格子可能属于一条船,有的不属于。一条船的长度为k,体现在地图上就是横向或者纵向连续的k个是船的格子。求可能属于的船的数量最多的格子。
数据范围:n 100

思路

枚举每个格子,然后暴力找到它可能属于的船的数量。具体来说,就是上下/左右找一通连通块,注意延伸出去不能超过k-1格不然不能把这个格子包含进去。然后答案就是这个连通块的大小减去k-1。
写了一会,由于当时CFR就写的很丑emm

代码

// Code by KSkun, 2018/4
#include <cstdio>
#include <cmath>

#include <algorithm>

int n, k;
char mmp[105][105];
int ax, ay, ans = -1;

inline int count(int x, int y) {
    if(mmp[x][y] == '#') return 0;
    int cnt = 0, tcnt = 1, bcnt = 0;
    for(int i = x - 1; i >= 1; i--) {
        if(mmp[i][y] == '#') break;
        tcnt++;
    }
    if(tcnt > k) tcnt = k; cnt += tcnt; tcnt = 1;
    for(int i = x + 1; i <= n; i++) {
        if(mmp[i][y] == '#') break;
        tcnt++;
    }
    if(tcnt > k) tcnt = k; cnt += tcnt - 1; tcnt = 1;
    bcnt += std::max(cnt - (k - 1), 0); cnt = 0;
    for(int i = y - 1; i >= 1; i--) {
        if(mmp[x][i] == '#') break;
        tcnt++;
    }
    if(tcnt > k) tcnt = k; cnt += tcnt; tcnt = 1;
    for(int i = y + 1; i <= n; i++) {
        if(mmp[x][i] == '#') break;
        tcnt++;
    }
    if(tcnt > k) tcnt = k; cnt += tcnt - 1; tcnt = 1;
    bcnt += std::max(cnt - (k - 1), 0); cnt = 0;
    return bcnt;
}

int main() {
    scanf("%d%d%d%d", &n, &k);
    for(int i = 1; i <= n; i++) {
        scanf("%s", mmp[i] + 1);
    }
    for(int i = 1; i <= n; i++) {
        for(int j = 1; j <= n; j++) {
            int cnt = count(i, j);
            if(cnt > ans) {
                ans = cnt; ax = i; ay = j;
            }
        }
    }
    printf("%d %d", ax, ay);
    return 0;
}

965C Greedy Arkady

题意简述

k个人想分n块糖。分糖的规则是这样的,先确定每个人每次分到的糖x,先给第一个人x个,然后第二个,一轮给完以后再轮到第一个人,直到剩下的糖数不足x,剩下的糖会直接扔掉。但是有限制每个人最多不能分到糖D次,且x也不能大于M。求第一个人分到糖的最大数目。
数据范围:n: 1e18,M: ≤n,D: 1e3

思路

我们看到D的范围很小,因此我们可以枚举每个人分到的糖的最多次数i。我们可以根据i计算出一个x的范围,而实际上我们肯定想要最大的x,因为第一个人分到糖的数量实际上是xi。这个x很好求:
x = \lfloor \frac{n}{k(i-1)+1} \rfloor
求出x以后看一下超没超M,超了就直接设成M检查x是否能满足i的要求。然后对于x直接计算答案即可。对于一个确定的x,它的答案是xi。
CFR的时候,一没看见剩下的糖扔掉,二没看见D的范围小,而且对数据范围并不敏感,直接导致没想到枚举D。想到了以后应该是很好做的吧。
当时打了个表,发现如果不管剩下的糖扔掉这条,答案在n的两个因数之间是单峰的,写了个找因数+三分找峰,复杂度O(\sqrt{n} + \log^2 n),直接TLE。

代码

// Code by KSkun, 2018/4
#include <cstdio>
#include <cmath>

#include <algorithm>

typedef long long LL;

inline char fgc() {
    static char buf[100000], *p1 = buf, *p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF : *p1++;
}

inline LL readint() {
    register LL res = 0, neg = 1;
    register char c = fgc();
    while(c < '0' || c > '9') {
        if(c == '-') neg = -1;
        c = fgc();
    }
    while(c >= '0' && c <= '9') {
        res = res * 10 + c - '0';
        c = fgc();
    }
    return res * neg;
}

LL n, k, M, D;

int main() {
    n = readint(); k = readint(); M = readint(); D = readint();
    LL ans = 0;
    for(LL i = 1; i <= D; i++) {
        LL lg = log(i - 1) / log(10) + log(k) / log(10); // 会爆LL,log处理一下
        if(lg > log(n) / log(10)) continue; 
        LL x = std::min(M, n / ((i - 1) * k + 1));
        if(!x) continue;
        LL ri = ceil(n / x / double(k));
        if(ri != i) continue;
        ans = std::max(ans, x * i);
    }
    printf("%I64d", (long long) ans);
    return 0;
}

965D Single-use Stones

题意简述

河宽w,一只青蛙最多能跳l这么远。给你河中每个位置的石头数,每个石头只能用一次,用完了就沉入水中了,求最多能让多少青蛙过河。
数据范围:w, l: 1e5

思路

最开始想了个网络流的模型,拆点限流石头个数,然后从i向[i+1, i+l]中的每个石头连出边,但是这样的边数是O(n^2)的空间开不下。
其实我们来想一下,S的出边到[1, l],而[1, l]的出边到[2, l+1],依次类推,我们可以把这样一个区间内的点合起来看做最大流的限制啊!也就是说,\min_{i=l}^{w-1} \{sum_i - sum_{i-l}\}就是答案。
其实CF官解的解释是这样的:二分答案k,对于确定的k,首先当从近到远第i块石头到第i+k块的距离大于l的时候,k只青蛙是过不去的。因为最左边那只就跳不过去了,所以就可以O(n)验证确定的k。简化这个过程也能得到上面的解法。

代码

// Code by KSkun, 2018/4
#include <cstdio>

#include <algorithm>

typedef long long LL;

inline char fgc() {
    static char buf[100000], *p1 = buf, *p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF : *p1++;
}

inline LL readint() {
    register LL res = 0, neg = 1;
    register char c = fgc();
    while(c < '0' || c > '9') {
        if(c == '-') neg = -1;
        c = fgc();
    }
    while(c >= '0' && c <= '9') {
        res = res * 10 + c - '0';
        c = fgc();
    }
    return res * neg;
}

int w, l, a[100005];

int main() {
    w = readint(); l = readint();
    for(int i = 1; i < w; i++) {
        a[i] = readint() + a[i - 1];
    }
    int ans = 1e9;
    for(int i = l; i < w; i++) {
        ans = std::min(ans, a[i] - a[i - l]);
    }
    printf("%d", ans);
    return 0;
}

965E Short Code

题意简述

把n个字符串用它的一个前缀不重复地代表,求最小的代表串长度和。
数据范围:字符串总长: 1e5

思路

如果放在Trie树上看这个问题,就是把一些word标记给提到这个节点的某个父亲的位置,且每个节点只能有一个word标记。考虑一个节点的子树内的word标记全都提前过,现在这个节点上本来就有word标记,那么这个节点的子树的提前也已经完成了,而如果没有word标记,就要从子树中选择一个提前,我们显然应该选择子树中深度较大的那个。逐子树递归完成这个操作,每个子树维护一个可并堆,最后把堆里的元素加个和就完事了。
复杂度O(n \log n)

代码

用了下pb_ds的可并堆,其实好像普通堆O(n \log n)合并也没事?

// Code by KSkun, 2018/4
#include <cstdio>

#include <ext/pb_ds/priority_queue.hpp>

typedef __gnu_pbds::priority_queue<int, std::less<int>, 
    __gnu_pbds::pairing_heap_tag> priority_queue;

const int MAXN = 100005;

int ch[MAXN][26], tot = 1;
bool wrd[MAXN];

inline void insert(char *s) {
    int p = 1;
    for(int i = 0; s[i]; i++) {
        int c = s[i] - 'a';
        if(!ch[p][c]) ch[p][c] = ++tot;
        p = ch[p][c]; 
    } 
    wrd[p] = true;
}

inline void dfs(int u, priority_queue &q, int dep) {
    for(int c = 0; c < 26; c++) {
        if(ch[u][c]) {
            priority_queue p;
            dfs(ch[u][c], p, dep + 1);
            q.join(p);
        }
    }
    if(!wrd[u]) q.pop();
    q.push(dep);
}

int n;
char s[MAXN];

int main() {
    scanf("%d", &n);
    for(int i = 1; i <= n; i++) {
        scanf("%s", s);
        insert(s);
    }
    int ans = 0;
    for(int c = 0; c < 26; c++) {
        if(ch[1][c]) {
            priority_queue q;
            dfs(ch[1][c], q, 1);
            while(!q.empty()) {
                ans += q.top();
                q.pop();
            }
        }
    }
    printf("%d", ans);
    return 0;
}