概述 
Hash (散列函数),其核心思想在于,将输入映射到一个值域较小、可以方便比较的范围。这种转换是一种压缩映射 ,也就是说,不同的输入可能会映射到相同的值,造成哈希碰撞 。
在 OI 中我们常用哈希来比较两数据是否相同,如:字符串哈希(序列哈希),树哈希(树同构),集合哈希。还有一种以 「key-value」 形式存储数据的数据结构:哈希表。
我们通常会设计一个 Hash 函数 F F F   将不方便比较的数据映射到整数。我们希望通过这个函数判断两个数据是否相等,具体来说,哈希函数最重要的性质可以概括为下面两条:
在 Hash 函数值不一样的时候,两个数据一定不一样; 
在 Hash 函数值一样的时候,两个数据不一定一样(但有大概率一样,且我们当然希望它们总是一样的)。 
 
字符串哈希(序列哈希) 
顾名思义,就是用哈希来判断字符串(序列)是否相等。
实现 
通常我们采用的是多项式 Hash 的方法,现有 一个长度为 n n n   的字符串 S S S  (下标以 1 1 1   开始),我们定义哈希函数 (其中 b , M b,M b , M   都是常数)
F ( S ) = ∑ i = 1 n S [ i ] × b n − i ( m o d M ) F(S)=\sum_{i=1}^{n} S[i]\times b^{n-i} \pmod{M}
 F ( S ) = i = 1 ∑ n  S [ i ] × b n − i ( mod M ) 
这样设计哈希函数有一个好处:通过 O ( n ) O(n) O ( n )   的预处理,我们可以 O ( 1 ) O(1) O ( 1 )   获取 S S S   某子串的哈希值。 
具体的,我们可以预处理出 S S S   每个前缀的哈希值,f i = ∑ j = 1 i S [ i ] × b i − j ( m o d M ) f_i=\sum_{j=1}^i S[i]\times b^{i-j}\pmod{M} f i  = ∑ j = 1 i  S [ i ] × b i − j ( mod M )  ,那么有 f i = f i − 1 × b + S [ i ] ( m o d M ) f_i=f_{i-1}\times b+S[i] \pmod{M} f i  = f i − 1  × b + S [ i ] ( mod M )  ,还需要预处理 b i ( m o d M ) b^i \pmod{M} b i ( mod M )   记为 p o w i pow_i p o w i   。
对于一子串 S [ l : r ]   ( 1 ≤ l ≤ r ≤ n ) S[l:r]\,(1\le l\le r\le n) S [ l : r ] ( 1 ≤ l ≤ r ≤ n )   其哈希值为
∑ i = l r S [ i ] × b r − i = ∑ i = 1 r S [ i ] × b r − i − ∑ i = 1 l − 1 S [ i ] × b r − i = ∑ i = 1 r S [ i ] × b r − i − b r − l + 1 ∑ i = 1 l − 1 S [ i ] × b l − 1 − i = f r − p o w r − l + 1 × f l − 1 \begin{aligned}
&\sum_{i=l}^r S[i]\times b^{r-i}\\
=&\sum_{i=1}^r S[i]\times b^{r-i} - \sum_{i=1}^{l-1}S[i]\times b^{r-i}\\
=&\sum_{i=1}^r S[i]\times b^{r-i} - b^{r-l+1}\sum_{i=1}^{l-1}S[i]\times b^{l-1-i}\\
=&f_r-pow_{r-l+1}\times f_{l-1}
\end{aligned}
 = = =  i = l ∑ r  S [ i ] × b r − i i = 1 ∑ r  S [ i ] × b r − i − i = 1 ∑ l − 1  S [ i ] × b r − i i = 1 ∑ r  S [ i ] × b r − i − b r − l + 1 i = 1 ∑ l − 1  S [ i ] × b l − 1 − i f r  − p o w r − l + 1  × f l − 1   
可以通过如下简短的代码实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const  int  MAX = 100000 ;const  int  b = 2333 ;const  int  M = 1e9 +7 ;typedef  long  long  ll;ll pw[MAX], f[MAX]; void  inithash (char  S[], int  n)   {     pw[0 ] = 1 ; f[0 ] = 0 ;     for  (int  i = 1 ; i <= n; ++i) {         f[i] = (f[i - 1 ] * b + S[i]) % M;         pw[i] = pw[i - 1 ] * b % M;     } } ll gethash (int  l, int  r)   {     return  (f[r] - f[l - 1 ] * pw[r - l + 1 ] % M + M) % M; } 
 
碰撞分析 
下面讲一下如何选择 M M M   和计算哈希碰撞的概率,以下内容摘自 OI-wiki 。
这里 M M M   需要选择一个素数(至少要比最大的字符要大),b b b   可以任意选择。如果我们用未知数 x x x   替代 b b b  ,那么 f ( s ) f(s) f ( s )   实际上是多项式环 Z M [ x ] \mathbb{Z}_M[x] Z M  [ x ]   上的一个多项式。考虑两个不同的字符串 s , t s,t s , t  ,有 f ( s ) = f ( t ) f(s)=f(t) f ( s ) = f ( t )  。我们记 h ( x ) = f ( s ) − f ( t ) = ∑ i = 1 l ( s [ i ] − t [ i ] ) x l − i ( m o d M ) h(x)=f(s)-f(t)=\sum_{i=1}^l(s[i]-t[i])x^{l-i}\pmod M h ( x ) = f ( s ) − f ( t ) = ∑ i = 1 l  ( s [ i ] − t [ i ]) x l − i ( mod M )  ,其中 l = max  ( ∣ s ∣ , ∣ t ∣ ) l=\max(|s|,|t|) l = max ( ∣ s ∣ , ∣ t ∣ )  。可以发现 h ( x ) h(x) h ( x )   是一个 l − 1 l-1 l − 1   阶的非零多项式。如果 s s s   与 t t t   在 x = b x=b x = b   的情况下哈希碰撞,则 b b b   是 h ( x ) h(x) h ( x )   的一个根。由于 h ( x ) h(x) h ( x )   在 Z M \mathbb{Z}_M Z M    是一个域(等价于 M M M   是一个素数,这也是为什么 M M M   要选择素数的原因)的时候,最多有 l − 1 l-1 l − 1   个根,如果我们保证 b b b   是从 [ 0 , M ) [0,M) [ 0 , M )   之间均匀随机选取的,那么 f ( s ) f(s) f ( s )   与 f ( t ) f(t) f ( t )   碰撞的概率可以估计为 l − 1 M \frac{l-1}{M} M l − 1   。简单验算一下,可以发现如果两个字符串长度都是 1 1 1   的时候,哈希碰撞的概率为 1 − 1 M = 0 \frac{1-1}{M}=0 M 1 − 1  = 0  ,此时不可能发生碰撞。
 
若进行 n n n   次比较,每次错误率 1 M \dfrac{1}{M} M 1   ,那么总错误率是 1 − ( 1 − 1 M ) n 1-\left(1-\dfrac 1 M\right)^n 1 − ( 1 − M 1  ) n  。在随机数据下,若 M = 10 9 + 7 M=10^9+7 M = 1 0 9 + 7  ,n = 10 6 n=10^6 n = 1 0 6  ,错误率约为 1 1000 \dfrac 1{1000} 1000 1   ,并不是能够完全忽略不计的。
所以,我们使用若干大质数分别取模,这样哈希函数的值域可以扩大到这些质数的乘积。事实上在实际运用中,我们通常取两个 10 9 10^9 1 0 9   左右的孪生质数 就够了。如 10 9 + 7 , 10 9 + 9 10^9+7,10^9+9 1 0 9 + 7 , 1 0 9 + 9  ;1019260817 , 1019260819 1019260817,1019260819 1019260817 , 1019260819  (社会主义好质数 )
应用 
字符串哈希主要是用来判断两字符串是否相同的工具,简单暴力,常用于骗分。下面举几个具体的例子,虽然字符串哈希可能并不是解决这些问题的最优方法,但起简单易懂,比较好实现。
字符串匹配 
求出模式串哈希值,对于文本串每个长度为模式串长度的子串求哈希值,分别与模式串进行比较即可。
允许 k k k   个位置失配的字符串匹配 
问题描述 :给定长度为 n n n   的文本串 T T T  ,以及长度为 m m m   的模式串 P P P  ,问文本串中有多少子串与 P P P   匹配。这里匹配定义为最多 k k k   个位置不相同。 (1 ≤ n , m ≤ 10 6 , 1 ≤ k ≤ 5 1\le n,m\le 10^6,1\le k\le 5 1 ≤ n , m ≤ 1 0 6 , 1 ≤ k ≤ 5  )
该问题 KMP 算法无法解决,但可以 二分+哈希。枚举 T T T   中每个可能匹配的子串,二分找到第一个失配的位置,忽略失配位置及之前位置,继续查找下一个位置,最多重复 k k k   次,复杂度 O ( m + k n log  n ) O(m+kn\log n) O ( m + kn log  n ) 
最长回文子串 
一种 O ( n log  n ) O(n\log n) O ( n log  n )   的做法是枚举回文中心,再二分回文半径,需要分别预处理字符串正着和倒着的哈希值。
实际上,通过哈希也有 O ( n ) O(n) O ( n )   的做法,记 r i r_{i} r i    表示以 i i i   为结尾的最长回文串长度,注意到 r i ≤ r i − 1 + 2 r_i\le r_{i-1}+2 r i  ≤ r i − 1  + 2  ,那么每次新枚举到一个 i i i  ,让 r i r_i r i    由 r i − 1 + 2 r_{i-1}+2 r i − 1  + 2   递减,找到第一个回文串。答案就是 max  { r i } \max\{r_i\} max { r i  }   。复杂度证明:每次 r i r_i r i    最多 + 2 +2 + 2  ,暴力判断一次变会 − 1 -1 − 1  ,,减小量小于等于增加量,故最多判断 2 n 2n 2 n   次。
最长公共子字符串 
问题描述 :给定 n n n   个总长度不超过 m m m   的字符串,求所有字符串的最长公共子字符串,若有多个,输出任意一个。 1 ≤ n , m ≤ 10 6 1\le n,m\le 10^6 1 ≤ n , m ≤ 1 0 6  。
显然若有一个长度为 k k k   的公共子串,必然有长度为 k − 1 k-1 k − 1   的公共子串。二分答案 l e n len l e n  ,考虑如何判断合法:我们只需要将每个长度为 l e n len l e n   的串的哈希值放到哈希表里,再取交集即可。
复杂度 O ( m log  m n ) O(m\log \frac{m}{n}) O ( m log  n m  )  。
当然,你可以用 SAM/SA 在线性时间内暴切此题。(话说若字符集大小为 10 6 10^6 1 0 6   ,SAM 也要带 log  \log log    )
后缀排序 
问题描述 :给定一个长度为 n n n   的字符串 S S S  ,将字符串的所有非空后缀按字典序排序,输出排名。
这是 SAM/SA 板题,显然有 O ( n log  n ) O(n\log n) O ( n log  n )   ,O ( n ) O(n) O ( n )   的优秀做法。这里字符串哈希来凑个热闹,提供一个 O ( n log  2 n ) O(n\log ^2n) O ( n log  2 n )   的做法。
暴力排序即 string + sort ,sort 本身复杂度 O ( n log  n ) O(n\log n) O ( n log  n )   ,string 一次比较 O ( n ) O(n) O ( n )   ,所以有了 O ( n 2 log  n ) O(n^2\log n) O ( n 2 log  n )   的极高复杂度。考虑如何降低两后缀一次比较的复杂度,对于后缀 S [ i : n ] , S [ j : n ] S[i:n],S[j:n] S [ i : n ] , S [ j : n ]  ,二分最大的 k k k   使 S [ i : i + k − 1 ] = S [ j : j + k − 1 ] S[i:i+k-1]=S[j:j+k-1] S [ i : i + k − 1 ] = S [ j : j + k − 1 ]   ,此时 S [ i + k ] ≠ S [ j + k ] S[i+k]\ne S[j+k] S [ i + k ]  = S [ j + k ]   ,即可通过这位比较两后缀的大小,以上判断子串相同字符串哈希足矣。
一次比较复杂度 O ( log  n ) O(\log n) O ( log  n )  ,故总复杂度 O ( n log  2 n ) O(n\log^2 n) O ( n log  2 n )  。
线段树维护哈希值 
顾名思义,就是说字符串会有修改,我们需要用线段树动态维护哈希值。
具体的,线段树每个节点维护其对应子串的哈希值 ,因为到时候要哈希值拼接,需要用到字符串长度,建议开结构体,重载运算符:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #define  fi first #define  se second typedef  long  long  ll;typedef  pair<ll,ll> pll;const  int  p1 = 1019260817 ; const  int  p2 = 1019260819 ;const  int  bs = 125641 ; pll operator *(const  pll &a, const  ll &b) {return  pll (a.fi * b % p1, a.se * b % p2);} pll operator *(const  pll &a, const  pll &b) {return  pll (a.fi * b.fi % p1, a.se * b.se % p2);} pll operator +(const  pll &a, const  ll &b) {return  pll ((a.fi + b) % p1, (a.se + b) % p2);} pll operator -(const  pll &a, const  pll &b) {return  pll ((a.fi - b.fi + p1) % p1, (a.se - b.se + p2) % p2);} pll operator +(const  pll &a, const  pll &b) {return  pll ((a.fi + b.fi) % p1, (a.se + b.se) % p2);} pll pw[N], pws[N];  void  init (int  n)   {    pw[0 ] = {1 , 1 };     for  (int  i = 1 ; i <= n; ++i) pw[i] = pw[i - 1 ] * bs; } struct  dat  {    pll f;      int  len;      dat () { f = {0 , 0 }; len = 0 ; }     dat (const  pll &_f,const  int  &_len) { f = _f; len = _len; } }; dat operator *(const  dat &a, const  dat &b) {      return  dat (a.f * pw[b.len] + b.f, a.len + b.len); } 
 
哈希表 
哈希表是又称散列表,一种以 「key-value」 形式存储数据的数据结构。只需要输入查找的值 k e y key k ey  ,就可以快速地找到其对应的 value。k e y key k ey   可以是很大的整数,浮点数,字符串甚至结构体。类似于 STL 中的 map ,不过 map 是用红黑树实现的,单次操作复杂度  O ( log  n ) O(\log n) O ( log  n )   的,而哈希表在平均情况下 能在 O ( 1 ) O(1) O ( 1 )   时间内完成(但在在最坏情况下,时间复杂度为 O ( s i z e ) O(size) O ( s i ze )    )。
实现 
在 OI 中,最常见的情况应该是 k e y key k ey   为大整数的情况,此时不能将 k e y key k ey   直接当作数组下标。一般把 k e y key k ey   模一个较大的质数作为索引,也就是取 f ( x ) = x   m o d   M f(x)=x \bmod M f ( x ) = x mod M   作为哈希函数。若 k e y key k ey   是字符串,一般先算出字符串的哈希值,再把其哈希值作为 k e y key k ey   插入到哈希表里。
理想情况下,不同的 k e y key k ey   产生不同的哈希值,假设我们用数组 a a a   存放数据,对于一对键值 ( k e y , v a l u e ) (key,value) ( k ey , v a l u e )  ,直接 a [ f ( k e y ) ] = v a l u e a[f(key)]=value a [ f ( k ey )] = v a l u e   即可。但往往会有不同的 k e y key k ey   会有相同的哈希值,此时需要处理哈希冲突。OI 中最常用的是拉链法。
拉链法 
即在每个存数据的位置开一个链表,将多个 f ( k e y ) f(key) f ( k ey )   相同的 k e y key k ey   放在对于位置的链表中,查询时将对于位置链表整个扫一遍,比较其 k e y key k ey   与查询的 k e y key k ey   是否一致。
闭散列法 
闭散列方法把所有记录直接存储在散列表中,如果发生冲突则根据某种方式继续进行探查。 
比如线性探查法:如果在 d d d   处发生冲突,就依次检查 d + 1 , d + 2 , … d+1,d+2,\dots d + 1 , d + 2 , …  ; 
二次探测再散列法:如果在 d d d   处发生冲突,就依次检查 d + 1 , d − 1 , d + 4 , d − 4 , d + 9 , d − 9 , … d+1,d-1,d+4,d-4,d+9,d-9,\dots d + 1 , d − 1 , d + 4 , d − 4 , d + 9 , d − 9 , …  。
代码实现 
以下是拉链法的代码实现,这里的 k e y , v a l u e key,value k ey , v a l u e   都是 long long 类型,异常操作返回 -1。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 typedef  long  long  ll;const  int  M = 19260817 ;const  int  MAX_SIZE = 2000000 ;struct  Hash  {    struct  data  {         int  nxt;         ll key, value;     } e[MAX_SIZE];     int  head[M], size;     inline  int  f (ll key)   { return  key % M; }      void  clear ()   {          memset (head, -1 , sizeof (head));         size = 0 ;     }     ll query (ll key)   {          for  (int  i = head[f (key)]; i != -1 ; i = e[i].nxt)             if  (e[i].key == key) return  e[i].value;         return  -1 ;     }     ll modify (ll key, ll value)   {          for  (int  i = head[f (key)]; i != -1 ; i = e[i].nxt)             if  (e[i].key == key) return  e[i].value = value;         return  -1 ;     }     ll insert (ll key, ll value)   {          if  (query (key) != -1 ) return  -1 ;         e[++size] = data{head[f (key)], key, value};         head[f (key)] = size;         return  value;     } }; 
 
简单封装的模板,可以像 map 那样直接用中括号访问 「下标」。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 typedef  long  long  ll;const  int  M = 19260817 ;const  int  MAX_SIZE = 2000000 ;struct  Hash_map  {    struct  data  {         int  nxt;         ll key, value;      } e[MAX_SIZE];     int  head[M], size;     inline  int  f (ll key)   { return  key % M; }      ll &operator [](const  ll &key) {          int  ky = f (key);         for  (int  i = head[ky]; i != -1 ; i = e[i].nxt)             if  (e[i].key == key) return  e[i].value;         return  e[++size] = data{head[ky], key, 0 }, head[ky] = size, e[size].value;     }     void  clear ()   {          memset (head, -1 , sizeof (head));         size = 0 ;     }     Hash_map () { clear (); }  }; 
 
STL 无序关联式容器 
自 C++11 标准起(注意目前 CCF 的一系列赛事不支持 C++11),四种基于哈希实现的无序关联式容器正式纳入了 C++ 的标准模板库中,分别是:unordered_set,unordered_map,unordered_multiset,unordered_multimap。
常用的是前两个。 map 被卡常,又懒得手写哈希表,可以使用 unordered_map,用法与 map 差不多, 但 unordered_map 是不排序的。
自定义哈希函数 
在哈希函数确定的情况下,可以构造出数据使得容器内产生大量哈希冲突,导致复杂度达到上界。标准库中的模数一般是 126271 , 107897 126271,107897 126271 , 107897   ,此时可以通过插入大量模数的倍数来造成大量冲突。自定义哈希函数可以有效避免构造数据产生的大量哈希冲突。
具体地,如下定义即可,需要注意 my_hash 中 x 的类型要和 unordered_map 中存储的类型对应:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 struct  my_hash  {    size_t  operator () (int  x)  const   {          x += 0x2e6f39b17f9c1a51 ;         x = (x ^ (x >> 30 )) * 0xad5299bbe1aee5b9 ;         x = (x ^ (x >> 27 )) * 0x94d04f24133111eb ;         return  x ^ (x >> 31 );     }          size_t  operator () (pair<int , int > x)  const   {         uint64_t  u = (uint64_t )((x.first ^ 0xd121a3e8c6453277 ) << 32 ) | (x.second ^ 0x7c8312029eb1c5d2 );         u ^= (u >> 32 );         u ^= (u << 27 );         u ^= (u >> 31 );         return  u;     } }; int  main ()   {    unordered_map<pair<int , int >, int , my_hash> mp;     mp[make_pair (1 , 2 )] = 10 ;     mp[make_pair (1 , 3 )] = 20 ;     mp[make_pair (3 , 4 )] = 30 ;     cout << "mp[(1, 3)] = "  << mp[make_pair (1 , 3 )] << endl;     mp.erase (make_pair (1 , 3 ));     cout << "size = "  << mp.size () << endl;     cout << "count (1, 3) = "  << mp.count (make_pair (1 , 3 )) << endl;     for  (auto  it = mp.begin (); it != mp.end (); ++it) {         cout << "("  << it->first.first << ", "  << it->first.second << ") = "  << it->second << endl;     }     mp.clear ();     cout << "is_empty = "  << mp.empty () << endl;      return  0 ; } 
 
常见的哈希 
树哈希 
有时我们要判断一些树是否同构,可以用恰当的哈希方式来将树映射成一个便于储存的哈希值。 
若解决了有根树的树哈希,那无根树哈希也迎刃而解了:以重心为根,如果重心有两个,分别判断即可。
树哈希的方法有很多,这里只介绍两种靠谱的方法。
方法一 
按照 dfs 序可以将树上结点对应到序列上,在序列上填上对应结点的深度,如上图得到序列为:
1 2 3 3 3 2 3 2 \begin{array}{c}
1&2&3&3&3&2&3&2
\end{array}
 1  2  3  3  3  2  3  2  
显然,根据序列可以还原出形态唯一的树,那么树哈希便转换成了序列上的哈希。具体的,需要递归处理,记 h a s h ( u ) hash(u) ha s h ( u )   为 u u u   子树的哈希值,初始 h a s h ( u ) = d e p u hash(u)=dep_u ha s h ( u ) = d e p u    ,插入 v v v   子树,就相当于两个序列接起来,h a s h ( u ) ′ = h a s h ( u ) × b a s e s i z v + h a s h ( v ) hash(u)'=hash(u)\times base^{siz_v}+hash(v) ha s h ( u ) ′ = ha s h ( u ) × ba s e s i z v  + ha s h ( v )   ,( b a s e base ba se   为底数,s i z v siz_v s i z v    为 v v v   子树大小 )。
如果交换子树的顺序算同构,把 u u u   所有孩子的哈希值排序后再加入; 
如果要判断两个不同深度的子树是否同构,把深度换成高度(到最深叶子结点的距离)。 
 
该树哈希冲突的概率与字符串哈希是一样的。
方法二 
按照以下公式:
f u = c + ∑ v ∈ s o n ( u ) f v × p r i m e ( s i z e v ) f_{u}=c+\sum_{v\in son(u)} f_{v}\times prime(size_{v})
 f u  = c + v ∈ so n ( u ) ∑  f v  × p r im e ( s i z e v  ) 
其中 f u f_u f u    表以 u u u   为根子树对应的哈希值,s i z e v size_v s i z e v    表 v v v   子树大小,s o n ( u ) son(u) so n ( u )   为 u u u   点孩子的集合,p r i m e ( i ) prime(i) p r im e ( i )   为第 i i i   个质数(也可以再 random_shuffle 一下效果更好)。c c c   随便搞个常数。
集合哈希 
集合中元素是无序的,将集合中元素排序后再序列哈希即可。若要动态维护,用平衡树维护每个元素的排名。
若全集是确定的,可以先将所有元素离散化,哈希函数定义为 f ( S ) = ∑ a ∈ S b i d ( a ) ( m o d M ) f(S)=\sum_{a\in S} b^{id(a)} \pmod{M} f ( S ) = ∑ a ∈ S  b i d ( a ) ( mod M )   ,其中 i d ( a ) id(a) i d ( a )   表示 a a a   离散化后的标号,b b b   是个常数,M M M   是模数。若是多重集哈希则可以 f ( S ) = ∑ a ∈ S c n t ( a ) × b i d ( a ) ( m o d M ) f(S)=\sum_{a\in S} cnt(a)\times b^{id(a)} \pmod{M} f ( S ) = ∑ a ∈ S  c n t ( a ) × b i d ( a ) ( mod M )  ,其中 c n t ( a ) cnt(a) c n t ( a )   表 a a a   出现次数。
例题 
难度应该是递增的,直接在这里贴代码不方便,放个链接吧 例题 AC CODE 
序列哈希板题 .
将矩阵每行按照序列哈希预处理,一个方阵的哈希值将几行拼起来即可。枚举方阵边长,第一个矩阵的所有哈希值放入 set,另一个查找 set 中是否出现即可。
树哈希 .
树的同构版题,按照 『树哈希 』 中介绍的两种方法做即可。
序列哈希+哈希表+二分 .
即 最长公共子字符串 
哈希+二项式反演 .
题目让我们求『恰有』 K 个区对应相同,且容易算得『至少』K 个区对应相同,设前者为 g k g_k g k   ,后者为 f k f_k f k    ,显然 f k = ∑ i = k 6 ( i k ) g i f_k=\sum_{i=k}^{6} \binom{i}{k} g_i f k  = ∑ i = k 6  ( k i  ) g i   ,根据套路 g k = ∑ i = k 6 ( − 1 ) k − i ( i k ) f i g_k=\sum_{i=k}^6 (-1)^{k-i}\binom{i}{k} f_i g k  = ∑ i = k 6  ( − 1 ) k − i ( k i  ) f i   。考虑如何算 f k f_k f k    ,枚举 k k k   个位置,计算只考虑这 k k k   个位置有多少对是相同的,这里需要哈希表。
树哈希+换根dp+排列组合 .
题目让我们求的是遍历一棵无根树产生的本质不同括号序列方案数。首先对于两个不同构的有根树,其产生的括号必然不同,考虑树哈希判同构。 
先令根为 1 1 1  ,设 f u f_u f u    表示遍历 u u u   子树本质不同括号序列方案数,考虑转移,s o n ( u ) son(u) so n ( u )   表 u u u   孩子的集合:
f u = ∣ s o n ( u ) ∣ ! n 1 ! n 2 ! … n k ! ∏ v ∈ s o n ( u ) f v f_u=\frac{|son(u)|!}{n_1!n_2!\dots n_k!}\prod_{v\in son(u)} f_v
 f u  = n 1  ! n 2  ! … n k  ! ∣ so n ( u ) ∣ !  v ∈ so n ( u ) ∏  f v  
右边所有 f v f_v f v    的乘积显然,左边表示有 ∣ s o n ( u ) ∣ ! |son(u)|! ∣ so n ( u ) ∣ !   种遍历顺序,但有的子树会同构,需要去重,n j n_j n j    就表示某种子树有多少个 ($\sum_{i=1}^k n_i = |son(u)| $),相当于多重集的排列。 
要求出每个点为根的本质不同括号序列方案数,换根 DP 就行了。(因为要换根所以方法一 不方便,使用方法二 被卡哈希的话可以看看 std )
线段树维护哈希+循环串 .
首先 S [ l : r ] S[l:r] S [ l : r ]   是周期为 d d d   的循环串    ⟺    \iff ⟺   S [ l + d : r ] = S [ l , r − d ] S[l+d:r]=S[l,r-d] S [ l + d : r ] = S [ l , r − d ]  。证明显然,画个图就明白了。 
题目要求区间赋值,显然线段树维护哈希,打懒标记没问题,考虑如何快速修改哈希值。显然一段长度为 l l l  ,都是 a a a   的字符串哈希值为 ∑ i = 0 n − 1 a × b i = a ∑ i = 0 n − 1 b i \sum_{i=0}^{n-1} a\times b^i= a\sum_{i=0}^{n-1} b^i ∑ i = 0 n − 1  a × b i = a ∑ i = 0 n − 1  b i   ,预处理 b i b^i b i   前缀即可。
主席树+哈希+最短路 .
首先需要往最短路方向想这题。每个点的 d i s dis d i s   可以表示成一个按排序表顺序的数量序列,第 i i i   个元素表示优先级为 i i i   的属性的出现次数。每经过一条边,就相当于单点加一,可以用主席树维护。 
那么如何判定两个序列的大小?我们需要找到的是这两个序列的第一个不同元素。主席树上维护区间哈希值来判断区间是否相同,然后二分即可。 
因为需要用主席树维护,后面的状态基于前面的,所以不能 spfa ,只能 dijkstra 。但是空间复杂度是 O ( m l o g n ) O(mlogn) O ( m l o g n )   ,会 MLE 。 
我们发现每一次修改的时候,由于一个点自身的属性不会变,所以每次修改这个点的时候,一定是从一个状态继承,并修改 其属性的 r a n k rank r ank   这个位置上的值,所以每次新建的节点都是一样的,所以直接覆盖到原先建出来的节点上即可,空间复杂度 O ( n l o g n ) O(nlogn) O ( n l o g n )   。堆优化 dijkstra ,时间复杂度 O ( n log  2 n ) O(n\log^2n) O ( n log  2 n )   。(比较大小需要一个 log  \log log   )
备用链接1  备用链接2 
字符串哈希+多重集哈希+哈希表+排列组合 .
自然想到枚举 d d d  (即矩阵的宽度),此时的划分方案只有 n / d n/d n / d   种(短串只有 n / d n/d n / d   个位置可以放 ),此时要先对这 n / d n/d n / d   个划分去重。两个划分等价当且仅但这两划分的 n / d n/d n / d   的字符串在排序后相同。 
具体地,在一个种划分中可能会出现多个相同的串,需要哈希表统计每种串出现次数(其实就是个多重集)。对比两种划分是否等价多重集哈希可以解决。最后考虑一下一种划分的贡献,假设改划分 s 1 s_1 s 1    出现了 c 1 c_1 c 1    次, s 2 s_2 s 2    出现了 c 2 c_2 c 2    次……  s k s_k s k    出现了 c k c_k c k    次,其贡献为 ( ∑ i = 1 k c i ) ! c 1 ! c 2 ! … c k ! \dfrac{(\sum_{i=1}^k c_i)!}{c_1!c_2!\dots c_k!} c 1  ! c 2  ! … c k  ! ( ∑ i = 1 k  c i  )!    (多重集排列数)。
毒瘤题 .
官方题解:sol 
References