学字符串(2) -- 哈希,字典序最小旋转
字符串字典序最小旋转
Booth,一种双指针做法
非常巧妙的方法
建议取局部证明理解,直接看整体会有点难理解
string s,t;
cin>>t;
int len=t.length();
s=t+t;
int i=0,j=1,k=0;
while(i<len && j<len && k<len){
if(s[i+k]==s[j+k]){
k++;
}else if(s[i+k]>s[j+k]){
i+=k+1;
if(i<=j)i=j+1;
k=0;
}else{
j+=k+1;
if(j<=i)j=i+1;
k=0;
}
}
int mov=min(i,j);
第一个判断内很好理解
第二个,如果s[i+k]>s[j+k],要执行这些操作,为什么?
首先s[i...i+k-1] = s[j...j+k-1]这是已知的,然后假如此时s[i+k]>s[j+k],就说明从i到i+k这个范围内,以这些点开头的旋转,都不可能比从j到j+k这个范围内开头的某个旋转优,所以直接跳过!
然后为什么还得保持在j的后面?因为是双指针,j前面的每个开头,不是被i覆盖,就是被j覆盖,也可能是被两者之一跳过了,按照比较规则来说,不可能更好!所以这样就行
k=0,没什么好说的,肯定要清零从头比较的
对于第三个,ij反过来也是一样,对称性
对于i到len或者j到len直接跳出来,首先跳的肯定是之前被淘汰的指针,更好的指针一定在前面,字符串最大下标是len-1,所以说明被淘汰的已经没地方淘汰了,答案取前面的(较小的)优的指针就行
那么为什么k到len也要跳出来?因为这说明字符串是有周期的!而且根据规则,我们一定能跳到周期字典序最小的位置,此时其实ij都行,取min也没影响
二分+哈希
没写这个写法,描述一下
就是常见的“扫一遍取min”的写法。对每个开头,和目前的min指针,二分求LCP,然后看下一个不同的点哪个大,来更新min,一样就不管,很好理解
哈希相关的技术
随机数
用这个模板就行
mt19937 rng(chrono::steady_clock::now().time_since_epoch().count() ^ (uintptr_t)new char); // 无符号32位整数
mt19937_64 rng(chrono::steady_clock::now().time_since_epoch().count() ^ (uintptr_t)new char); // 无符号64位整数
要取随机模数,就用如下的板子
bool isp(int x){ //暴力素性测试
for(int i=2;i*i<=x;i++){
if(x%i==0)return 0;
}
return 1;
}
void preprocess() {
while(mods.size()<3){ //3个随机质模数,足够面对很多场合
int p=uniform_int_distribution<int>(1e9,2e9)(rng);
if(isp(p)){
mods.push_back(p);
}
}
}
base也可以随机,这样就有个很好的字符串哈希板子
bool isp(int x) {
for (int i = 2; i * i <= x; i++) {
if (x % i == 0)
return 0;
}
return 1;
}
struct StrHash {
inline static int M1, M2, B1, B2;
inline static vector<int> P1, P2;
vector<int> h1, h2;
static void init() {
if (!P1.empty()) return;
mt19937 rng(chrono::steady_clock::now().time_since_epoch().count() ^ (uintptr_t)new char);
while(1){
M1=uniform_int_distribution<int>(1e9,2e9)(rng);
if(isp(M1))break;
}
while(1){
M2=uniform_int_distribution<int>(1e9,2e9)(rng);
if(M1!=M2 && isp(M2))break;
}
B1 = uniform_int_distribution<int>(200, M1 - 1)(rng);
B2 = uniform_int_distribution<int>(200, M2 - 1)(rng);
P1.push_back(1);
P2.push_back(1);
}
StrHash(const vector<int>& s) {
init();
int n = s.size();
h1.resize(n + 1);
h2.resize(n + 1);
while (P1.size() <= n) {
P1.push_back(1LL * P1.back() * B1 % M1);
P2.push_back(1LL * P2.back() * B2 % M2);
}
for (int i = 0; i < n; i++) {
h1[i + 1] = (1LL * h1[i] * B1 + s[i]) % M1;
h2[i + 1] = (1LL * h2[i] * B2 + s[i]) % M2;
}
}
pair<int, int> get(int l, int r) {
int res1 = (h1[r + 1] - 1LL * h1[l] * P1[r - l + 1]) % M1;
if (res1 < 0) res1 += M1;
int res2 = (h2[r + 1] - 1LL * h2[l] * P2[r - l + 1]) % M2;
if (res2 < 0) res2 += M2;
return {res1, res2};
}
};
unsigned long long merge_pr(const pair<int,int> &pr) { //用来合并两个int,方便放进一个u64数组排序查大小
return ((unsigned long long)pr.first << 32) | pr.second;
}
比较板的
CF271D - Good Substrings
SPOJ - Ada and Spring Cleaning
CF727E - Games on a CD
二分 + 哈希
Longest Common Substring
题意:求s和t的最长子串,注意不是LCS
解法:二分子串长度,然后对于这个长度,把s和t的所有这个长度的子串构造两个集合,看集合有没有交集
具体来说,可以一边放到一个set里,另外一边用.count来找;也可以一边全放进vector里面,排序,然后另外一边二分来找。推荐后者,常数更小,因为我写前者TLE了
据说高手用SA或SAM,不会
乘法 + 随机质模数
abc339_f - Product Equality
题意:给你一堆很大(<1e1000)的数,问有多少对满足 x * y == z
解法:随机模数存数字即可,用3个模,冲突概率非常低。乘积也这样用三个数表示,然后很容易统计了
有个这个函数分享一下:
equal_range(tmp.begin(),tmp.end(),val);
返回pair<int,int>,然后second-first就知道数量了
哈希 + 线段树
要是知道子段长度,字符串哈希就是结合的,能用线段树搞
CF580E - Kefa and Watch
题意:给你字符串和两种操作,修改是把一个区间全改成同一个字符,询问是问区间是否有长度为k的周期
解法:首先,哈希在判断周期循环节上有优势:只要看hash[l,r-k]==hash[l+k,r]就行!而且还能看任意子串,KMP只适合字符串整体,不太灵活
ACL风格线段树,像这样写:
struct S{
m1 h1;
m2 h2;
int len=1;
};
S op(const S&a,const S&b){
if(a.len==0)return b;
if(b.len==0)return a;
return {
a.h1*(P1[b.len]-P1[b.len-1])+b.h1,
a.h2*(P2[b.len]-P2[b.len-1])+b.h2,
a.len+b.len
};
}
S e(){
return {
0,0,0
};
}
struct F{
int val=-1;
};
S mapping(const F&f,const S&s){
if(f.val==-1 || s.len==0){
return s;
}
return {
m1(f.val) * P1[s.len - 1],
m2(f.val) * P2[s.len - 1],
s.len,
};
}
F comp(const F&f1,const F&f2){
if(f1.val==-1)return f2;
return f1;
}
F id(){
return {-1};
}
细节还挺多的,很多判断不能少
CSES - Palindrome Queries
题意:给字符串,两种操作,单点修改和问是否回文
解法:字符串正反分别构造两个线段树即可,单点修改,定义甚至更简单
哈希 + 回文
CF835D - Palindromic characteristics
题意:如果一个回文串是k阶回文串,那么它的左半边必须是k-1阶回文串。求所有阶的回文串的数量
解法:有个简单的区间DP,判断回文来说就是正反构造哈希,很容易理解。马拉车效果也是一样的,常数更小
CF7D - Palindrome Degree
题意:回文阶数定义同前,求所有前缀的回文阶数,数据5e6
解法:很大的数啊,解法类似前一题,DP+哈希回文判断,我用的是马拉车就是了
哈希 + 循环
CF19C - Deletion of Repeats
题意:数组形式的字符串,每次找出先是最短,然后最左出现的s+s形式的子串,删除左半边以及前面的所有字符,直到删不了,注意相同字符不超过10个
解法:我是维护一个“被删到的点”,起始点在这之前的就不能操作。首先,找出所有的相同字符对,一对表示一串可能的双循环子串的两个结尾点,然后从短到长,从左到右排序,从前到后删除。注意每次要算出起始点,不能在被删掉的范围之内。哈希的部分就很容易了
因为10个的限制,总共对数不会很多的!
据说高手用SA或SAM解决没有字符个数限制的版本,不会
CF1056E - Check Transcription
题意:给定一个01序列s和一个目标字符串t,你要想办法把t映射为s,就是说0代表特定的字符串,1代表另外一个,问方法数
解法:先数0和1数量,然后就要解个线性方程,暴力枚举即可,然后就是哈希判断所有的点即可。当时我想的是调和级数证明复杂度,但是AI说可以证明是O(|T|)的,如下:
Ax + By = C,正整数解个数最多 C/max(A,B) 个,所有解最多|T|/|S|个,验证复杂度|S|,相乘就是O(T)级别了!
哈希 + 集合
我们要用数字尽可能不冲突地表示集合,用于统计或比较
CF154C - Double Profiles
这题踩了很多坑!
题意:给出所有的“认识”关系。两个人“相同”,等价于他们认识的(除了对方外)和不认识的人相同,求这样的“相同”对数。人数和“认识”数都是1e6级别
解法:做这道题,肯定会先想到:想办法表示每个人认识的所有人,然后对于“不认识”且“相同”的人,就很好统计了,用vector+sort+equal_range或者map什么的,但是对于“认识”且“相同”的怎么办?
然后想到,对每条边,想办法撤销两端的人在对方的“认识集合”的表示,不就行了?
那么就得有个可以撤销的哈希表示。我先想到的是,给1e6每个数都赋个质数,然后搞三模乘法哈希。结果写起来贼麻烦,空间还炸了,时间上还得求好多逆元,真不太行
然后就是AI给我讲了很好用的异或哈希!先给每个人随机赋值,然后异或表示集合,撤销就再搞一次即可。不过由于估算冲突概率的生日原理:
一个通俗的记法:当你的数据量 N 达到模数 M 的开方 sqrt(M) 量级时,哈希碰撞就会频繁发生。
如果是u32随机数,肯定不行;如果是u64随机,sqrt(1e18) == 1e9,感觉距离1e6还是过于接近了;两个u64拼在一起,似乎就很好了!然后就能写了
然后看了题解,教了一个很好的思想:加自环。什么意思呢?State[i]表示i的不包含自己的集合状态表示(就是我们刚才求的),State[i] == State[j]我们已经求了一次。那么对于“认识”且“相同”的,就能这么求:
每个点把自己加入State[i],也就是说State[i] ^= val[i],然后重新跑一遍之前的求“不认识”且“相同”的求法,和之前的答案累加,直接求出来了!
也就是说,如果去掉边后状态相同,那么同时加上自己,状态也是相同的,这样就能复用前面的计算逻辑了!(虽然对每个边去掉对方也不算难写)

浙公网安备 33010602011771号