《算法竞赛进阶指南》读书笔记汇总
这里面是我在阅读《算法竞赛进阶指南》这本书时的一些思考,有兴趣可以瞧瞧!
如若发现什么问题,可以通过评论或者私信作者提出。希望各位大佬不吝赐教!
字符串哈希
这里主要介绍字符串哈希,它算是一个暴力算法,当我们在处理字符串类型的题目时,如果没有思路,不妨试试哈希。字符串哈希在
O
(
n
)
O(n)
O(n)处理出字符串所有前缀的哈希值之后,可以
O
(
1
)
O(1)
O(1)查询任意子串的哈希值。
一般,我们的哈希函数设计为:取一个固定的
P
P
P值,把字符串看成是
P
P
P进制数,并分配一个大于0的数值,代表每种字符,然后取一固定值
M
M
M,求出该
P
P
P进制数对
M
M
M的余数,即对
M
M
M取模,作为该字符串的哈希值。
一般而言,我们取
P
P
P值为131或13331,此时产生冲突的概率几乎为0;我们取
M
M
M为
2
64
2^{64}
264。为了代码写起来方便,我们定义哈希值类型为
u
n
s
i
g
n
e
d
unsigned
unsigned
l
o
n
g
long
long
l
o
n
g
long
long。
下面我们看看例题与习题来体会一下字符串哈希的简单应用吧。
【例题】兔子与兔子(AcWing138)
题目链接
思路: 题目要快速查询某两个子串是否相等,那我们不难想到用字符串哈希。预处理出所有前缀的哈希值之后,可以实现
O
(
1
)
O(1)
O(1)查询任意子串的哈希值。具体实现比较简单,见代码。
AC代码:
#include<bits/stdc++.h>
#define N 1000005
#define ull unsigned long long
using namespace std;
const ull P = 13131;
int n;
char s[N];
ull h[N];
ull qpow(ull a,int b){
ull res = 1;
while(b){
if(b & 1) res *= a;
a *= a;
b >>= 1;
}
return res;
}
void solve(){
scanf("%s",s + 1);
n = strlen(s + 1);
for(int i = 1;i <= n;i ++){
h[i] = h[i - 1] * P + s[i] - 'a';
}
int q;scanf("%d",&q);
while(q --){
int l1,r1,l2,r2;
scanf("%d%d%d%d",&l1,&r1,&l2,&r2);
if(h[r1] - h[l1 - 1] * qpow(P,r1 - l1 + 1) == h[r2] - h[l2 - 1] * qpow(P,r2 - l2 + 1))
puts("Yes");
else
puts("No");
}
}
int main(){
solve();
return 0;
}
【例题】回文子串的最大长度
题目链接
思路: 首先我们需要统一奇数与偶数的回文子串的处理方式,常见的方法是在每两个字符中间添加一个没有出现过的特殊字符,把原串长度变为原来的两倍。
然后预处理出新串及其反串的所有前缀的哈希值。
最后我们枚举每一个位置作为回文中心,二分最大长度。对于每一个长度
m
i
d
mid
mid的判定,我们判断反串中对应子串的哈希值与新串中对应子串的哈希值是否相同即可。对应的子串位置可以自己画图看看,很容易找到滴。
AC代码:
#include<bits/stdc++.h>
#define N 2000005
#define ull unsigned long long
using namespace std;
int n;
char s[N],sr[N];
ull h[N],hr[N];
ull p[N];
int kase;
ull get(ull h[], int l, int r){
return h[r] - h[l - 1] * p[r - l + 1];
}
void solve(){
n = strlen(s + 1);
for(int i = 2 * n;i > 0;i -= 2){
s[i] = s[i / 2];
s[i - 1] = 'z' + 1;
}
n *= 2;
for(int i = 1, j = n;i <= n;i ++, j --) sr[j] = s[i];
p[0] = 1;
for(int i = 1;i <= n;i ++)
{
h[i] = h[i - 1] * 13131 + s[i] - 'a';
hr[i] = hr[i - 1] * 13131 + sr[i] - 'a';
p[i] = p[i - 1] * 13131;
}
int ans = 0;
for(int i = 1;i <= n;i ++){
int l = 0,r = min(i - 1,n - i);
while(l < r){
int mid = l + r + 1 >> 1;
if(get(h, i - mid, i - 1) == get(hr, n - (i + mid) + 1, n - (i + 1) + 1)) l = mid;
else r = mid - 1;
}
if(s[i - l] == 'z' + 1) ans = max(ans, l);
else ans = max(ans,l + 1);
}
printf("Case %d: %d\n", ++kase, ans);
}
int main(){
while(scanf("%s",s + 1) && !(s[1] == 'E' && s[2] == 'N' && s[3] == 'D'))
solve();
return 0;
}
【例题】后缀数组(AcWing140)
题目链接
思路:后缀数组的哈希做法时间复杂度比基数排序做法多了一个
l
o
g
log
log,也是比较经典的,可以了解一下。我们用最朴素的想法,对所有后缀进行排序,用自己定义的比较函数去快排所有后缀。由于我们可以
O
(
1
)
O(1)
O(1)查询任意子串的哈希值,那么我们可以二分两个不同后缀最长公共前缀的长度,那么这个长度的下一个位置就是这两个不同后缀第一个不相同字符位置,我们只需比较这两个字符的大小即可比较两个不同后缀的大小,复杂度为
O
(
l
o
g
n
)
O(logn)
O(logn)。对于
h
e
i
g
h
t
height
height数组的求解也是二分,一模一样就不再赘述啦。
AC代码:
#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ULL;
const int N = 300005;
const int base = 131;
int n;
char s[N];
ULL h[N],p[N];
int sa[N];
ULL get(int l,int r){
return h[r] - h[l - 1] * p[r - l + 1];
}
bool cmp(int a,int b){
int l = 0,r = min(n - a + 1,n - b + 1);
while(l < r){
int mid = l + r + 1 >> 1;
if(get(a,a + mid - 1) == get(b,b + mid - 1)) l = mid;
else r = mid - 1;
}
return s[a + l] < s[b + l];
}
void solve(){
scanf("%s",s + 1);
n = strlen(s + 1);
p[0] = 1;
for(int i = 1;i <= n; i ++){
p[i] = p[i - 1] * base;
h[i] = h[i - 1] * base + s[i] - 'a';
sa[i] = i;
}
sort(sa + 1, sa + 1 + n, cmp);
for(int i = 1; i <= n;i ++){
if(i != 1) printf(" ");
printf("%d",sa[i] - 1);
}
puts("");
printf("0");
for(int i = 2;i <= n;i ++){
int l = 0,r = min(n - sa[i] + 1,n - sa[i - 1] + 1);
while(l < r){
int mid = l + r + 1 >> 1;
if(get(sa[i],sa[i] + mid - 1) == get(sa[i - 1],sa[i - 1] + mid - 1)) l = mid;
else r = mid - 1;
}
printf(" %d",l);
}
}
int main(){
solve();
return 0;
}