2017 Multi-University Training Contest - Team 6:String(字典树)

题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=6096

题目意思:

输入第一个T,代表测试数据数组。

输入n,q。n代表将要给出n个字符串,q是q次查询。

q次查询每次输入的是一组前缀和后缀。

针对每一次询问,问n个字符串中,有多少个字符串是以当前询问的前缀开头,后缀结尾。

注意前缀和后缀是不交叉的。

比赛的时候不会写。后来看了好多人的博客。

有字典树写的又有AC自动机写的,AC自动机写的代码简直太长了,看不明白。我AC自动机才

刚入门,看不懂,太弱了。

我就寻找用字典树写的。用字典树的有好多人都是那样写的。处理n个字符串的时候,建立字典树,

按照前面的字符,后面的字符这样交替存储,但是这样的话建立字典树的时候需要建立很多

节点,我看的有些代码,压根提交就没过,用这种方法,选择动态链表建立字典树的时候

由于每次都要动态分配空间,最后提交超时了,选择静态链表建立字典树,很不幸超内存了。

后来我在别人博客看到了也是字典树的方法,但是比上面的好理解多了,个人认为。而且要

建的节点数也少得多,我用动态链表建立字典树就AC了。

思路来源:http://blog.csdn.net/hnust_derker/article/details/77088157

其思路是这样的,建立字典树不在根据n个字符串来建立树,而是根据Q次输入的前缀和后缀的组合

来建立字典树。例如:对于cd ef这个询问:我们转换成这样:cd#fe。即前缀不变,前缀后面加上不是小写字母

的字符,这个字符代表通配符,就像数据库中我们可以用的通配符一样。#表示可以匹配任意字符。当然在代码

中我们并不会用#去作为通配符,而是用'a'+26这个字符‘{’来代表通配符,这样的话,每个字典树节点的儿子数组

开27就够了,刚好可以存储'a'+26。用'#'我们就要有于'#'-‘a'对应的位置去存储他,这样很浪费空间。对于cd ef这

样的询问,我们把它转换成 cd{fe 插入到字典树。

字典树的节点里面有两个成员,一个就是son[27] 分别用来映射字符a~z,和通配符’{‘,'{'在ASCII表中的位置

'z'之后。另一个成员就是num,当插入完成cd{fe后,最后一个节点的num我们赋值称为当前查询的编号。意识

是这个节点所代表的前缀后缀的组合是对应于第i个询问的。接下来我们就要看这n个字符串对字典树中的前后缀

贡献了。

假如我们当前的n个字符串里面有cdabef这个字符串,当在字典树中查询的时候,我们会查询到c,然后看c后面有没有通配符,发现没有,继续往后查询cd,发现cd后来有通配符’{‘这个时候,我们说明前缀已经匹配了。然后我们需要倒着来匹配所查询的字符串看看后缀存不存在。也就是看feba来匹配字典树上的后缀。可以知道能够匹配上fe.那么那么我们要把所有匹配上的后缀它所代表的查询的位置都加上1。

总体思路就是,每次扩展前缀,发现通配符,我们去倒着匹配后缀。循环此过程。

        想不明白的话,看看这个数据:字典树中有cd{fe 和 cda{fe,然后当前给出的cdabef

        处理到cd的时候,d后面发现有通配符,然后去匹配后缀。匹配完后,我们扩展前缀,即

考虑cda发现a后面有通配符,然后去匹配后缀。这样cdabef对cd ef 和 cda ef这两个查询都贡献了一次。

有一点非常坑,题目给出的Q次查询中会有重复的出现,对于两个给出的完全相同的前缀和后缀的组合

它在插入字典树最后所到达的节点P必定是相同的。如果p->num发现不是0,说明之前已经有一个前缀后缀

的组合到达这个位置了,现在再次到达这个位置,p->num只统计最早到达p节点的前后缀组合就行,后到达

的和之前到达的一样,当然最后输出的时候答案也是一样的。我们可以用一个id[]数组,原来第i次询问的

id[i] = i就行了,如果第i个询问和第j个询问是相同的,他们最终答案一样,我们让id[j] = i;就可以了。

这样我们最后输出ans[id[i]]就是答案了。


具体详情看代码:

动态链表写法

AC代码:

#include <iostream>
#include <stdio.h>
#include <malloc.h>
#include <string.h>

const int maxn = 27;
const int maxstr = 1e7;
char str[maxstr],s1[maxstr],s2[maxstr];
int id[maxstr];     
int len[maxstr];    ///len[i]代表第i个字符串的长度。
int ans[maxstr];    ///ans[i]是第i个最后输出的满足第i个询问的字符串数量
typedef struct TrieNode ///字典树节点
{
    int num;            ///该节点所代表的查询的编号。
    struct TrieNode *son[maxn];  ///26个字母加上一个通配符。
}Trie;
Trie* createNode()  ///创建节点的函数
{
    Trie *node;
    node = (Trie*)malloc(sizeof(Trie));
    for(int i = 0; i < maxn; i++)
        node->son[i] = NULL;
    node->num = 0;
    return node;
}
void insertWord(Trie *root,int index)  ///将第index个询问插入字典树。
{
    Trie *p;    ///辅助指针
    p = root;
    int i = 0;
    while(s1[i] != '\0')
    {
        int lowercase = s1[i]-'a';   ///求小写字母对应的整数
        if(p->son[lowercase]==NULL)
        {
            p->son[lowercase] = createNode();
        }
        p = p->son[lowercase];
        i++;
    }
    if(p->num == 0)  ///还没有询问到达这里
        p->num = index;
    else id[index] = p->num;  ///遇到相同的数据只存最早出现的那个查询的编号。
}
void query(Trie *root,int strbegin,int strend)
{
    Trie *p,*tmp;
    p = root;
    int i = strbegin;
    while(p!=NULL && i<=strend)
    {
        int lowercase = str[i]-'a';
        if(p->son[lowercase] == NULL) return;  ///前缀匹配的时候就失配了
        p = p->son[lowercase];
        if(p->son[26]!=NULL)  ///发现通配符,考虑当前前缀。然后匹配后缀
        {
            tmp = p->son[26];    ///现在tmp是通配符,我们继续往后匹配后缀
            for(int j=strend; j > i; j--)
            {
                lowercase = str[j]-'a';
                if(tmp->son[lowercase]==NULL)  ///失配边跳出
                    break;
                tmp = tmp->son[lowercase];
                if(tmp->num)
                    ans[tmp->num]++;
            }
        }
        i++;   ///扩展前缀,然后继续找通配符,去匹配后缀
    }
}
void free_Trie(Trie *root)  ///释放内存,没用了就换给系统,不然内存会爆的。
{
    if(root != NULL)
    {
        for(int i = 0; i < maxn; i++)
            if(root->son[i]!=NULL)
                free_Trie(root->son[i]);
    }
    free(root);
}
int main()
{
    int t;
    scanf("%d",&t);   ///t组测试数据
    while(t--)
    {
        int n,q;      ///n个字符,q次查询
        scanf("%d%d",&n,&q);
        Trie *root;
        root = createNode();
        int prelen = 0;            ///当前串长
        for(int i = 0; i < n; i++) ///这里妙啊,不明白的可以str输出,肯定一看就秒懂。
        {
            scanf("%s",str+prelen);
            len[i] = strlen(str+prelen);
            prelen += len[i];
        }
        for(int i = 1; i <= q; i++)
        {
            id[i] = i;
            ans[i] = 0;
        }
        for(int i = 1; i <= q; i++)
        {   ///s1是前缀,s2是后缀,把s2反转,然后组成:前缀+通配符+后缀的反转。然后插入字典树。
            scanf("%s",s1);
            int len1 = strlen(s1);
            s1[len1++] = 'a'+26;  ///'{'
            scanf("%s",s2);
            int len2 = strlen(s2);
            for(int j = len2-1; j >= 0; j--)
            {
                s1[len1++] = s2[j];
            }
            s1[len1] = '\0';
            insertWord(root,i);
        }
        prelen = 0;
        for(int i = 0; i < n; i++)
        {
            query(root,prelen,prelen+len[i]-1);
            prelen += len[i];
        }
        for(int i = 1; i <= q; i++)
            printf("%d\n",ans[id[i]]);
        free_Trie(root);
    }
    return 0;
}


静态数组写法:

#include <iostream>
#include <stdio.h>
#include <string.h>

using namespace std;

typedef long long LL;
const int maxn = 6e5+10;
using namespace std;

int node[maxn][27];                 ///node[i]是节点。二维开27是因为还需要一个通配符。
char str[maxn],s1[maxn],s2[maxn];
int len[maxn];                      ///len[i]存放第i个字符串的长度。
int ans[maxn];                      ///ans[i]用来存放以第i个查询为前后缀的字符串有多少个。
int num[maxn];
int id;                             ///节点编号
int nex[maxn];

void insertWord(int i)              
{
    int p = 0;
    int index=0;
    while(s1[index]!='\0')
    {
        int lowercase = s1[index]-'a';
        if(node[p][lowercase]==0)   ///这个字母不存在
        {
            memset(node[id],0,sizeof(node[id]));
            num[id] = 0;
            node[p][lowercase] = id++;    ///存放节点的编号
        }
        p = node[p][lowercase];  ///P指向子节点
        index++;
    }
    if(num[p]==0)
    {
        num[p] = i;
    }
    else
    {
        nex[i] = num[p]; 
    }
}
///传入要查询字符串在str数组中下标的起点和终点。
void query(int strbegin,int strend)
{
    int p = 0;  ///根节点编号永远都是0
    for(int i = strbegin; i <= strend; i++)
    {
        int lowercase = str[i]-'a';
        if(node[p][lowercase]==0) return;   ///前缀都匹配不上的直接pass.
        p = node[p][lowercase];
        if(node[p][26]==0)  continue;      ///发现当前节点所代表的前缀后面没有统配符。
        int tmp = node[p][26];  	   ///tmp为通配符。开始倒着匹配后缀
        for(int j = strend; j > i; j--)
        {
            lowercase = str[j]-'a';
            tmp = node[tmp][lowercase];
            if(tmp==0)  ///失配
                break;
            if(num[tmp]) ans[num[tmp]]++;
        }
    }
}
int main()
{
    int t;  ///测试数据组数
    scanf("%d",&t);
    while(t--)
    {
        id = 1;  ///节点编号从1开始。
        memset(node[0],0,sizeof(node[0]));   ///把根节点的孩子节点置为空
        memset(ans,0,sizeof(ans));
        num[0] = 0;
        int n,q;
        scanf("%d%d",&n,&q);                 ///n个字符串,q次查询。
        for(int i = 1; i <= q; i++)
            nex[i] = i;
        int number = 0;
        ///每次把当前输入的字符串链接到之前输入的字符串后面。
        for(int i = 0; i < n; i++)
        {
            scanf("%s",str+number);
            len[i] = strlen(str+number);
            number += len[i];
        }
        for(int i = 1; i <= q; i++)
        {
            scanf("%s",s1);
            int len1 = strlen(s1);
            s1[len1++] = 'a'+26;      ///放入一个通配符。
            scanf("%s",s2);           ///将s2反转放入s1后面
            int len2 = strlen(s2);
            for(int j = len2-1; j >= 0; j--)
            {
                s1[len1++] = s2[j];
            }
            s1[len1] = '\0';
            insertWord(i);   ///现在插入字典树的是第i个前缀和后缀组合
        }
        number = 0;
        for(int i = 0; i < n; i++)  ///查询n个字符串
        {
            ///第i个字符串的长度为len[i].
            query(number,number+len[i]-1);
            number += len[i];
        }
        for(int i = 1; i <= q; i++)
            printf("%d\n",ans[nex[i]]);
    }
    return 0;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值