目录
引入:[BalticOI 2014 Day1] Three Friends - 洛谷
入门:活动 - AcWing
题意:给定一个长度为n的字符串,再给定m个询问,每个询问包含四个整数l1,r1,l2,r2l1,r1,l2,r2,请你判断[l1,r1l1,r1]和[l2,r2l2,r2]这两个区间所包含的字符串子串是否完全相同。
字符串中只包含大小写英文字母和数字。
算法:
(字符串哈希) O(n)+O(m)
全称字符串前缀哈希法,把字符串变成一个p进制数字(哈希值),实现不同的字符串映射到不同的数字。
对形如 X1X2X3⋯Xn−1Xn的字符串,采用字符的ascii 码乘上 P 的次方来计算哈希值。映射公式 (X1×Pn−1+X2×Pn−2+⋯+Xn−1×P1+Xn×P0)modQ
注意点:
1. 任意字符不可以映射成0,否则会出现不同的字符串都映射成0的情况,比如A,AA,AAA皆为0
2. 冲突问题:通过巧妙设置P (131 或 13331) , Q (264)(264)的值,一般可以理解为不产生冲突。问题是比较不同区间的子串是否相同,就转化为对应的哈希值是否相同。
求一个字符串的哈希值就相当于求前缀和,求一个字符串的子串哈希值就相当于求部分和。
前缀和公式 h[ i + 1]=h[ i ]× P +s[ i ] (i∈[ 0, n − 1] ) h为前缀和数组,s为字符串数组
区间和公式 h[ l , r ]=h[ r ]−h[ l − 1 ] × P[ r − l + 1 ]
区间和公式的理解: ABCDE 与 ABC 的前三个字符值是一样,只差两位,
乘上 P² 把 ABC 变为 ABC00,再用 ABCDE - ABC00 得到 DE 的哈希值。
AcWing 841. 字符串哈希 【公式助理解】 - AcWing
//字符串哈希
typedef unsigned long long ULL;//ULL数组用于存储时,为的就是溢出时有取模效果
const int N = 100010, P = 131; //131 or 13331
int n, m;
char str[N];
ULL h[N], p[N];
// h[i]前i个字符的hash值
//p[i]表示P的i次方
// 字符串变成一个p进制数字,
//体现了字符+顺序,需要确保不同的字符串对应不同的数字
ULL get(int l, int r)
{
return h[r] - h[l - 1] * p[r - l + 1];
}
int main()
{
scanf("%d%d%s", &n, &m, str + 1);
//字符串从1开始编号,h[1]为前一个字符的哈希值
p[0] = 1;
h[0] = 0;
for (int i = 1; i <= n; i++)
{
p[i] = p[i - 1] * P;//p进制 把p想象成10就好理解了
h[i] = h[i - 1] * P + str[i];前缀和求整个字符串的哈希值
}
while (m--)
{
int l1, r1, l2, r2;
scanf("%d%d%d%d", &l1, &r1, &l2, &r2);
if (get(l1, r1) == get(l2, r2))
puts("Yes");
else
puts("No");
}
return 0;
}
引入:[BalticOI 2014 Day1] Three Friends - 洛谷
题目描述
给定一个字符串S,先将字符串S复制一次(变成双倍快乐 ),得到字符串T,然后在T中插入一个字符,得到字符串U。
给出字符串U,重新构造出字符串S。
所有字符串只包含大写英文字母。
输入格式
第一行一个整数N,表示字符串U的长度。
第二行一个长度为N的字符串,表示字符串U。
输出格式
一行一个字符串,表示字符串S。
特别地:
如果字符串无法按照上述方法构造出来,输出NOT POSSIBLE;
如果字符串S不唯一,输出 NOT UNIQUE。
题解:
为了减少hash冲突而*31。
因为S翻倍后要插入一个字符,所以始终是2n+1,也就是奇数,所以如果是偶数就输出NOT POSSIBLE,并结束。
插入的字符有2种情况:
1.插入的字符在前面一半之间,则S(也就是答案)就在后面一半,我们就在前面枚举字符的位置,然后根据 hash值来判断是否相等;
2.插入的字符在后面一半之间,则S(也就是答案)就在前面一半,我们就在后面枚举字符的位置,然后根据 hash值来判断是否相等;
“三个朋友”「BalticOI 2014 Day1 T2 Three Friends」【题解】_cqbz_JiangJinBei的博客-CSDN博客
进制hash板子:
//[l,r]的hash值
ll get(int l, int r)
{
return (h[r] - h[l - 1] * p[r - l + 1] % mod + mod) % mod;
}
//[l,r]删去x的hash值
ll query(int l, int r, int x)
{
return (get(l, x - 1) * p[r - x] % mod + get(x + 1, r)) % mod;
}
想解释一下query函数:
注意[l,r]删去x, 前面的get*p[r-x],注意这个r-x是关键,
相当于12345 ,删去3的话,应该是12*100+45。
只有两个0,没有x这一位了噢。
「BalticOI 2014 Day 1」三个朋友(进制hash)_从冬的博客-CSDN博客
再来说说我看完别人代码之后自己敲这个题时的想法(应该更容易理解一点)
首先你要理解上面的get函数和query,上面已经写过了。
然后再来看这道题:
其实代码里是三种情况,第三种就是这个插入的字符在正中间(mid + 1)。
前两种体现在代码里就是:
因为n是奇数,mid就会是,比如9/2 = 4,那么假设插入字符是第一种情况,就会是前五个(包含插入字符)减去插入字符的hash值和最后四个的hash值比较,所以我们枚举插入字符为1-mid,(不是mid+1,是因为第三种情况考虑的就是这种情况)。然后前五个的hash值用query函数,后四个用get函数,这样看代码应该就很好理解了叭。
const int N = 2e6 + 10, P = 31;
ll h[N], p[N];
char s[N];
//[l,r]的hash值
ll get(int l, int r)
{
return (h[r] - h[l - 1] * p[r - l + 1]);
}
//[l,r]删去x的hash值
ll query(int l, int r, int x)
{
return (get(l, x - 1) * p[r - x] + get(x + 1, r));
}
ll sum = 0;
map<ll, bool> flag;
int main()
{
int n;
cin >> n;
ll ans = 0;
cin >> s + 1;
if (n % 2 == 0)
{
cout << "NOT POSSIBLE\n";
return 0;
}
p[0] = 1;
h[0] = 0;
for (int i = 1; i <= n; i++)
{
p[i] = p[i - 1] * P;
h[i] = h[i - 1] * P + (ll)(s[i] - 'A' + 1);
}
ll mid = n / 2;
for (ll i = 1; i <= mid; i++)
{
if (query(1, mid + 1, i) == get(mid + 2, n) && !flag[get(mid + 2, n)])
{
sum++;
ans = i;
flag[get(mid + 2, n)] = 1;
}
}
if (get(1, mid) == get(mid + 2, n) && !flag[h[mid]])
{
sum++;
ans = mid + 1;
flag[h[mid]] = 1;
}
for (ll i = mid + 2; i <= n; i++)
{
if (!flag[h[mid]] && get(1, mid) == query(mid + 1, n, i))
{
sum++;
ans = i;
flag[h[mid]] = 1;
}
}
if (!sum)
{
cout << "NOT POSSIBLE\n";
}
else
{
if (sum > 1)
{
cout << "NOT UNIQUE\n";
}
else
{
if (ans <= mid + 1)
{
for (ll i = mid + 2; i <= n; i++)
{
cout << s[i];
}
}
else
{
for (ll i = 1; i <= mid; i++)
{
cout << s[i];
}
}
cout << endl;
}
}
return 0;
}
做完上面的题 有了一定的理解,现在来看进阶(更专业的理解哈)
进阶:hash进阶:使用字符串hash乱搞的姿势_牛客博客
字符串-hash - 随笔分类 - henry_y - 博客园
题目:
NUMOFPAL - Number of Palindromes - 洛谷
LOJ#2452. 「POI2010」反对称 Antisymmetry - henry_y - 博客园
马拉车算法
最长回文子串——马拉车算法详解_HappyRocking的博客-CSDN博客
马拉车算法(不懂问我)_algsup的博客-CSDN博客_马拉车算法
string Mannacher(string s)
{
//插入"#"
string t = "$#";
for (int i = 0; i < s.size(); ++i)
{
t += s[i];
t += "#";
}
vector<int> p(t.size(), 0);
// mx表示某个回文串延伸在最右端半径的下标,id表示这个回文子串最中间位置下标
// resLen表示对应在s中的最大子回文串的半径,resCenter表示最大子回文串的中间位置
int mx = 0, id = 0, resLen = 0, resCenter = 0;
//建立p数组
for (int i = 1; i < t.size(); ++i)
{
p[i] = mx > i ? min(p[2 * id - i], mx - i) : 1;
//遇到三种特殊的情况,需要利用中心扩展法
while (t[i + p[i]] == t[i - p[i]])
++p[i];
//半径下标i+p[i]超过边界mx,需要更新
if (mx < i + p[i])
{
mx = i + p[i];
id = i;
}
//更新最大回文子串的信息,半径及中间位置
if (resLen < p[i])
{
resLen = p[i];
resCenter = i;
}
}
//最长回文子串长度为半径-1,起始位置为中间位置减去半径再除以2
return s.substr((resCenter - resLen) / 2, resLen - 1);
}
分析:简单分析后发现n必须是偶数,反对称字串必须是,对称位置不能相同。
解法一: 马拉车算法 模板改一下判断条件即可。代码如下
ll Mannacher(string s)
{
//插入"#"
ll ans = 0;
string t = "$#";
for (int i = 0; i < s.sz; ++i)
{
t += s[i];
t += "#";
}
vector<int> p(t.size(), 0);
// mx表示某个回文串延伸在最右端半径的下标,id表示这个回文子串最中间位置下标
// resLen表示对应在s中的最大子回文串的半径,resCenter表示最大子回文串的中间位置
int mx = 0, id = 0;
//建立p数组
for (int i = 1; i < t.size(); i += 2)
{
p[i] = mx > i ? min(p[2 * id - i], mx - i) : 1;
//遇到三种特殊的情况,需要利用中心扩展法
while (t[i + p[i]] == t[i - p[i]] && t[i-p[i]] == '#' || t[i + p[i]]-'0' + t[i-p[i]] -'0'== 1)
++p[i];
//半径下标i+p[i]超过边界mx,需要更新
if (mx < i + p[i])
{
mx = i + p[i];
id = i;
}
ans += p[i] / 2 ;
}
//最长回文子串长度为半径-1,起始位置为中间位置减去半径再除以2
return ans ;
}
int main()
{
int n ;
cin>>n;
string s;
cin>>s;
cout<<Mannacher(s)<<endl;
return 0;
}
解法二:字符串哈希