题目地址:http://acm.hdu.edu.cn/showproblem.php?pid=1277

题目:


                                                              全文检索

Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others)
Total Submission(s): 2500    Accepted Submission(s): 839

Problem Description

我们大家经常用google检索信息,但是检索信息的程序是很困难编写的;现在请你编写一个简单的全文检索程序。
问题的描述是这样的:给定一个信息流文件,信息完全有数字组成,数字个数不超过60000个,但也不少于60个;再给定一个关键字集合,其中关键字个数不超过10000个,每个关键字的信息数字不超过60个,但也不少于5个;两个不同的关键字的前4个数字是不相同的;由于流文件太长,已经把它分成多行;请你编写一个程序检索出有那些关键字在文件中出现过。

Input

第一行是两个整数M,N;M表示数字信息的行数,N表示关键字的个数;接着是M行信息数字,然后是一个空行;再接着是N行关键字;每个关键字的形式是:[Key No. 1] 84336606737854833158。

Output

输出只有一行,如果检索到有关键字出现,则依次输出,但不能重复,中间有空格,形式如:Found key: [Key No. 9] [Key No. 5];如果没找到,则输出形如:No key can be found !。

Sample Input

20 10 
646371829920732613433350295911348731863560763634906583816269
637943246892596447991938395877747771811648872332524287543417
420073458038799863383943942530626367011418831418830378814827
679789991249141417051280978492595526784382732523080941390128 
。。。。。。。。。。。。
000361739177065479939154438487026200359760114591903421347697 

[Key No. 1] 934134543994403697353070375063
[Key No. 2] 261985859328131064098820791211 
[Key No. 3] 306654944587896551585198958148 
[Key No. 4] 338705582224622197932744664740 
[Key No. 5] 619212279227080486085232196545 
[Key No. 6] 333721611669515948347341113196 
[Key No. 7] 558413268297940936497001402385 
[Key No. 8] 212078302886403292548019629313 
[Key No. 9] 877747771811648872332524287543 
[Key No. 10] 488616113330539801137218227609

Sample Output

Found key: [Key No. 9] [Key No. 5]

解题思路:


ac自动机法:

ac自动机模版上稍微做一点修改,存关键字对应的NO编号(初始化编号为0),其他和模版基本上都是一样的。

但是疯狂提交疯狂RE,MLE,TLE,后来看网上的代码发现是match函数写崩了,对于每个遍历到的节点都要去搜索它现在的字符串的后缀是不关键字,是的话存入结果队列中,否则一直找下去。(´・Д・)」

字典树法:

遍历长字符串,每次都取后半截字符串,再在字典树上遍历,如果找到了一个未出现的关键字,存入记录结果的容器中,停止在字典树上的遍历。


感觉我讲的不是太清楚,举个简单的例子吧:

长字符串:

i 0 1 2 3 4 5 6 7 8 9 10 11
s[i] 1 2 3 4 5 6 2 3 4 7 9 2

关键字:No.1 : 2345623        No.2 :23479      No.3: 34562     No.45678

ac自动机匹配过程:

  1. 遍历长字符串,当遍历到i=6的位置时,s[6]=2,从字典树中可以看出虽然最左侧下面的2不是单词节点,但是它指向的fail对应的2是单词节点,将fail的2对应的No编号存入结果数组中
  2. 再接着从原来的2开始在字典树上往下遍历,紧接着遍历到s[7]=3,是单词节点,No编号存入结果数组
  3. 再继续往下遍历s[10]=9,是单词节点,No编号存入结果数组中

所以最终输出的结果是Found key: [Key No. 3] [Key No. 1] [Key No. 2]

字典树匹配过程:

  1. i=0,截取[0,11]的字符串ss,遍历ss,在字典树上并没有找到从s[0]=1开始的单词
  2. i=1,截取[1,11]的字符串ss,遍历ss,在字典上找到了以s[1]=2开头的单词2345623,将该单词对应的No编号存入结果数组,结束遍历
  3. i=2,同理找到了以s[2]=3开头的单词34562,将该单词对应的No编号存入结果数组,结束遍历
  4. 。。。i=6,找到了以s[6]=2开头的单词23479,将该单词对应的No编号存入结果数组,结束遍历

所以最终输出的结果是Found key: [Key No. 1] [Key No. 3] [Key No. 2]

这道题之所以第二种方法也是正确的,因为给出的关键字都不相同,且没有34562,345623这种情况的关键字,否则用第二种方法的话在搜索以3开头的单词时,当搜索到34562的时候就结束遍历了,无法搜索到345623,这样结果就是错误的了。

但是hdu的这道题这两种方法提交都是ac。。。emmm应该是数据太弱了吧。还是以第一种方法为准吧。

还有,虽然输出只有一行,但是要换行,否则PE(;´༎ຶД༎ຶ`)

ac代码:


ac自动机

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<queue>
#include<map>
#include<vector>
#include<set>
#include<string>
#include<cmath>
#include<cstring>
const int maxn=6e4+6;
const int maxt=600005;
using namespace std;
struct node{
    int next[10];
    int fail,no;
}state[maxt];
queue<int> q;//存字母的编号
queue<int> res;
int size=1,no=0;
string s,tots="",ss;
void init()
{
    while(!q.empty()) q.pop();
    for(int i=0;i<maxt;i++)
    {
        memset(state[i].next,0,sizeof(state[i].next));
        state[i].fail=state[i].no=0;
    }
    size=1;
}

void insert(char *s,int no)
{
    int len=strlen(s),now=0;
    for(int i=0;i<len;i++)
    {
        int id=s[i]-'0';
        if(!state[now].next[id])
        {
            state[now].next[id] = size++;
            //cout<<state[now].next[id]<<" 对应的数字:"<<id<<endl;
        }

        now=state[now].next[id];
    }
    state[now].no=no;
}
void build()
{
    state[0].fail=-1;//根节点
    q.push(0);
    while(!q.empty())
    {
        int u=q.front();q.pop();
        for(int i=0;i<10;i++)
        {
            int now=state[u].next[i];//当前节点的编号
            if(now)//有这个字母节点
            {
                if(u==0) state[now].fail=0;//第一层都指向根节点
                else
                {
                    int v=state[u].fail;//父节点的fail
                    while(v!=-1)
                    {
                        if(state[v].next[i])
                        {
                            state[now].fail=state[v].next[i];//指向父节点下面和当前节点字母相同的节点
                            break;
                        }
                        v=state[v].fail;//一直找,父节点fail位置的字母和父节点的字母相同
                    }
                    if(v==-1) state[now].fail=0;//没找到指向根
                }
                q.push(now);
            }
        }
    }
}

void match(string s)
{
    int len=s.length(),now=0;
    for(int i=0;i<len;i++)
    {
        int id=s[i]-'0';
        if(state[now].next[id]) now=state[now].next[id];//有当前节点继续往下找
        else//失配
        {
            int p=state[now].fail;
            while(p!=-1 && state[p].next[id]==0) p=state[p].fail;
            if(p==-1) now=0;//没找到,回到根,从根开始找s中的下一个字母
            else now=state[p].next[id];
        }
        int u=now;
        while(u)
        {
            if(state[u].no)
            {
                res.push(state[u].no);
                state[u].no=0;
            }
            u=state[u].fail;
        }
    }
}
int main()
{
    //freopen("/Users/zhangkanqi/Desktop/11.txt","r",stdin);
    int n,m;
    char ws[70];
    init();
    scanf("%d %d",&m,&n);
    getline(cin,ss);//读取m,n之后的换行符
    while(m--)
    {
        getline(cin,ss);
        tots+=ss;
    }
    getline(cin,ss);
    while(n--)
    {
        getline(cin,s);
        sscanf(s.c_str(),"[Key No. %d] %s",&no,ws);
        //cout<<no<<" "<<ws<<endl;
        insert(ws,no);
    }
    build();
    match(tots);
    if(res.empty()) printf("No key can be found !\n");
    else
    {
        printf("Found key:");
        while(!res.empty())
        {
            int u=res.front();res.pop();
            printf(" [Key No. %d]",u);
        }
        printf("\n");
    }
    return 0;
}

字典树:参考博客https://blog.csdn.net/GodJing007/article/details/80993149 

#include <iostream>
#include <cstring>
#include <string>
#include <cstdio>
#include <algorithm>
#include <cstdlib>
#include <ctype.h>
#include <cmath>
#include <stack>
#include <queue>
#include <sstream>
#include <set>
#include <map>
#include <vector>
#include <limits.h>
using namespace std;
typedef long long ll;
const int N = 600000 + 10;
int tree[N][10];
int num[N];
string arr[60100];
string big = "";
int pos = 1, n, m, co = 1;
set<int > s;//记录是否该单词编号是否已经被匹配过,相当于一个vis数组
vector<int> ss;//记录结果
void insert(string a){
    int root = 0;
    for(int i = 0; i < a.length(); i++){
        int n = a[i] - '0';
        if(!tree[root][n]){
            tree[root][n] = pos++;
        }
        root = tree[root][n];
    }
    num[root] = co++;//存编号
}
void find(string a){
    int root = 0;
    for(int i = 0; i < a.length(); i++){
        int n = a[i] - '0';
        if(!tree[root][n]){
            return ;
        }
        root = tree[root][n];
        if(num[root]){//是单词节点
            if(!s.count(num[root])){//s中没有存入该节点
                ss.push_back(num[root]);
                s.insert(num[root]);
            }
            return ;
        }
    }
}
void finds(string a){
    for(int i = 0; i < a.length(); i++){
        string b = a.substr(i,a.length());
        find(b);
    }
}
int main(){
    //freopen("/Users/zhangkanqi/Desktop/11.txt","r",stdin);
    cin >> n >> m;
    for(int i = 0; i < n; i++){
        cin >> arr[i];
        big += arr[i];
    }
    for(int i = 0; i < m; i++){
        string a, b;
        cin >> b >> b >> b >> a;
        insert(a);
    }
    finds(big);
    if(!ss.empty()){
        cout << "Found key:";
        for(vector<int>::iterator it = ss.begin(); it != ss.end(); it++){
            printf(" [Key No. %d]", *it);
        }
        cout << endl;
    }else{
        cout << "No key can be found !" << endl;
    }
    return 0;
}