Trie树_初章

Trie树

L R J LRJ LRJ白书字典树学习总结

我们常常用 T r i e Trie Trie[也叫前缀树]来保存字符串集合。
从根节点到每个单词结点的路径上所有字母连接而成的字符串就是该结点对应的字符串。
根节点标号为0,其余结点编号为从1开始的正整数。


具体来说,用 c h [ i ] [ j ] ch[i][j] ch[i][j]表示结点 i i i的编号为 j j j的子结点。这里我们把编号从 0 − 25 0-25 025分别对应 26 26 26个字母。
如果需要在单词结点上添加信息,就可以保存在 v a l [ i ] val[i] val[i]中。

int ch[maxnode][sigma_size];
int val[maxnode];

struct Trie{
    int sz;
    Trie(){sz=1;memset(ch[0],0,sizeof(ch[0]));}
    int idx(char c){return c-'a';}
    void insert(char *s,int v){
        int u=0,n=strlen(s);
        for(int i=0;i<n;i++){
            int  c = idx(s[i]);
            if(!ch[u][c]){
                memset(ch[sz],0,sizeof(ch[sz]));
                val[sz]=0;
                ch[u][c]=sz++;
            }
            u=ch[u][c];
        }
        val[u]=v;//字符串的最后一个字符的附加信息为v,表示是单词结点
    }
    bool query(char *s){
        int u=0,n=strlen(s);
        for(int i=0;i<n;i++){
            int c=idx(s[i]);
            if(!ch[u][c]){
                return false;
            }
            else {
                u=ch[u][c];
	//val[u]此时可以处理单词节点的前缀信息
            }
        }
        if(!val[u])return false;
        else return true;
    }
}trie;

还有一种左儿子右兄弟的写法,对应每个点扩展出来的编号,我们采用链表的方式连起来,能够节省很多空间,不过会牺牲一点复杂度。
基本形式一样,但找结点的时候必须 O ( s i g m a − s i z e [ 字 符 集 大 小 , 26 个 字 母 ] ) O(sigma-size[字符集大小,26个字母]) O(sigmasize[26])

const int maxnode = 2000050;//为总字符串长度*2(原因未知)

int he[maxnode];//左儿子编号
int ne[maxnode];//右兄弟编号
char ch[maxnode];//ch[i]为第i个结点上的字符
int tot[maxnode];//每个节点包含的信息

struct Trie{
    int sz;
    Trie(){sz=1;tot[0]=he[0]=ne[0];}//初始只有一个0结点

    void insert(const char *s,int flag){
        int u=0,v,n=strlen(s);
        tot[0]++;
        for(int i=0;i<n;i++){
            bool found=false;//寻找字符,找不到就创建
            for(v=he[u];v!=0;v=ne[v]){
                if(ch[v]==s[i]){
                    found=true;
                    break;
                }
            }
            if(!found){
                v=sz++;
                tot[v]=0;
                ch[v]=s[i];
                ne[v]=he[u];
                he[u]=v;
                he[v]=0;
            }
            u=v;
            tot[u]++;
        }
    }
}tr;

引入例题: U v a 1401 Uva 1401 Uva1401
题意:给出多个单词,给出一个字符串,求字符串能够由单词拼成的组合数。
容易得到: d p [ i . . . n ] = ∑ 符 合 前 缀 的 单 词 s t r d p [ i + l e n ( s t r ) . . . n ] dp[i...n]=\sum_{符合前缀的单词str}{dp[i+len(str)...n]} dp[i...n]=strdp[i+len(str)...n]
只要我们找出 i . . . n i...n i...n的前缀即可。 T r i e Trie Trie是个优秀的做法,对于从 i i i开始的字符串,只要遇到单词结点我们就执行递推式。
初始化 d p [ l e n ] = 1 dp[len]=1 dp[len]=1(就是什么也没有的组合数为1)

_代码弄丢了,有空补

U v a 11732 Uva11732 Uva11732
题意:给出 n n n个字符串,求出所有字符串互相比较字典序需要的比较次数(比到不同的就不比了)。
相同部门比较次数为 2 l e n 2len 2len,不同的为 1 1 1,字符串结尾 &quot; 0 &quot; &quot;0&quot; "0"也需要比较。为什么是 2 l e n 2len 2len目前未知。
首先一共有 4000 4000 4000个。暴力计算会超时( 4000 ∗ 4000 ∗ 1000 4000*4000*1000 400040001000)后面是字符串长度,最大 1000 1000 1000
我们需要注意的是:我们可以每次记录每个节点经过的单词数,这样每次都可以直接加上这里。
为了考虑不重复加的问题,对于 T r i e Trie Trie只有两种情况,完全相同和不同,前者会比较的末尾,所以在末尾我们加上 s u m ∗ ( s u m − 1 ) / 2 ∗ 2 ∗ l e n = s u m ∗ ( s u m − 1 ) ∗ l e n sum*(sum-1)/2*2*len=sum*(sum-1)*len sum(sum1)/22len=sum(sum1)len,对于分叉点 u u u的每个孩子节点 p p p a n s + = ( s u m [ u ] − s u m [ p ] ) ∗ s u m [ p ] / 2 ∗ ( 2 l e n + 1 ) ans+=(sum[u]-sum[p])*sum[p]/2*(2len+1) ans+=(sum[u]sum[p])sum[p]/2(2len+1),显然表示的意思是,对于这个孩子结点,和它不同的比较次数肯定到这一层位置,价值等于前面相同的加上此时不同的 1 1 1, s u m [ u ] sum[u] sum[u]等价于孩子数,除了 s u m [ p ] sum[p] sum[p]的其他都和 p p p不同字符。
有更简便的做法,但是这里主要是省空间做法的模板。[单词长度过长,空间会炸]

#include<bits/stdc++.h>
#define FOR(i,a,b) for(int i=a;i<=b;i++)
#define ll long long
#define ull unsigned long long
using namespace std;

const int maxnode = 4000 * 1000 + 10;

int he[maxnode];//左儿子编号
int ne[maxnode];//右兄弟编号
char ch[maxnode];//ch[i]为第i个结点上的字符
int tot[maxnode];//每个节点包含的叶节点总数(单词数)

struct Trie{
    int sz;
    long long ans;//答案
    Trie(){sz=1;ans=0;tot[0]=he[0]=ne[0];}//初始只有一个0结点

    void insert(const char *s){
        int u=0,v,n=strlen(s);
        tot[0]++;
        for(int i=0;i<=n;i++){
            bool found=false;//寻找字符,找不到就创建
            for(v=he[u];v!=0;v=ne[v]){
                if(ch[v]==s[i]){
                    found=true;
                    break;
                }
            }
            if(!found){
                v=sz++;
                tot[v]=0;
                ch[v]=s[i];
                ne[v]=he[u];
                he[u]=v;
                he[v]=0;
            }
            ans+=(tot[u]-tot[v]-1)*(2*i+1);//cout<<ans<<endl;
            if(i==n){
                ans+=(tot[v])*(2*i+2);
            }
            u=v;
            tot[u]++;
        }
    }
}tr;

int main(){
    int n,kase=0;
    char str[24050];
    while(scanf("%d",&n)){
        if(n==0)break;
        tr=Trie();
        FOR(i,1,n){
            scanf("%s",str);
            tr.insert(str);
        }
        printf("Case %d: %lld\n",++kase,tr.ans);
    }
}

T i r e Tire Tire树在求异或上有很优异的做法,看这里

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值