zoukankan      html  css  js  c++  java
  • Luogu P2292 [HNOI2004]L语言(Trie+dp)

    P2292 [HNOI2004]L语言

    题面

    题目描述

    标点符号的出现晚于文字的出现,所以以前的语言都是没有标点的。现在你要处理的就是一段没有标点的文章。

    一段文章 (T) 是由若干小写字母构成。一个单词 (W) 也是由若干小写字母构成。一个字典 (D) 是若干个单词的集合。我们称一段文章 (T) 在某个字典 (D) 下是可以被理解的,是指如果文章 (T) 可以被分成若干部分,且每一个部分都是字典 (D) 中的单词。

    例如字典 (D) 中包括单词 ({‘is’, ‘name’, ‘what’, ‘your’}) ,则文章 (‘whatisyourname’) 是在字典 (D) 下可以被理解的,因为它可以分成 (4) 个单词: (‘what’, ‘is’, ‘your’, ‘name’) ,且每个单词都属于字典 (D) ,而文章 (‘whatisyouname’) 在字典 (D) 下不能被理解,但可以在字典 $D’=D+ { ‘you’ } $ 下被理解。这段文章的一个前缀 (‘whatis’) ,也可以在字典 (D) 下被理解,而且是在字典 (D) 下能够被理解的最长的前缀。

    给定一个字典 (D) ,你的程序需要判断若干段文章在字典 (D) 下是否能够被理解。并给出其在字典 (D) 下能够被理解的最长前缀的位置。

    输入输出格式

    输入格式:

    输入文件第一行是两个正整数 (n)(m) ,表示字典 (D) 中有 (n) 个单词,且有 (m) 段文章需要被处理。之后的 (n) 行每行描述一个单词,再之后的 (m) 行每行描述一段文章。

    其中 (1 leq n, m leq 20) ,每个单词长度不超过 (10) ,每段文章长度不超过 (1M)

    输出格式:

    对于输入的每一段文章,你需要输出这段文章在字典 (D) 可以被理解的最长前缀的位置。

    输入输出样例

    输入样例:

    4 3 
    is
    name
    what
    your
    whatisyourname
    whatisyouname
    whaisyourname
    

    输出样例:

    14
    6
    0
    

    思路

    今天的任务是复习 (Trie) 树和 (AC) 自动机! --Uranus
    (30 mins later)
    艹 ---Uranus

    今天复习字符串算法,然后就随到了这道字符串毒瘤题。调试了很久,终于过了。

    首先拿到这题,显然要先建立一棵 (Trie) 或者造一台 (AC) 自动机 (量词和动词都没有错呢) ,然而我太蒻了忘了怎么跳 (AC) 自动机的 (fail) 指针,于是就写的 (Trie)

    接下来如何去匹配呢?我首先想到的是深度优先搜索。对于一段文章,每次查询到到一个前缀存在于 (Trie) 中,就选择从根节点重新来找单词,还是继续找下去。这样就可以很容易写出深搜代码:

    int dfs(int now)//now表示当前查找的开始位置
    {
        int p=0,re=0;//re为返回值,p为Trie的节点编号,root=0
        for(register int i=now;i<tot_len;i++)//tot_len为文章的长度
        {
            int id=str[i]-'a';
            if(!nex[p][id]) return re;//不存在这个单词
            p=nex[p][id];
            if(len[p]) re=max(re,len[p]+dfs(i+1));//len!=0时表示当前单词的长度(Trie中该节点深度),考虑重新搜索
        }
        return re;//溜了溜了
    }
    

    然后就有了 (74) 分:

    #include<bits/stdc++.h>
    using namespace std;
    const int MAXN=210;
    int n,m,cnt,tot_len,nex[MAXN][26],len[MAXN];
    string str;
    inline void add()
    {
        int p=0;
        for(register int i=0;i<tot_len;i++)
        {
            int id=str[i]-'a';
            if(!nex[p][id]) nex[p][id]=++cnt;
            p=nex[p][id];
        }
        len[p]=tot_len;
    }
    int dfs(int now)
    {
        int p=0,re=0;
        for(register int i=now;i<tot_len;i++)
        {
            int id=str[i]-'a';
            if(!nex[p][id]) return re;
            p=nex[p][id];
            if(len[p]) re=max(re,len[p]+dfs(i+1));
        }
        return re;
    }
    int main()
    {
        cin>>n>>m;
        while(n--)
        {
            cin>>str;
            tot_len=str.length();
            add();
        }
        while(m--)
        {
            cin>>str;
            tot_len=str.length();
            cout<<dfs(0)<<endl;
        }
        return 0;
    }
    

    在一番玄学优化无用之后开始思考使用 (dp) 来优化搜索。首先写一个 (bool) 函数 (fd(int l,int r)) 表示文章的 ([l,r]) 区间所形成的单词是否存在于 (Trie) 中,然后定义 (bool) 变量 (f[i]) 表示 文章中的 ([1,i]) 区间是否可以为最长前缀。那么有

    [f[i]= { k | (f[k]=true or k=-1) and fd(k+1,i)=true } ]

    于是我们就有了 (24) 分代码:

    #include<bits/stdc++.h>
    using namespace std;
    const int MAXN=210;
    int n,m,cnt,len,ans,nex[MAXN][26];
    bool en[MAXN],f[1100000];
    string str;
    inline void add()
    {
        int p=0;
        for(register int i=0;i<len;i++)
        {
            int id=str[i]-'a';
            if(!nex[p][id]) nex[p][id]=++cnt;
            p=nex[p][id];
        }
        en[p]=true;
    }
    inline bool fd(int l,int r)
    {
        int p=0;
        for(register int i=l;i<=r;i++)
        {
            int id=str[i]-'a';
            if(!nex[p][id]) return false;
            p=nex[p][id];
        }
        return en[p];
    }
    int main()
    {
        ios::sync_with_stdio(false);
        cin.tie();
        cout.tie();
        cin>>n>>m;
        while(n--)
        {
            cin>>str;
            len=str.length();
            add();
        }
        while(m--)
        {
            cin>>str;
            len=str.length(),ans=0;
            for(register int i=0;i<len;i++)
            {
                f[i]=false;
                for(register int j=max(i-len,-1);j<i;j++)
                    if((j==-1||f[j])&&fd(j+1,i))
                    {f[i]=true;ans=i+1;break;}
            }
            cout<<ans<<endl;
        }
        return 0;
    }
    
    

    (24) 分?没错,改用了动态规划之后时间反而变慢了,这是因为填表法的时间复杂度太高了。改用刷表法,利用前面的状态,就可以大大降低时间复杂度。

    AC代码:

    #include<bits/stdc++.h>
    using namespace std;
    const int MAXN=210;
    int n,m,cnt,len,ans,nex[MAXN][26];
    bool en[MAXN],f[1100000];
    string str;
    inline void add()
    {
        int p=0;
        for(register int i=0;i<len;i++)
        {
            int id=str[i]-'a';
            if(!nex[p][id]) nex[p][id]=++cnt;
            p=nex[p][id];
        }
        en[p]=true;
    }
    int main()
    {
        ios::sync_with_stdio(false);
        cin.tie();
        cout.tie();
        cin>>n>>m;
        while(n--)
        {
            cin>>str;
            len=str.length();
            add();
        }
        while(m--)
        {
            cin>>str;
            memset(f,0,sizeof f);
            len=str.length(),ans=0;
            for(register int i=-1;i<len;i++)
                if(i==-1||f[i])
                {
                    int j=i+1,id=str[j]-'a',p=nex[0][id];
                    while(p&&j<len)
                    {
                        if(en[p]) f[j]=true;
                        id=str[++j]-'a',p=nex[p][id];
                    }
                }
            for(register int i=len-1;i>=0;i--) if(f[i]) {ans=i+1;break;}
            cout<<ans<<endl;
        }
        return 0;
    }
    
    

    总结

    用了三种算法,过了一种。那要是在 (NOIP) 赛场上,写挂了之后又不知道自己挂了该怎么办呢?所以分析时间复杂度就很重要了。

    插入操作的时间比较少,我们就不考虑在时间复杂度之中了。首先我们定义一个单词的长度为 (|P|) ,一段文章的长度为 (|S|)

    第一种解法:

    最坏情况下,每次 (dfs) 都考虑是否重新 (dfs) ,那么查询复杂度为 (O(|S|*|P|!)) ,总的时间复杂度就是 (O(m|S|*|P|!))

    第二种解法:

    一次 (fd) 函数的调用的时间复杂度为 (O(|S|)) ,动态规划的两层循环是 (O(|S|^2)) 的,所以总的时间复杂度就是 (O(m|S|^3)) 。这样写的话时间复杂度就与 (|P|) 无关而与 (|S|) 关系太大了,而 (|S|>>|P|) ,自然得分很低。

    第三种解法:

    一次刷表的时间为 (|P|) , 总共循环 (|S|) 次,所以总的时间复杂度为 (O(m|S||P|)) 了,显然能过。

  • 相关阅读:
    Photoshop CC 与前端那些事
    gulp入門指南
    谈谈css左右等高的几个方法
    angularjs给Model添加拦截过滤器,路由增加限制,实现用户登录状态判断
    前端自动化工具
    React Router
    Sublime Text 3 常用插件安装
    c++ primer学习指导(13)--1.6书店程序
    c++ primer学习指导(12)--1.5.2初识成员函数
    c++ primer学习指导(11)--1.5.1Sales_item类
  • 原文地址:https://www.cnblogs.com/coder-Uranus/p/9729785.html
Copyright © 2011-2022 走看看