2024-12-23 基础字符串算法
入门篇
字符串的基础概念及存储
基本概念不用多说,字符串就是一堆字符串在一起所构成的一个结构。
存储多有两种:
- 用c++自带的string
- 用字符数组char[]。(笔者习惯用这种方法)
如何输入/输出一个字符串?
//c++自带string
string s;
cin >> s;
cout << s;
//字符数组char
char s[];
scanf("%s", s);
printf("%s", s);
这算是最最基础的内容了,刚入门的小朋友也应该会。
字符串模拟
有关字符串的模拟。语法是大问题?
I 牛客小白月赛33 A
发现要读入空格,但是 cin 和 scanf 貌似都不能避免。可以使用 getline 函数。
基本定义
先了解一些形式化的字符串的定义。
- 这个子树被插了几个数
w
然后考虑怎么做操作。
push_up
注意只需考虑
void pushup(int now) {
w[now] = sxor[now] = 0;
if (son[now][0]) {
w[now] += w[son[now][0]];
sxor[now] ^= sxor[son[now][0]] << 1;
}
if (son[now][1]) {
w[now] += w[son[now][1]];
sxor[now] ^= (sxor[son[now][1]] << 1) | (w[son[now][1]] & 1);
}
}
- 插入
直接模拟,然后对叶子节点的
- 删除
和插入非常类似
void ins(int &now, int len, int v) {
if (!now) now = ++tot;
if (len > 20) {
w[now]++;
return;
}
ins(son[now][v & 1], len + 1, v >> 1);
pushup(now);
}
void del(int &now, int len, int v) {
if (len > 20) {
w[now]--;
return;
}
del(son[now][v & 1], len + 1, v >> 1);
pushup(now);
}
- 全局加一
思考每个数加一的过程——把最后一段
void add(int p) {
swap(son[p][0], son[p][1]);
if (son[p][0]) add(son[p][0]);
pu(p);
}
- trie的合并
可以把trie的合并与线段树合并看成一类。复杂度证明和写法都非常类似。
void merge(int x, int y) {
if (!x || !y) return x | y;
y -> x;
for (int i = 0; i < sigma; i++) {
son[x][i]=merge(son[x][i], son[y][i]);
}
return x;
}
V Ynoi2010 Fusion tree
VI 省选联考2020A卷 树
两道近乎板子的题。。。
可持久化trie
VII bzoj4212
给定
n 个字符串,m 次询问,每次询问给定两个串s_1,s_2 ,查询有多少个串的前缀是s_1 ,后缀是s_2 。
可持久化 trie 的方式和线段树一样,都是在修改的部分新建节点。
考虑对于每一个
code
复杂一点儿的应用
VIII AGC047B
不妨先求一个字符串
那可以把所有前面的字符串反向建trie树,然后反过来在trie树上跑,如果不是第一位就走当前为,如果是第一位就随便走,如果走了当前为就返回。
可这样复杂度不对,考虑优化。对于每个trie树上的节点记录一下这个节点开始有几个字符串在前面有某个字符,然后如果是第一位就直接加上。那怎么维护呢,其实很简单,把插入变成递归插入,统计在回溯的时候记录一下当前哪些字符出现了,如果出现了某个字符回溯时就更新。
IX LG4036
因为有插入所以很难直接维护。考虑使用平衡树,用哈希值维护字符串之间的比较即可。查询是二分答案,平衡树维护子树哈希值。
X CF580E
众所周知,period 对应一个 border。所以如果一个区间
注意,千万不要在 cf 上用自然溢出,否则你会被卡得很惨。
XI AGC044C
观察到这个
XII CF464E
给定一张图,边权为
2^{x_i} ,求s\to t 的最短路。1\le n,m,x_i\le 10^5 。答案对10^9+7 取模。
无法直接把路径长度记到数组里跑 dij。然后据说可以 Hash,我猜测是在比大小部分。但是我一直没有想到怎么去存储这个大数,于是我打开了题解,然后发现其实只要一个数据结构(线段树)就可以做了(因为要动态模拟)。
至于如何比较,其实就是比较两个二进制串的字典序,找到第一个不一样的位置,然后比较下一个位置即可。用线段树维护 Hash 值,时间复杂度
XIII CF504E
给定一棵
n 个节点的树,每个节点有一个小写字母。有m 组询问,每组询问为树上a \to b 和c \to d 组成的字符串的最长公共前缀。感谢洛谷提供的翻译
因为这里是查询最长公共前缀,所以考虑使用二分+Hash来解决。首先找到
貌似思路不是很难想但是代码貌似有点 ex(不是难写,只是单纯的烦)。
kmp
定义 字符串 border 为其前缀等于后缀的集合,也就是
对于每个位置,统计
如果将
下面来证明这个做法是正确的。考虑现在有一个势能函数
利用
kmp 自动机
定义 确定有限状态自动机(DFA)
对于每个状态,和字符集,都可以转移到另一个状态。
定义字符串每个位置为状态代表
XIV CF808G Anthem of Berland
给定
s 串和t 串,其中s 串包含小写字母和问号,t 串只包含小写字母。你需要给把每个问号变成一个小写字母,求出所有匹配中t 匹配s 次数的最大值。|s|,|t|\le 10^5,|s|\cdot|t|\le 10^7.
有了两者长度乘积的限制就不难了,直接dp,设
Code
Boyer-Moore
Z-function
定义 最长公共前缀(lcp)
我们称这是最基础的算入算法。
2.3 代码实现
int fail[N],len[N],go[N][c];
void build(){
s[0]=-1;
len[1]=-1;len[0]=0;tot=1;lst=0;
fail[1]=fail[0]=1;//这里 1 是 odd 节点,0 是 even 节点
}
/*
* 为什么 0 为偶,1 为奇。因为这时如果最长回文后缀是空串就会直接指向 0,比较方便。
*/
void ins(int n,int c){
int p=lst;
while(s[n-len[p]-1]!=s[n])p=fail[p];
if(!go[p][c]){
//因为没有这个节点,所以还需考虑求出其 fail,复杂度分析同理
int v=++tot,k=fail[p];
len[v]=len[p]+2;
while(s[n-len[k]-1]!=s[n])k=fail[k];
fail[v]=go[k][c];
go[p][c]=v;
}
lst=go[p][c];
}
2.4 经典应用
【模板】回文自动机(PAM)
给定一个字符串
s 。保证每个字符为小写字母。对于s 的每个位置,请求出以该位置结尾的回文子串个数。强制在线。1\le |s|\le 5\times 10^5
容易发现以某个位置为结尾的回文串为
[APIO2014]回文串
给你一个字符串,求其所有回文子串出现次数
\times 长度的最大值。1 \leq |s| \leq 3 \times 10^5 。
首先建出
对于它
因为PAM的 feature 所以我们可以从后往前遍历,假设现在遍历到
如果要动态维护
<details>我会 LCT !!!</details>
3 一些扩展
3.1 前后插入的PAM
loj #141. 回文子串
写一个数据结构支持前端后端插入,输出回文子串个数以及位置不同的回文子串个数
由于回文串的 feature,所以回文串的最长回文后缀其实和最长回文前缀是一个东西,那么只用一个
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int base = 4 * 1e5 + 5;
const int maxn = 1e6 + 5;
char s[maxn], t[maxn];
int len[maxn], fail[maxn], go[maxn][26], tot, lstf, lstb, tot_len, dep[maxn];
ll ans;
void build() {
s[0] = -1; len[tot = 1] = -1; len[0] = 0; fail[1] = fail[0] = 1;
}
void ins_f(int n, int c) {
int p = lstf;
while (s[n + len[p] + 1] != s[n]) p = fail[p];
if (!go[p][c]) {
int v = ++tot, k = fail[p];
len[v] = len[p] + 2;
while (s[n + len[k] + 1] != s[n]) k = fail[k];
fail[v] = go[k][c];
dep[v] = dep[fail[v]] + 1;
go[p][c] = v;
}
lstf = go[p][c];
if (len[lstf] == tot_len) lstb = lstf;
ans += dep[lstf];
}
void ins_b(int n, int c) {
int p = lstb;
while (s[n - len[p] - 1] != s[n]) p = fail[p];
if (!go[p][c]) {
int v = ++tot, k = fail[p];
len[v] = len[p] + 2;
while (s[n - len[k] - 1] != s[n]) k = fail[k];
fail[v] = go[k][c];
dep[v] = dep[fail[v]] + 1;
go[p][c] = v;
}
lstb = go[p][c];
if (len[lstb] == tot_len) lstf = lstb;
ans += dep[lstb];
}
int main() {
scanf("%s", t + 1);
build();
int Len = strlen(t + 1);
for (int i = 1; i <= Len; i++) s[i + base] = t[i];
int l = base + 1, r = base + Len;
for (int i = l; i <= r; i++) tot_len++, ins_b(i, s[i] - 'a');
int Q;
scanf("%d", &Q);
while (Q--) {
int op;
scanf("%d", &op);
if (op == 1) {
scanf("%s", t + 1);
Len = strlen(t + 1);
for (int j = 1; j <= Len; j++) {
tot_len++; s[++r] = t[j];
ins_b(r, s[r] - 'a');
}
} else if (op == 2) {
scanf("%s", t + 1);
Len = strlen(t + 1);
for (int j = 1; j <= Len; j++) {
tot_len++; s[--l] = t[j];
ins_f(l, s[l] - 'a');
}
} else {
printf("%lld\n", ans);
}
}
return 0;
}
3.2 不基于势能分析的插入算法与可持久化
3.2.1 不基于势能分析的插入算法
每次插入一个字符
我们在回文树上的每个节点在多存储一个失配转移数组
我们考虑如何维护
插入的时空复杂度都是
int fail[N],len[N],go[N][S],quick[N][S];
void build(){
s[0]=-1;
len[1]=-1;len[0]=0;tot=1;lst=0;
for(int i=0;i<26;i++)quick[0][i]=quick[1][i]=1;
fail[1]=fail[0]=1;//这里 1 是 odd 节点,0 是 even 节点
}
void ins(int n,int c){
int p=quick[lst][c];
if(!go[p][c]){
//因为没有这个节点,所以还需考虑求出其 fail,复杂度分析同理
int v=++tot
len[v]=len[p]+2;
fail[v]=go[quick[p][c]][c];
memcpy(quick[v],quick[fail[v]],sizeof(qucik[fail[v]]));
quick[v][s[n-len[fail[v]]-1]-'a']=fail[v];
go[p][c]=v;
}
lst=go[p][c];
}
3.2.1 后端删除
将 PAM 可持久化,然后删除即相当于查询历史版本。考虑将
然而这玩意似乎并不会有应用,因为出题人也不想搞这么毒瘤
3.3 在 trie 上建 PAM
https://www.codechef.com/problems/TREEPAL
给定一棵以
1 为根的N 个点的树,树上每个点都有一个小写字母。对于树上每个点,求出从根到这个点形成的字符串的最长回文后缀,输出所有点的答案的和。
在 trie 上建 PAM 如果用基础插入算法的话复杂度容易证明是
#include <iostream>
#include <cstdio>
using namespace std;
const int N = 100010;
struct node{
int pre, to;
}edge[N << 1];
int head[N], tt;
struct PAM{
int go[26], quick[26];
int fail;
long long len;
}tr[N];
int tot, cnt[N], Len, top, id[N];
long long ans;
char col[N], s[N];
long long mx[N];
void build() {
s[0] = -1;
tot = 0;
tr[++tot].len = -1;
tr[0].fail = tr[1].fail = 1;
for (int i = 0; i < 26; i++) tr[0].quick[i] = tr[1].quick[i] = 1;
}
int insert(int c, int n, int lst) {
int p = lst;
if (s[n - tr[p].len - 1] != s[n]) p = tr[p].quick[c];
if (!tr[p].go[c]) {
int v = ++tot;
tr[v].len = tr[p].len + 2;
tr[v].fail = tr[tr[p].quick[c]].go[c];
for (int i = 0; i < 26; i++) tr[v].quick[i] = tr[tr[v].fail].quick[i];
tr[v].quick[s[n - tr[tr[v].fail].len] - 'a'] = tr[v].fail;
tr[p].go[c] = v;
}
return tr[p].go[c];
}
void dfs(int x, int fa) {
s[++top] = col[x];
id[x] = insert(s[top] - 'a', top, id[fa]);
mx[x] = max(mx[fa], tr[id[x]].len);
ans += mx[x];
for (int i = head[x]; i; i = edge[i].pre) {
int y = edge[i].to;
if (y == fa) continue;
dfs(y, x);
}
top--;
}
void add(int u, int v) {
edge[++tot] = node{head[u], v};
head[u] = tot;
}
int main() {
cin >> Len;
scanf("%s", col + 1);
for (int i = 1, x, y; i < Len; i++) {
cin >> x >> y;
add(x, y);
add(y, x);
}
build();
dfs(1, 0);
cout << ans;
return 0;
}
3.4 前后插入删除PAM
咕咕咕
4 应用
GDKOI2013大山王国的城市规划(country)
给定一个长度为
n 的字符串s ,现在要从s 中选出尽量多的子串,满足选出的子串都是回文的,并且不存在一个串是另外一个串的子串的情况。(可以部分重叠)n \leq 10^5 。
疑似错题(
首先考虑建出
所以我们先跑传递闭包,然后拆点二分图网络流求最小路径覆盖。
可是这复杂度不是爆炸吗(
如果有更好的方法可以在讨论里提出qwq。
回文串计数
对于一个字符串是
s ,考虑一个四元组(l,r,L,R) ,当S_{l,r} 和S_{L,R} 均为回文串时,且满足\leq l \leq r < L \leq R \leq Len 时,我们称S_{l,r} 和S_{L,R} 为一对互不相交的回文串。本题所求,也即为这种四元组的个数。两个四元组相同当且仅当对应的l,r,L,R 都相同。
直接考虑枚举
双倍回文「SHOI2011」
若要x是双倍回文,它的长度必须是4的倍数,而且x、x的前半部分、x的后半部分都要是回文的,你的任务是,对于给定的字符串,计算它的最长双倍回文子串的长度。
n \leq 5 \times 10^5 。
在将这道题时我们需要引入一个新的指针
我们来看假如求出了这个
那这个
-
若
len_p \leq 2 ,则其half 即为其fail 。 -
若
len_p > 2 ,则在f_i (i 的父亲)的half 的fail 链上找,找到第一个l ,满足s_{|s|-len_l-1}=c (当前插入的字符)且len_l+2 \leq \frac{len_p}{2} ,令half_p=l 的go_c 。
时间复杂度为
#include <iostream>
#include <cstdio>
using namespace std;
struct PAM{
int go[26], fail, len;
int half;
}pt[500010];
int lst, tot;
int len, ans;
char s[500010];
void build() { s[0] = -1; pt[++tot].len = -1; pt[0].fail = pt[1].fail = 1; }
void insert(int c, int n) {
int p = lst;
while (s[n] != s[n - pt[p].len - 1]) p = pt[p].fail;
if (!pt[p].go[c]) {
int v = ++tot, k = pt[p].fail, l = pt[p].half;
pt[v].len = pt[p].len + 2;
while (s[n] != s[n - pt[k].len - 1]) k = pt[k].fail;
pt[v].fail = pt[k].go[c];
if (pt[v].len > 2) {
while (s[n] != s[n - pt[l].len - 1] || pt[l].len + 2 > pt[v].len / 2) l = pt[l].fail;
pt[v].half = pt[l].go[c];
} else pt[v].half = pt[v].fail;
pt[p].go[c] = v;
lst = pt[p].go[c];
if (pt[lst].len % 4 == 0 && pt[pt[lst].ffail].len == pt[lst].len / 2) ans = max(ans, pt[lst].len);
}
lst = pt[p].go[c];
}
int main() {
scanf("%d%s", &len, s + 1);
build();
for (int i = 1; i <= len; i++) insert(s[i] - 'a', i);
printf("%d", ans);
return 0;
}
CF835D
给你一个串,让你求出
k 阶回文子串有多少个。k 从1 到n 。k 阶子串的定义是:子串本身是回文串,而且它的左半部分也是回文串。 首先明确: 1、如果一个字串是k 阶回文,那他一定还是k-1 阶回文。 2、如果一个串是k 阶回文,那么这个串需要满足: 1.它本是是回文的。 2.他的左半部分是k-1 回文的。
考虑求出所有回文串的最高阶,记为
#include <bits/stdc++.h>
using namespace std;
int go[5005][26], len[5005], fail[5005], dp[5005], cnt[5005], ans[5005], half[5005], lst, tot, Len;
char s[5005];
void build() { s[0] = -1; len[0] = 0; len[1] = -1; fail[0] = fail[1] = 1; tot=1; lst=0;}
void ins(int n, int c) {
int p = lst;
while (s[n - len[p] - 1] != s[n]) p = fail[p];
if (!go[p][c]) {
int v = ++tot, k = fail[p], o = half[p];
len[v] = len[p] + 2;
while (s[n - len[k] - 1] != s[n]) k = fail[k];
fail[v] = go[k][c];
go[p][c] = v;
if (len[v] <= 2) half[v] = fail[v];
else {
while (s[n - len[o] - 1] != s[n] || len[o] + 2 > len[v] / 2) o = fail[o];
half[v] = go[o][c];
}
if (len[half[v]] == len[v] / 2) dp[v] = dp[half[v]] + 1;
else dp[v] = 1;
} lst = go[p][c]; cnt[lst]++;
}
int main() { scanf("%s", s + 1); Len = strlen(s + 1); build();
for (int i = 1; i <= Len; i++) ins(i, s[i] - 'a');
for (int i = tot; i >= 2; i--) cnt[fail[i]] += cnt[i];
for (int i = 2; i <= tot; i++) ans[dp[i]] += cnt[i];
for (int i = Len - 1; i >= 1; i--) ans[i] += ans[i + 1];
for (int i = 1; i <= Len; i++) printf("%d ", ans[i]);
return 0;
}
[CERC2014]Virus synthesis
初始有一个空串,利用下面的操作构造给定串
S 。 1、串开头或末尾加一个字符 2、串开头或末尾加一个该串的逆串 求最小化操作数,|S| \leq 10^5 。
花费数量最少那肯定是
第二种操作后形成的字符串一定是回文串,考虑枚举
建出
考虑转移。
在由父节点更新子节点过程中,若直接前后增加一个字符,则花费为
所以有
我们还得考虑
显然这个值只有可能是不超过当前回文长度一半的最长回文后缀(即
总时间复杂度是
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
struct node{
int go[4], fail, len;
int half;
}pt[500010];
int lst, tot, t, len, ans, dp[500010];
char s[500010];
int f(char ch) {
if (ch == 'A') return 0;
if (ch == 'C') return 1;
if (ch == 'G') return 2;
if (ch == 'T') return 3;
return 19260817;
}
void build() {
for (int i = 0; i <= tot; i++) for (int j = 0; j < 4; j++) pt[i].go[j] = 0;
tot = lst = 0;
s[0] = -1;
pt[++tot].len = -1;
pt[0].fail = pt[1].fail = 1;
}
void insert(int c, int n) {
int p = lst;
while (s[n] != s[n - pt[p].len - 1]) p = pt[p].fail;
if (!pt[p].go[c]) {
int v = ++tot, k = pt[p].fail;
pt[v].len = pt[p].len + 2;
while (s[n] != s[n - pt[k].len - 1]) k = pt[k].fail;
pt[v].fail = pt[k].go[c];
if (pt[v].len > 2) {
int l = pt[p].half;
while (s[n] != s[n - pt[l].len - 1] || pt[l].len + 2 > pt[v].len / 2) l = pt[l].fail;
pt[v].half = pt[l].go[c];
} else pt[v].half = pt[v].fail;
pt[p].go[c] = v;
dp[v] = pt[v].len;
if (pt[v].len % 2 == 0) {
dp[v] = min(dp[v], dp[p] + 1);
dp[v] = min(dp[v], dp[pt[v].half] + 1 + pt[v].len / 2 - pt[pt[v].half].len);
ans = min(ans, len - pt[v].len + dp[v]);
}
}
lst = pt[p].go[c];
}
int main() {
scanf("%d", &t);
while (t--) {
scanf("%s", s + 1);
len = strlen(s + 1);
ans = len;
build();
dp[0] = 1;
for (int i = 1; i <= len; i++) {
insert(f(s[i]), i);
}
printf("%d\n", ans);
}
return 0;
}
#1113. 秩序魔咒 & #1105. 快乐的 JYY[JSOI 2013]
双串最长公共回文子串。
求两个字符串的相等回文子串对子数。
建出两棵 PAM 然后直接在上面同时 dfs 即可。
//秩序魔咒
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
struct node{
int go[26], len, fail;
}pt[2][500010];//0:A ; 1:B
int Len[2], tot[2], lst[2], ans_len, ans_num;//长度 个数
char s[2][500010];
void build(int id) {
s[id][0] = -1;
pt[id][++tot[id]].len = -1;
pt[id][0].fail = pt[id][1].fail = 1;
}
void insert(int id, int c, int n) {
int p = lst[id];
while (s[id][n - pt[id][p].len - 1] != s[id][n]) p = pt[id][p].fail;
if (!pt[id][p].go[c]) {
int v = ++tot[id], k = pt[id][p].fail;
pt[id][v].len = pt[id][p].len + 2;
while (s[id][n - pt[id][k].len - 1] != s[id][n]) k = pt[id][k].fail;
pt[id][v].fail = pt[id][k].go[c];
pt[id][p].go[c] = v;
}
lst[id] = pt[id][p].go[c];
}
void init(int id) { for (int i = 1; i <= Len[id]; i++) {insert(id, s[id][i] - 'a', i); }
void solve(int x, int y) {
//第一棵树中的编号,第二棵树中的编号
if (pt[0][x].len > ans_len) ans_len = pt[0][x].len, ans_num = 1;
else if (pt[0][x].len == ans_len) ans_num++;
for (int i = 0; i < 26; i++) if (pt[0][x].go[i] && pt[1][y].go[i]) solve(pt[0][x].go[i], pt[1][y].go[i]);
}
int main() {
scanf("%d%d%s%s", &Len[0], &Len[1], s[0] + 1, s[1] + 1);
build(0); build(1);
init(0); init(1);
solve(1, 1); solve(0, 0);
printf("%d %d\n", ans_lne, ans_num);
return 0;
}
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
struct node{
int go[26], len, fail;
ll cnt;
}pt[2][50010];//0:A ; 1:B
int Len[2], tot[2], lst[2];
char s[2][50010];
ll ans;
void build(int id) {
s[id][0] = -1;
pt[id][++tot[id]].len = -1;
pt[id][0].fail = pt[id][1].fail = 1;
}
void insert(int id, int c, int n) {
int p = lst[id];
while (s[id][n - pt[id][p].len - 1] != s[id][n]) p = pt[id][p].fail;
if (!pt[id][p].go[c]) {
int v = ++tot[id], k = pt[id][p].fail;
pt[id][v].len = pt[id][p].len + 2;
while (s[id][n - pt[id][k].len - 1] != s[id][n]) k = pt[id][k].fail;
pt[id][v].fail = pt[id][k].go[c];
pt[id][p].go[c] = v;
}
pt[id][pt[id][p].go[c]].cnt++;
lst[id] = pt[id][p].go[c];
}
void init(int id) {
for (int i = 1; i <= Len[id]; i++) insert(id, s[id][i] - 'A', i);
for (int i = tot[id]; i > 1; i--) pt[id][pt[id][i].fail].cnt += pt[id][i].cnt;
}
void solve(int x, int y) {
for (int i = 0; i < 26; i++) {
if (pt[0][x].go[i] && pt[1][y].go[i]) {
ans += pt[0][pt[0][x].go[i]].cnt * pt[1][pt[1][y].go[i]].cnt;
solve(pt[0][x].go[i], pt[1][y].go[i]);
}
}
}
int main() {
scanf("%s%s", s[0] + 1, s[1] + 1);
Len[0] = strlen(s[0] + 1);
Len[1] = strlen(s[1] + 1);
build(0); build(1);
init(0); init(1);
solve(1, 1); solve(0, 0);
printf("%lld\n", ans);
return 0;
}
#1114. 「2017 山东一轮集训 Day4」基因
区间本质不同回文串个数,强制在线,
n\le 10^5 。
- 算法1 大力分块
不想讲了,就是一暴力。
-
算法2 利用回文 border 的性质
-
算法3 套用 LCT???做到
O(n\log{n}) ???
[alpha dog]()
求
\sum_{1\le x\le y\le |s|}LCP(x,y) ,其中LCP(x,y) 是前缀的最长公共回文后缀,动态加入字符,查询,强制在线。操作数\le 10^5 。
题目中的LCP其实就是fail树上的LCA,那么就好做了,考虑加入字符时的贡献。从其每个祖先入手,再转换为边的贡献,变量只有子树大小,用LCT维护即可。
5 回文串border
我不会,快去问 xwp 或 mhy。
根据字符串border性质,border构成 log 段等差数列。应用在回文串上即为回文后缀。
考虑求出每段等差数列的公差和首项
int ins(int c, int n) {
int p = lst;
while (s[n - len[p] - 1] != s[n]) p = fail[p];
if (!son[p][c]) {
int v = ++tot, k = fail[p];
len[v] = len[p] + 2;
while (s[n - len[k] - 1] != s[n]) k = fail[k];
fail[v] = son[k][c];
d[v] = len[v] - len[fail[v]];
top[v] = d[v] == d[fail[v]] ? top[fail[v]] : fail[v];
son[p][c] = v;
}
return lst = son[p][c];
}
CF932G Palindrome Partition
code
https://codeforces.com/problemset/problem/906/E
3300
给定字符串
s,t ,可以翻转t 的若干不相交的区间,使得t=s ,求最少翻转几个区间?|s|,|t|\le 5\times 10^5
翻转区间相等看起来想一个回文串,我们重新构造,把
具体考虑 dp,设
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e6 + 5;
int mark[maxn], pre[maxn], g[maxn], f[maxn], top[maxn], d[maxn], len[maxn], fail[maxn], go[maxn][26], tot, lst;
char t[maxn], tt[maxn], s[maxn];
void build() { s[0] = -1; len[tot = 1] = -1; len[0] = 0; fail[1] = fail[0] = 1; }
int ins(int n, int c) {
int p = lst;
while (s[n - len[p] - 1] != s[n]) p = fail[p];
if (!go[p][c]) {
int v = ++tot, k = fail[p];
len[v] = len[p] + 2;
while (s[n - len[k] - 1] != s[n]) k = fail[k];
fail[v] = go[k][c];
d[v] = len[v] - len[fail[v]];
top[v] = d[v] == d[fail[v]] ? top[fail[v]] : fail[v];
go[p][c] = v;
}
return lst = go[p][c];
}
int main() {
memset(f, 0x3f, sizeof(f));
f[0] = 0;
scanf("%s%s", t + 1, tt + 1);
int Len = strlen(t + 1);
for (int i = 1; i <= Len; i++) s[2 * i - 1] = t[i], s[2 * i] = tt[i];
Len <<= 1; build();
for (int i = 1; i <= Len; i++) {
ins(i, s[i] - 'a');
for (int j = lst; j > 1; j = top[j]) {
g[j] = i - len[top[j]] - d[j];
if (d[j] == d[fail[j]]) {
if (f[g[j]] > f[g[fail[j]]]) {
g[j] = g[fail[j]];
}
}
if (i % 2 == 0) {
if (f[g[j]] + 1 < f[i]) {
f[i] = f[g[j]] + 1;
pre[i] = g[j];
mark[i] = 1;
}
}
}
if (i % 2 == 0) {
if (s[i] == s[i - 1] && f[i - 2] < f[i]) {
f[i] = f[i - 2];
pre[i] = i - 2;
mark[i] = 0;
}
}
}
if (f[Len] >= 0x3f3f3f3f) {
puts("-1");
return 0;
}
printf("%d\n", f[Len]);
for (int i = Len; i; i = pre[i]) {
if (mark[i])
printf("%d %d\n", pre[i] / 2 + 1, i / 2);
}
return 0;
}
做题的收获?知道了翻转相等和回文串的转化关系;明白了为什么这么转移