初等字符串

$CuO + CO \triangleq Cu + CO_2 $

初等字符串

字符串Hash


\(\bf{Hash}:\)一种好用又cd的算法

\(·First\)

如果要比较两个字符串的大小,开\(string\)两两比较是\(O(n)\)の算法如果进行\(m\)次比较的话$O( m \times n ) $显然去世

考虑\(O(m)\)の算法 , 即让比较过程变为\(O(1)\)

那么如果可以将字符串表示成一个数,那么就好说很多了。

如何用k进制来表示一个数?

\(eg:\)

\[(114514)_{10} \ = \ \ \ \ ( \ ? \ )_k \]

......

其实字符串也可以这么表示,最后の值叫他 Hash

对于字符串\(s_1s_2s_3.....s_n\)

我们可以用\(hash_s\)表示

那么\(hash_s:\)
$$ hash_s = s_1*k^{n-1} \times s_2 \times k ^{n-2} \times s_3 \times k^{n-3} \times ... \times s_n \times k^0 $$
其中\(k\)是一个\(\ge\)字符串中最大字符大小の数\((例:Cekasauk中の's')\),最好是个素数(前置知识·素数)

我用\(131\)较习惯

此时是不是觉得\(hash\)非常好用?

\(\mathrm{·美中不足:\color{red}哈希冲突}\)

\(\because\)对于字符来说,\(0\)的值是\(48\)

\(\therefore\)任一其他字符都会很大

如果我在每一个字符上都乘上一个\(131^\alpha\)次方,再乘起来 , 很有可能爆 $long \ long $

\(\therefore\)在计算字符串哈希值时,再摸一个大整数(最好是素数)

这时就可以知道一个字符串の\(hash\)值了。

\(\color{blue}But:\)

$ mod \ p $ 带来の问题十分明显了。

如果两个字符串不相等,但\(mod\)让他们整好相等了,\(hash\)值返回的就是\(WA\)的。

这就叫做哈希冲突。

哈希冲突无法避免,但可以考虑优化。


$·考虑优化\implies second : \ hash \ の优化 $

  1. 双模数哈希

    模一个数\(p\)如果不准的话就再模一个数\(q\),且\(gcd( \ p \ , \ q \ ) \ = 1\)

    精准很多

  2. $ unsigned \ long \ long $

    如果模数太小导致哈希冲突の话,就想方法让模数尽可能的大。

    $ unsigned \ long \ long $ : 是很不错的选择。

    \(2^{64}-1の超大存储量\) ,oppo23,模出你的美

    $ but $ 永远不是完美的

    $ unsigned \ long \ long $ 是一个 $ \rm{很好卡的数值} $。

    出题人会卡死你。
    但平常の题用它还是很不错的。

  3. 哈希表

    (详见\(Fifth\))


$ code:(\ of \ unsigned \ long \ long \ ) $

点击查看代码
#define int unsigned long long 
using namespace std ; 
const int down = 131 //底数
int does[ N ] ; // down^k
string s ; // 模式串
string b[ N ] ; 
int n ; 
int hash_s , hasher[ N ] ; 
inline void Make_Hash( string s , int &hashwer )
{
    int len = s.size( ) ; 
    for ( int i = 0 ; i < len ; ++ i )
    {
        hashwer += s[ i ] * does[ n - i + 1 ] ; // 看做从一开始
    }
}
signed main( )
{
    cin >> n ; 
    cin >> s ; 
    Make_Hash( s , hash_s ) ; 
    for( int i = 1 ; i <= n ; ++ i )
    {
        cin >> b[ i ] ; 
        Make_Hash( b[ i ] , hasher[ i ] ) ; 
        if( hasher[ i ] == hash_s )
        cout << "Yes" << '\n' ;
        else cout << "Fucking Bich " << '\n' ; 
    }
}

\(·Third - hash \ 求前缀和\)

如果只能比较单个字符串,这个算法就太\(\color{white}逊\)了。

\(\therefore\) 这里有这么个题:

给定模式串b , 目标串s , 求在目标串中的前缀[1,r]是否等于b

\(s\)中前\(i\)个元素

\[\ \ \ \ hash_i = s_1 \times k ^ { n - 1 } \times s_2 \times k ^ { n - 2 } \times ... \times s_i \times k ^{ n - i } \]

\[hash_b = b_1 \times k ^ { i - 1 } \times b_2 \times k ^ { i - 2 } \times ... \times b_i \times k^0 \]

你要让\(b = \!\!= s[ \ 1 \ , \ r \ ]\)

那么b.size( )一定 \(=\!\!=\) s[ 1 , r ].size( )

\(hash_b\)左右同时乘$k^{ n - i } $

\[hash_b \times k ^{ \ n \ - \ i \ } =\!\!= b_1 \times k ^ { n - 1 } \times b_2 \times k ^ { n - 2 } \times b_i \times k ^ { n - i } \]

\(\because\)模过了数 $ \ \ \ $ \(\therefore\) 不能再除了\((不要和我一样sb)\)

\(\therefore\)只需要比较 \(hash_i\) $ and $ $ hash_b \times k ^{ n - i } $


$ ·Fourth $

求在目标串中与模式串相等の个数

运用\(Thrid\)の知识,自己想(md烦不想写)


·\(Fifth\) 哈希冲突の另一个解决方法

( 东西比较多 , 放这里讲 )

\(\bf{Hash表}\) 一种不是很神奇,但有时很给力。

把它作用说一下:

当查询这个\(Hash\)值代表的字符串时,查询这一个\(Hash\)值所代表的所有字符串。

类比一下:

当你在查字典时:

比如你想查这个字时,你按照cai这个音去寻找,

但是发现cai这个地方第一个字是

cai就相当于那个\(Hash\)值,此时就相当于哈希冲突了。

你在思索解决方法:

  1. 一个字一页

这种想法属实\(GS\),太废纸了。

  1. 接着往下翻

翻不了几页就到了。

一页之后才是一页,是一个链式的结构。

哈希表就是这样了,对于\(Hash\)值相同时,用链将所有此串连起来。

·\(code\)

点击查看代码
struct Hash_Table_Of_List
{  
    struct One_node 
    {
        int key_save ;
        int value , next ;
    } ;
    One_node grass[ CuFeO4 << 1 ] ;   
    int head[ CuFeO4 << 1 ] , cnt ;
    int Hash_return( int key ) 
    { 
        return ( key % mod + mod ) % mod ; 
    }
    int& operator[]( int key_in ) 
    {
        int inlist = Hash_return( key_in ) ;  // 获取头指针
        for (int i = head[ inlist ] ; i; i = grass[ i ].next )
        if ( grass[ i ].key_save == key_in )
        return grass[ i ].value ;
        return grass[ ++ cnt ] = ( One_node ){ key_in, 0, head[ inlist ] } , head[ inlist ] = cnt , grass[ cnt ].value ;
    }
    Hash_Table_Of_List( ) 
    {
        cnt = 0;
        memset( head , 0 , sizeof( head ) ) ; 
    }
}hasher;

当然还用另一种哈希表速度还要快一点,就是当此时此位被占后,就往下接着数,直到没有元素的位置,将此个放进去

最后插一句,最好的还是平板电视。


$ ·小结 $

在做字符串的题时,\(Hash\)是一个很给力的算法。

但他是很危的。\(Hash\)出题人想卡就能被卡掉。

在当下\(Hash\) 是一个\(dying\)的算法

只要知道你的模数,无论你的底数,都能卡掉你。(参见 \(Hash \ killer_2\) )

如果\(Hash \ killer_3\)被人AC了的话

那么这个算法,

\(\bf{我们真的要和她说再见了...}\)

所以,

珍惜当下,这个好用的算法

\(\bf{毕竟错过了,就不会再有了}\)


KMP


·赘述

说实话,不是一个很好理解的算法。

  1. \(KMP\)( $ D.E.Knuth $ ,$ J.H.Morris$ $ and $ $ V.R.Pratt $ )
    是三个科学家提出的,\(\bf{不要理解错误!}\)

例题:

求在目标串中与模式串相等の个数,并输出左右端点。

当然可以用\(Hash\) $ , O( \ n \ ) $ 算法也很棒棒了

但 $ \ . \ . \ . \ . \ . \ . \ $

如果卡你 怎么办

\(\color{yellow}KMP,你值得拥有\)

$border $(可能拼错了 $ \ \ \ $ \([:-)]\) $ \ \ \ $ ):前缀和后缀相等

\(eg.\) $ \color{red}{114514}$ \(1919\) \(\color{red}114514\)

\(Next_i:\)字符串s中$[ \ 1 \ , \ i \ ] $ 前缀中最长的\(border\)


·$First \ $ 求\(Next\)

1.简述

首先定义$ Next_1 = 0 $

\(Next_2\)开始遍历。

定义\(j\)指向\(Next_i\)表示当前\(i\)指向の\(Next\)

\(j\)\(i\)一起向上跳

并让$s_{j+1} = s_i $

如果不等,\(j=Next_j\)去求真正最大的\(border\)

2.形述

仔细想一想,\(i\)肯定由\(i-1\)递过来

当$ j + 1 $ 的字符值与$ i $相等时,那肯定要它呀。

但如果不是,对于你这个真正要求的\(Next_i\)来说,肯定是是在 [ 1 , j + 1 ) 中 。 ( \(\because\) $ j + 1 $ 无法取,肯定不在$ j + 1 $外 )

对于这个\(j\)它是\(Next_{i-1}\)

$ i - 1 $ 后面加了一个字符,所以这个新的最长 \(border\) 肯定是原来的一个 \(border\) 后多一个等于 \(i\) 字符值的字符。

如果这个$ Next_j \ + \ 1 $ 必须得字符值等于 \(i\)


$code \ of \ asking \ for \ Next_i $

点击查看简短的代码
    for( int i = 2 , j = 0 ; i <= len ; ++ i )
    {
        while ( j && s[ i ] != s[ j + 1 ] ) j = Next[ j ] ; 
        if( s[ i ] == s[ j + 1 ] ) j ++ ; 
        Next[ i ] = j ; 
    }

肯定会有人质疑这个算法的时间复杂度(\(somebody \ have \ said \ : \ md \ 一眼 \ n^2 \ 嘛!\))

但是很抱歉,\(\bf{它不是}\)

\(j\) 最多到 \(n\) , 但在其中 \(j\) 处于单调递减状态,\(\therefore\) $ while $ 循环最多执行\(n\)次。
因此复杂度为\(O( \ n \ )\)

//\(注意\) \(Next\)N大写


\(·second\) 匹配串

其实代码长相和前面的几乎一模一样

1. \(Next_i\)在这里の用处

jb想一想也是求模式串的\(Next\)呀亲

2.实现

用 $ j $ 表示目前模式串跳到的位置,$ i $ 表示目标串跳到的位置

循环时枚举的肯定是\(i\)

如果此时\(s_i = t_{j+1}\)那么肯定是要它的。但如果不等了怎么办?

\(j\)回到\(Next_j\)那么如果此时还不行接着跳。

直至$s_i = t_{j+1} \ || \ $ 将\(j\)排空

当你判完后(已经find完一个匹配串)我不想修改\(j\)\(\color{red}:-)\)

在让\(j=Next_j\)的时候多加一种\(j=\!\!\!=n\)の情况


$code \ of \ matching \ strings $

点击查看代码
    for ( int i = 1 , j = 0 ; i <= len ; ++ i ) // t是目标串,s是模式串,len是t长,n是s长
    {
        while ( j && ( j == n || t[ i ] != s[ j + 1 ] ) ) j = Next[ j ] ; 
        if( t[ i ] == s[ j + 1 ] ) 
        {
            j ++ ; 
        }
        if( j == n )
        {
            cout << i - j + 1 << ' ' << j  << '\n' ; 
        }
    }
代码复杂度分析如上。$O( \ n \ ) $

例题


poj2752

hzoj,Seek the Name, Seek the Fame

简化题意:给一个目标串,求其所有のborder长度,按增序输出

adcad (ad、adcad) => 2 5 

考察对\(Next\)的理解

只需要求\(Next_i、Next_{Next_i}、Next_{Next_{Next_i}} ... 求到0前\)

注意输出总长。


uojNOI2014动物园

hzoj、动物园

简化题意:
定义num[ i ] 为在长度为i的前缀,没有任何重叠的border的个数

求:

\[\prod_{i=1}^n (num_i+1) \ \ mod \ \ 1000000007 \]

考虑\(num\)\(Next\)的关系。

定义一个\(numer_i\) 为长度为\(i\)的前缀中\(border\)的个数。

易求出:$numer_i = numer_{next_i} + 1 $

对于这个\(num\)来说,其中最长的长度是 $\leq \ i \div 2 $

\(\therefore\) 对于此时我们可以对于一个\(j\)如果不符合就使之变为\(Next_j\)

具体看代码:

$ code : $
点击查看代码
#include<bits/stdc++.h>
#define int long long 
using namespace std ; 
const int N = 1e6 + 100 ; 
int n ; 
int Next[ N ] ; 
char s[ N + 11 ] ;  
int num[ N ] , numer[ N ] ;  
const int mod = 1000000007 ; 
inline int read( )
{
    int x = 0 , f = 1 ; 
    char c = getchar( ) ; 
    while ( c < '0' || c > '9' )
    {
        if( c == '-' )
        {
            f = - f ; 
        }
        c = getchar( ) ; 
    }
    while ( c >= '0' && c <= '9' ) 
    {
        x = x * 10 + c - '0' ; 
        c = getchar( ) ; 
    }
    return x * f ; 
}
signed main( )
{
    n = read( ) ; 
    while( n -- )
    {
        memset( Next , 0 , sizeof ( Next ) ) ; 
        memset( num , 0 , sizeof ( num ) ) ; 
        memset( numer , 0 , sizeof( numer ) ) ; 
        cin >> s + 1 ; 
        int len = strlen( s + 1 ) ; 
        Next[ 1 ] = 0 ; 
        int ans = 1 ; 
        int jj ; 
        numer[ 1 ] = 1 ; 
        for ( int i = 2 , j = 0 ; i <= len ; ++ i )
        {
            while ( j && s[ j + 1 ] != s[ i ] ) j = Next[ j ] ; 
            if( s[ j + 1 ] == s[ i ] ) ++ j ; 
            Next[ i ] = j ; 
            numer[ i ] = ( numer[ j ] + 1 ) % mod ;
        }
        for( int i = 2 , j = 0 ; i <= len ; ++ i )
        {
            while ( j && s[ j + 1 ] != s[ i ] ) j = Next[ j ] ; 
            if( s[ j + 1 ] == s[ i ] ) ++ j ; 
            while( j * 2 > i ) j = Next[ j ] ; 
            //cout << numer[ j ] << endl ; 
            ans = ( ans * ( numer[ j ] + 1 ) % mod ) % mod ; 
        }
        cout << ans << '\n'; 
    }
}

\(小结\)

\(KMP\)在大多数情况下时非常好用的,你并没有能够卡掉它的方法

$O( \ n \ ) $ 的复杂度也令人垂涎三尺。

为了在迟暮之年不会忘了自己曾经学过这个算法,于此写下题解。


Manacher (马拉车算法)

  1. 由来

科学家\(Manacher\)提出的算法

\(·First\) 赘述

马拉车解决的问题:

求一个字符串中各个回文串的长度

正反哈希再加上二分答案,$O( \ n \ log \ n \ ) $的算法其实已经够了。

但是,马拉车提供了更加优的算法,远超并碾压其他的求解此题的的算法

以$O( \ n \ ) $の司马复杂度傲视群雄。


\(·second\) 算法分析

·\(일\) -- 提前の处理

回文串有几种不同的类型,$sach \ as $ cxxxc \(||\) cxxc .

对于前面一种来说 , 回文中心是最中间的x,但对于后一种长度为偶数的来说回文中心在xx中间。

我去这怎么处理啊?

诶,对咯。牛波一的人经常有,\(OI\)特别多。

你也不想一下,如何不改变回文串的回文性质,且使回文中心在字符串上呢?

在原序列中的每两个字符间和开头结尾都填上#字符,这样\(OK\)

特别的,为了后面的操作,使最开头是@ , 结尾是/0 .

有问题的:

1. 如果在你的串中用过新添加的字符怎么办?

sb,用个别的呗。

2.如果键盘上所有的字符都包括在字符串内怎么办?

......\(\bf{难绷}\)


·\(이\) -- 核心

\(O(n)\)肯定只枚举一个值,大概猜一猜也知道是枚举中间の中心\(Centre\)

\(p_i\)存储以\(i\)为中心,\(p_i\)长为半径の半径长。

\(R\)表示目前所到最远处。

在枚举\(i\)时需要考虑:

  1. 如果\(i\)\(R\)左:

其实 \(i\)的回文长度与 \(i\)\(Centre\) 对称到 \(Centre\) 左端的对称点 $ Centre \times 2 - i $ 有着很大关系。

但并不是完全相等的。

因为你\(i\)后面还有在\(R\)后的字符,对称点左端最左也是\(R\)的对称点了。

\(\therefore\) 转移一:

\[p_i = \min( p_{ Centre \times 2 - i , R - i } ) \]

然后暴力跳,最后看看 \(i\) 的右端点和目前的 \(R\) 的关系,如果前者远,\(Centre\) 变为 \(i\)

·$code \ of \ 板子 $

点击查看代码
#include<bits/stdc++.h> 
#define int long long
using namespace std ;
const int N = 2.1*1e7 + 100 ;   
char s[ N ] , so[ 2 * N ] ; int len ; 
int p[ N ] , Centre = -1 , R = -1 ; 
signed main( )
{
    cin >> s ; 
    len = strlen( s ) ; 
    so[ 0 ] = '@' , so[ 1 ] = '#' ; 
    int j = 2 ; 
    for( int i = 0 ; i < len ; ++ i )
    {
        so[ j ++ ] = s[ i ] ; 
        so[ j ++ ] = '#' ; 
    }
    so[ j ] = '\0' ; 
    len = j ; int ans = - 1 ;  
    for( int i = 1 ; i < len ; ++ i )
    {
        if( i < R ) p[ i ] = min( p[ Centre * 2 - i ] , R - i ) ; 
        else p[ i ] = 1 ; 
        while ( so[ i + p[ i ] ] == so[ i - p[ i ] ] ) p[ i ] ++ ; 
        if( i + p[ i ] > R )
        {
            R = i + p[ i ] ; 
            Centre = i ; 
        }
        ans = max( ans , p[ i ] - 1 ) ; 
    }
    cout << ans ; 
}

·小结

马拉车好像不是提高知识,考的也不是很多...

所以, 理解就好 $ [ : - ) ] $


Tire 树


· 赘述

\(OJ\)使用不当害死人啊。。。(关于\(OJ\)上写的是tire树...)

咳咳,进入正题。

·\(First\) -- 建树||插入

\(Trie树\) 一种神奇的数据结构,又名字典树。

可以把它的运算模式看做查字典。

现在有han,mo,hangry几个单词来建字典树。

你发现han&hangry都包含han

\(so\)n 处标一个标记 , hangry在这个基础上继续搜。

树:

(旁边表**意思是标记)

    0 
   / \
  m   h 
  |   |
 *o*  a 
      |
     *n*
      |
      g
      |
      r
      |
     *y*


$ code \ of \ insert $

点击查看代码
struct chain_trie_tree 
{
    int to , next ;
    char price ;
    chain_trie_tree( )
    {
        to = next = price = 0 ; 
    } 
} ;
struct Trie_Tree
{
    chain_trie_tree e[ 40 * N ] ; 
    int head[ 40 * N ] , cnt ;
    void insert( char Checkee[ ] , int Point_at , int Last_Point , int place  ) // 开始于根
    { 
        bool flag = 0 ; 
        for( int i = head[ place ] ; i ; i = e[ i ].next )
        {
            int To_go = e[ i ].to ; 
            int charee = e[ i ].price ; 
            if( charee == Checkee[ Point_at ] )
            {
                flag = 1 ; 
                if( Last_Point == Point_at ) pd[ To_go ] ++ ; 
                else 
                { 
                    insert( Checkee , Point_at + 1 , Last_Point , To_go  ) ; 
                }
                return ; 
            }
        }
        if( flag == 1 ) return ;  
        cnt ++ ;
        e[ cnt ].to = ++ numbol ;
        e[ cnt ].price = Checkee[ Point_at ] ; 
        e[ cnt ].next = head[ place ] ; 
        head[ place ] = cnt ; 
        if( Last_Point == Point_at ) 
        {
            pd[ numbol ] ++ ; 
            return ;
        }
        insert( Checkee , Point_at + 1 , Last_Point , numbol  ) ; 
    }
    Trie_Tree( )
    {
        tail = 0 ; 
        cnt = 0 ; 
        memset( head , 0 , sizeof( head ) ) ; 
    }
    inline void clear( )
    {
        flag = 0 ; 
        cnt = 0 ; 
        memset( head , 0 ,sizeof( head ) ) ; 
    }
}tree ; 

·\(Second\) -- 查询

其实和插入差不多,具体看代码

·\(code\)

点击查看代码
inline void print_word( int tail )
{
    for( int i = 1 ; i <= tail ; ++ i )
    {
        cout << e[ stac[ i ] ].price ; 
    }
    cout << '\n' ; 
}
void find_word( int x )
{
    if( head[ x ] == 0 )
    {
        tail = 0 ; 
        return ; 
    }
    for( int i = head[ x ] ; i ; i = e[ i ].next )
    {
        int y = e[ i ].to ; 
        stac[ ++ tail ] = i ; 
        if( pd[ y ] )
        { 
            print_word( tail ) ; 
        }
        find_word( y ) ; 
    }
}
void checkol_string( int point )
{
    for( int i = head[ point ] ; i ; i = e[ i ].next )
    {
        int y = e[ i ].to ; 
        if( pd[ y ] )
        {
            if( ( !head[ y ] && pd[ y ] > 1 ) || head[ y ] )
            {
                cout << "NO" << '\n' ; 
                flag = 1 ; 
            } 
            return ; 
        }
        checkol_string( y ) ;
        if( flag == 1 ) return ;  
    }
    return ; 
}

·例题

\(Phone \ List\)

here

\(code\)
点击查看代码
#include<bits/stdc++.h> 
#define int  long long 
#define together ios::sync_with_stdio( 0 ) ; cin.tie( 0 ) ; cout.tie( 0 )  
using namespace std ;
const int N = 4e5 + 100 ;
int numbol ; 
int pd[ N * 50 ] ;    
queue<char>q ;
namespace Trie
{    
    struct chain_trie_tree 
    {
        int to , next ;
        char price ;
        chain_trie_tree( )
        {
            to = next = price = 0 ; 
        } 
    } ; 
    struct Trie_Tree
    {
        chain_trie_tree e[ 40 * N ] ; 
        int head[ 40 * N ] , cnt ;
        void insert( char Checkee[ ] , int Point_at , int Last_Point , int place  ) // 开始于根
        { 
            bool flag = 0 ; 
            for( int i = head[ place ] ; i ; i = e[ i ].next )
            {
                int To_go = e[ i ].to ; 
                int charee = e[ i ].price ; 
                if( charee == Checkee[ Point_at ] )
                {
                    flag = 1 ; 
                    if( Last_Point == Point_at ) pd[ To_go ] ++ ; 
                    else 
                    { 
                        insert( Checkee , Point_at + 1 , Last_Point , To_go  ) ; 
                    }
                    return ; 
                }
            }
            if( flag == 1 ) return ;  
            cnt ++ ;
            e[ cnt ].to = ++ numbol ;
            e[ cnt ].price = Checkee[ Point_at ] ; 
            e[ cnt ].next = head[ place ] ; 
            head[ place ] = cnt ; 
            if( Last_Point == Point_at ) 
            {
                pd[ numbol ] ++ ; 
                return ;
            }
            insert( Checkee , Point_at + 1 , Last_Point , numbol  ) ; 
        }
        void BFS_print_tree( )
        { 
            q.push( 0 ) ; 
            while( !q.empty( ) )
            {
                int x = q.front( ) ; q.pop( ) ; 
                for( int i = head[ x ] ; i ; i = e[ i ].next )
                {
                    cout << x << "->" << e[ i ].to << ":" << e[ i ].price << endl ;
                    q.push( e[ i ].to ) ;  
                }
            }
        }
        int stac[ N ] , tail ; 
        inline void print_word( int tail )
        {
            for( int i = 1 ; i <= tail ; ++ i ) //送你个密码:chengxiao
            {
                cout << e[ stac[ i ] ].price ; 
            }
            cout << '\n' ; 
        }
        void find_word( int x )
        {
            if( head[ x ] == 0 )
            {
                tail = 0 ; 
                return ; 
            }
            for( int i = head[ x ] ; i ; i = e[ i ].next )
            {
                int y = e[ i ].to ; 
                stac[ ++ tail ] = i ; 
                if( pd[ y ] )
                { 
                    print_word( tail ) ; 
                }
                find_word( y ) ; 
            }
        }
        int flag = 0 ; 
        void checkol_string( int point )
        {
            for( int i = head[ point ] ; i ; i = e[ i ].next )
            {
                int y = e[ i ].to ; 
                if( pd[ y ] )
                {
                    if( ( !head[ y ] && pd[ y ] > 1 ) || head[ y ] )
                    {
                        cout << "NO" << '\n' ; 
                        flag = 1 ; 
                    } 
                    return ; 
                }
                checkol_string( y ) ;
                if( flag == 1 ) return ;  
            }
            return ; 
        }
        Trie_Tree( )
        {
            tail = 0 ; 
            cnt = 0 ; 
            memset( head , 0 , sizeof( head ) ) ; 
        }
        inline void clear( )
        {
            flag = 0 ; 
            cnt = 0 ; 
            memset( head , 0 ,sizeof( head ) ) ; 
        }
    }tree ;
}
using namespace Trie ; 
int T , n ; 
char s[ N ][ 101 ] ; 
signed main( )
{
    #ifndef ONLINE_JUDGE
        freopen( "cjs.in" , "r" , stdin ) ;
        freopen( "cjs.out" , "w" , stdout ) ;
    #endif
    together ; 
    cin >> T ; 
    while ( T -- )
    {
        tree.clear( ) ; 
        cin >> n ; 
        for ( int i = 1 ; i <= n ; ++ i )
        {
            cin >> s[ i ] + 1 ; 
            tree.insert( s[ i ] , 1 , strlen( s[ i ] + 1 ) , 0 ) ; 
        }
        //tree.BFS_print_tree( ) ; 
        tree.checkol_string( 0 ) ; 
        if( tree.flag == 0 )
        {
            cout << "YES" << '\n' ; 
        }
    }
}

·\(Third\)--\(01Trie\)

异或是基于\(2\)进制的,所以如果将二进制的树扔进\(Trie\)树里,可以完成一些神奇的操作。

·例:

$ The \ XOR \ Largest \ Pair $

here

解:

将所有的数按照\(01\)字符串方式插入字典树。

以每一个\(i\)为主,观察每一层:

如果有与这个相反的边,就从那边走,否则直接下。

·\(code\)
点击查看代码
#include<bits/stdc++.h> 
#define int  long long 
#define together ios::sync_with_stdio( 0 ) ; cin.tie( 0 ) ; cout.tie( 0 )  
using namespace std ;
const int N = 1e7 + 100 ; 
int n , trie[ N * 2 ][ 2 ] ; 
int a[ N ] ; int tot ; 
void insert( int v )
{
    int p = 0 ; 
    for( int i = 31 ; i >= 0 ; -- i )
    {
        int l = ( v >> i ) & 1 ; 
        if( !trie[ p ][ l ] )
        trie[ p ][ l ] = ++ tot ; 
        p = trie[ p ][ l ] ; 
    }
} 
int find( int v )
{
    int p = 0 ; 
    int sum = 0 ; 
    for( int i = 31 ; i >= 0 ; -- i )
    {
        int l = ( v >> i ) & 1 ; 
        if( trie[ p ][ l ^ 1 ] )
        {
            p = trie[ p ][ l ^ 1 ] ; 
            sum += 1 << i  ; 
        }
        else 
        {
            p = trie[ p ][ l ] ; 
        }
    }
    return sum ; 
}
signed main( )
{
    #ifndef ONLINE_JUDGE
        freopen( "qzs.in" , "r" , stdin ) ;
        freopen( "qzs.out" , "w" , stdout ) ;
    #endif
    together ; 
    cin >> n ; 
    for( int i = 1 ; i <= n ; ++ i )
    {
        cin >> a[ i ] ; 
        insert( a[ i ] ) ; 
    } 
    int ans = - 114514 ; 
    for( int i = 1 ; i <= n ; ++ i )
    {
        ans = max( ans , find( a[ i ] ) ) ; 
    }
    cout << ans ; 
}

·\(Fourth\)--可持久化字典树

\(NOI\)知识点,浅说一下。

如果想查修改前的历史版本,就需要这个数据结构。

在新的上面建新的,并演出新根。


·小结

字典树是一个较重要的知识点,虽然理解不难,但一定要明白。


·结尾散花\(\color{pink}✿✿ヽ(°▽°)ノ✿\)

完成不易......

posted @ 2024-02-01 08:42  HANGRY_Sol&Cekas  阅读(67)  评论(4编辑  收藏  举报