字符串乱记
就是一些关于字符串的笔记,没什么别的。
毫无顺序的目录
- 最小表示法
- Manacher
- trie
最小表示法
定义:
首先要了解一个东西叫循环同构。
形式化定义为:
当字符串 \(S\) 中可以选定一个位置 \(i\) 满足
则称 \(S\) 与 \(T\) 循环同构
举一个通俗易懂的例子:
例如有一个字符串:\(absdc\)
它的循环同构有 \(bsdca\) , \(sdcab\) , \(dcabs\) , \(cabsd\)。
至于最小表示就是它与它所有循环同构中字典序最小的字符串。
做法
考虑 \(O(n)\) 时间复杂度的做法。
首先为了防止越界的分类讨论,可以将字符串进行倍长。
然后设置两个指针,表示最小循环的起始位置,暴力求出最长公共前缀,即 \(\text{lcp(i,j)}\) 设其为 \(k\)。
接着比较 \(s_{i+k}\) 与 \(s_{j+k}\)。
假设 \(s_{i+k}<s_{j+k}\) ,则显然 \(j-j+k\) 均不能成为起始位置,直接令 \(j\gets j+k+1\) 后继续比较即可。
点击查看代码
int l = 1 , r = 2;
while(l <= n && r <= n)
{
int k = 0;
while(k <= n && s[l + k] == s[r + k])
++k;
if(s[l + k] < s[r + k])
r += k + 1;
else l += k + 1;
r += (r == l);
}
一道例题
\(\text{P5211 [ZJOI2017]字符串}\)
这确实是一道很好的例题,可惜太弱了,不会做。
以后做了,再补吧。
Manacher
算法步骤
一个短小,精悍还贼快的一个算法。
重点在于理解其实也不难理解
首先考虑对于原字符串进行一些改变。
我们发现,对于一个回文字符串而言,它有两种形式:
\(abba\) 与 \(aba\)。
即中心轴是一个点或者一个空位,我们考虑在每一个字符中间加一个板子(就是随意一个原字符串不会出现的字符),就可以避免分类讨论了。
下文中的操作,都是在新字符串上进行操作的。
记 \(len_i\) 表示以位置 \(i\) 为中心的最长奇回文串半径,考虑从左往右依次求 \(len_i\)。
记录前缀中回文串右端点的最大值 \(mx\) 及其对应的回文中心 \(pos\)。
由于回文的对称性我们发现,如果当前扫到的节点在此时记录的最大的回文串内。
那么在回文串的另一边的回文半径就与此时节点一样的。
但是,回文串的对称性同样只在回文串的范围内,因此还要特判一下边界。
至于如何求对称点,小学奥数的中点公式: \(l+r=mid*2 \gets l=mid*2-r\)
即:
继承完了之前的直接暴力继续枚举更新即可。
显然 \(mx\) 是单调递增的,故时间复杂度 \(O(n)\)
代码很好写
for(int i = 1 , mid = 0 , r = 0;i <= n;i++)
{
l[i] = (i < r ? min(l[mid * 2 - i] , r - i) : 1);
while(s[i + l[i]] == s[i - l[i]]) l[i]++;
if(i + l[i] > r) r = i + l[i] - 1 , mid = i;
ans = max(ans , l[i]);
}
两道例题
\(\text{P5446 [THUPC2018]绿绿和串串}\)
solution
合法的串 \(S\) 一定是 \(T\) 的前缀。先用 \(\text{Manacher}\) 求出每个位置的回文半径,从后往前处理,一个位置合法当且仅当其回文半径右侧达到 \(|T|\),或者左侧达到 \(1\) 且右侧位置合法。
代码也很好写
#include <bits/stdc++.h>
using namespace std;
const int N = 2000010;
int t , n , l[N] , vis[N];
char s[N] , s1[N];
inline int read()
{
int asd = 0 , qwe = 1; char zxc;
while(!isdigit(zxc = getchar())) if(zxc == '-') qwe = -1;
while(isdigit(zxc)) asd = asd * 10 + zxc - '0' , zxc = getchar();
return asd * qwe;
}
int main()
{
t = read();
while(t--)
{
scanf("%s" , s1 + 1) , n = strlen(s1 + 1);
s[0] = '~' , s[1] = '*';
for(int i = 1;i <= n;i++) s[i * 2] = s1[i] , s[i * 2 + 1] = '*';
n = n * 2 + 1 , s[n + 1] = '@';
for(int i = 1 , mid = 0 , r = 0;i <= n;i++)
{
l[i] = (i <= r ? min(l[mid * 2 - i] , r - i + 1) : 1);
while(s[i + l[i]] == s[i - l[i]]) l[i]++;
if(i + l[i] > r) r = i + l[i] - 1 , mid = i;
}
for(int i = n;i >= 1;i--)
{
if(i + l[i] - 1 == n) vis[i] = 1;
else if(i - l[i] + 1 == 1 && vis[i + l[i] - 2]) vis[i] = 1;
}
for(int i = 1;i <= n;i++)
{
if(vis[i] && s[i] >= 'a' && s[i] <= 'z') printf("%d " , i / 2);
vis[i] = 0 , s[i] = ' ';
}
puts("");
}
return 0;
}
\(\text{P4555 [国家集训队]最长双回文串}\)
solution
利用 \(\text{Manacher}\) 的结果,可以求出以每个位置为开头/结尾的最长回文串长度。
代码还是很好写
#include <bits/stdc++.h>
using namespace std;
const int N = 200010;
int n , ans , l[N] , f1[N] , f2[N];
char s[N] , s1[N];
inline int read()
{
int asd = 0 , qwe = 1; char zxc;
while(!isdigit(zxc = getchar())) if(zxc == '-') qwe = -1;
while(isdigit(zxc)) asd = asd * 10 + zxc - '0' , zxc = getchar();
return asd * qwe;
}
int main()
{
scanf("%s" , s1 + 1) , n = strlen(s1 + 1);
s[0] = '~' , s[1] = '*';
for(int i = 1;i <= n;i++) s[i * 2] = s1[i] , s[i * 2 + 1] = '*';
n = n * 2 + 1;
for(int i = 1 , mid = 0 , r = 0;i <= n;i++)
{
l[i] = i < r ? min(r - i + 1 , l[mid * 2 - i]) : 1;
while(s[i + l[i]] == s[i - l[i]]) l[i]++;
if(i + l[i] > r) r = i + l[i] - 1 , mid = i;
f1[i + l[i] - 1] = max(l[i] - 1 , f1[i + l[i] - 1]);
f2[i - l[i] + 1] = max(l[i] - 1 , f2[i - l[i] + 1]);
}
for(int i = 2;i <= n;i++) f1[i] = max(f1[i] , f1[i + 2] - 2);
for(int i = 2;i <= n;i++) f2[i] = max(f2[i] , f2[i - 2] - 2);
for(int i = 1;i <= n;i++) if(f1[i] && f2[i]) ans = max(ans , f1[i] + f2[i]);
cout << ans;
return 0;
}
trie
算法步骤
这个东西还好意思要算法步骤吗?
感觉 \(\text{xxs}\) 都随便写。
一道例题
solution
直接线段树套 \(01_trie\),思路没什么好说的,写对就可以。
代码不是很好写了,但也不难
#include <bits/stdc++.h>
using namespace std;
const int N = 40000010;
int n , m , cnt;
inline int read()
{
int asd = 0 , qwe = 1; char zxc;
while(!isdigit(zxc = getchar())) if(zxc == '-') qwe = -1;
while(isdigit(zxc)) asd = asd * 10 + zxc - '0' , zxc = getchar();
return asd * qwe;
}
struct Trie
{
int c[N][2] , sum[N];
inline void update(int k , int x , int tim)
{
for(int i = 20;i >= 0;i--)
{
int tmp = (x >> i) & 1;
// cout << " :: 1 " << k << " " << tmp << endl;
k = c[k][tmp] ? c[k][tmp] : (c[k][tmp] = ++cnt);
sum[k] = max(sum[k] , tim);
}
}
inline int ask(int k , int x , int tim)
{
int res = 0;
// cout << k << " " << x << " " << tim << endl;
for(int i = 20;i >= 0;i--)
{
int tmp = (x >> i) & 1;
// cout << " :: 2 " << k << " " << tmp << endl;
if(c[k][tmp ^ 1] && sum[c[k][tmp ^ 1]] >= tim) res += 1 << i , k = c[k][tmp ^ 1];
else if(sum[c[k][tmp]] >= tim) k = c[k][tmp];
else return res;
}
return res;
}
}t;
inline void change(int p , int ls , int rs , int num , int tim , int id)
{
t.update(p , num , tim);
if(ls == rs) return;
if((ls + rs) / 2 >= id) change(p * 2 , ls , (ls + rs) / 2 , num , tim , id);
else change(p * 2 + 1 , (ls + rs) / 2 + 1 , rs , num , tim , id);
}
inline int get(int p , int l , int r , int ls , int rs , int num , int tim)
{
if(l <= ls && rs <= r) return t.ask(p , num , tim);
int sum = 0;
if((ls + rs) / 2 >= l) sum = max(sum , get(p * 2 , l , r , ls , (ls + rs) / 2 , num , tim));
if((ls + rs) / 2 < r) sum = max(sum , get(p * 2 + 1 , l , r , (ls + rs) / 2 + 1 , rs , num , tim));
return sum;
}
int main()
{
n = read() , m = read() , cnt = n * 4;
for(int i = 1;i <= n;i++)
{
int x = read();
change(1 , 1 , n , x , 1e9 , i);
}
int tim = 0;
for(int i = 1;i <= m;i++)
{
int opt = read();
if(opt == 0)
{
++tim;
int s = read() , v = read();
change(1 , 1 , n , v , tim , s);
}
else
{
int l = read() , r = read() , x = read() , d = read();
cout << get(1 , l , r , 1 , n , x , max(tim - d + 1 , 0)) << endl;
}
}
return 0;
}