题目描述
这天天气不错,hzhwcmhf 神犇给 VFleaKing 出了一道题:
给你一个长度为 N 的字符串 S,求有多少个不同的长度为 L 的子串。
子串的定义是 S[l]、S[l + 1]、… S[r] 这样连续的一段。
两个字符串被认为是不同的当且仅当某个位置上的字符不同。
VFleaKing 一看觉得这不是 Hash 的裸题么!于是果断写了哈希 + 排序。
而 hzhwcmhf 神犇心里自然知道,这题就是后缀数组的 height 中 < L 的个数 + 1,就是后缀自动机上代表的长度区间包含 L 的结点个数,就是后缀树深度为 L 的结点的数量。
但是 hzhwcmhf 神犇看了看 VFleaKing 的做法表示非常汗。于是想卡掉他。
VFleaKing 使用的是字典序哈希,其代码大致如下:
u64 val = 0;
for (int i = 0; i < l; i++)
val = val * base + s[i] - 'a';
u64 是无符号 int64,范围是 [0, 2^64)。VFleaKing 让 val 自然溢出。
base 是一个常量,VFleaKing 会根据心情决定其值。
VFleaKing 还求出来了 base ^ l,即 base 的 l 次方,这样就能方便地求出所有长度为 L 的子串的哈希值。
然后 VFleaKing 给哈希值排序,去重,求出有多少个不同的哈希值,把这个数作为结果。
其算法的 C++代码如下:
typedef unsigned long long u64;
const int MaxN = 100000;
inline int hash_handle(const char *s, const int &n, const int &l, const int &base)
{
u64 hash_pow_l = 1;
for (int i = 1; i <= l; i++)
hash_pow_l *= base;
int li_n = 0;
static u64 li[MaxN];
u64 val = 0;
for (int i = 0; i < l; i++)
val = val * base + s[i] - 'a';
li[li_n++] = val;
for (int i = l; i < n; i++)
{
val = val * base + s[i] - 'a';
val -= (s[i - l] - 'a') * hash_pow_l;
li[li_n++] = val;
}
sort(li, li + li_n);
li_n = unique(li, li + li_n) - li;
return li_n;
}
hzhwcmhf 当然知道怎么卡啦!但是他想考考你。
题目分析
随机生成数据直到卡掉此代码(并不)
先%%%% 一下 VFK 神犇:
首先,假如 base 是偶数,则很容易卡掉,因为 aaa…aa 和 baaa…a(长度在 64 以上),即可卡掉('a'=1,'b'=2)
如果 base 是奇数,VFK 神犇首先证明了一个东西:
我们假设 not(A) 表示将 A 字符串 “取反”,即所有 a 都变成 b,所有 b 都变成 a 后得到的字符串。
假设 A[1]="a",A[2]=A[1]+not(A[1])="ab",A[3]=A[2]+not(A[2])="abba"A[4]=“abbabaab" 以此类推。
则 A[i] 的长度为 $2^{i-1}$
$hash(A[i])=hash(A[i-1]) * base^{2^{i-2}}+hash(not(A[i-1]))$
现在假设 $f[i]=hash(A[i])-hash(not(A[i]))$
则:$f[i]=f[i-1] * (base^{2^{i-2}}-1)$
假设 $g[i]=base^{2^{i-1}}-1$
则:$f[i]=f[i-1] * g[i-1]$, 即 $f[i]=f[1] * g[1] * g[2] * g[3] * … * g[i-1]$
由于 base 是一个奇数,所以 base 的任意次方也是奇数,所以所有的 g 都是偶数。因此:
$$2^{i-1}|f[i]$$
不过这样还不够,因为我们不能构造 $2^{64}$这么长的字符串,所以继续分析:
$g[i]=base^{2^{i-1}}-1=(base^{2^{i-2}}-1)(base^{2^{i-2}}+1)$, 所以 $g[i]=g[i-1]*$偶数
所以 $2^i|g[i]$, 所以
$$2^{i * \frac{i-1}{2}}|f[i]$$
i=11 的时候就可以卡掉了!所以构造就很简单了。
代码
#include<algorithm>
#include<iostream>
#include<cstring>
#include<climits>
#include<cstdio>
using namespace std;
const int N=100000;
char s[N+1];int n=1,l;
int main()
{
s[0]='a';
for(int i=0;i<12;++i){
for(int j=0;j<n;++j)
if(s[j]=='a')s[j+n]='b';
else s[j+n]='a';
n<<=1;
}
l=n>>1;
printf("%d %d\n",n+65,l);
printf("%s",s);
for(int i=1;i<=65;++i)printf("%c",'a');
printf("\n");
return 0;
}
3 条评论
EdithMadshiZhang · 2020年2月12日 11:04 上午
为何证明 pow(2, i(i-1)/2) | f(i) 后就可以确定 i = 11 就可以卡掉了?
Remmina · 2020年2月14日 8:32 上午
好吧,不得不提醒您这篇文章的作者马上就要高考了,也许我可以帮您召唤一下她……
字符串Hash · 2022年1月24日 11:15 下午
[…] 构造题:BZOJ3097 https://www.mina.moe/archives/2391 […]