EOJ 3261 分词 dp+字典树

题目:

http://acm.ecnu.edu.cn/problem/3261/

题意:

有一句句子因为粘贴的时候出现了一点问题空格全部丢失了。现在给一本字典,每个词都对应这个词出现的频率(每十亿)。根据这个频率,我们可以根据下面的公式算出这个词带来的收益 P(word):

P(word)=len2(word)⋅ln(frequency(word))
其中 frequency 就是上面所提到的频率。len 指的是单词的长度。

特别的,对于字典中没有出现过的词,P(word)=0。

请对句子进行适当的分割,使得分割得到的所有词收益之和最大。同一个词可以重复出现,收益算作多次。

Input
先给出一本词典,词典的第一行是词条数(词条数约为 40 000),下面每行分别是单词和对应出现频率,用空格隔开。单词中只会出现英文字母大小写,不会有多余的空格。每个单词只会出现一次。频率是一个正实数。

所有单词长度之和不超过 3⋅105,最长单词长度不超过 30。

接下来一行一个整数 T (T≤10),表示有 T 个查询。

下面 T 行,每行一个句子,句子长度不超过 5 000。句子中保证只含有英文字母大小写。注意单词匹配时,不区分大小写。

词典数据来源于 Wikipedia Project Gutenberg(可能需要代理),其中 1-10000 词汇。

查询数据来源于 IELTS Test。

Output
对于每组数据,输出两行。

第一行是一个实数,表示最大能达到的收益。输出和答案相差不超过 10−3 即可认为正确。

第二行输出一连串单词,单词和单词之间用空格隔开。满足:

把这些单词依次串联起来可以得到原句子;
所有单词的收益值相加得到第一行的实数。

思路:

首先把每个单词与其价值关联起来便于查询,用字典树或者STL的map皆可,担心map超时,用了字典树。接下来便是如何去划分单词,定义 dp[i] 为以句子中第i个字母结尾时能得到的最大价值,可以得到状态转移方程: dp[i]=max(dp[i],dp[j]+val) ,其中 0<=j<i ,表示取以第 j 个字母为结尾得到的价值加上从j+1 i 构成的新单词的价值中的最大值。题意又说单词长度不超过30,所以j max(i30,0) 即可。另外怎么记录句子从哪里划分呢?我们可以记录每个位置由之前的某个位置更新而来,即记录每个点的前驱,前驱就是句子划分处

#include <bits/stdc++.h>
using namespace std;

const int N = 5000 + 10;

struct trie
{
    double val;
    trie *next[26];
    trie()
    {
        val = 0.0;
        memset(next, 0, sizeof next);
    }
}*root;
char str[N], tmp[N];
double dp[N];
int pre[N];
bool vis[N];
void trie_insert(char *s, double val)
{
    trie *p = root;
    for(int i = 0; s[i]; i++)
    {
        int j = s[i] - 'a';
        if(p->next[j] == NULL) p->next[j] = new trie;
        p = p->next[j];
    }
    p->val = val;
}
double trie_query(int l, int r, char *s)
{
    if(r - l + 1 > 30) return 0.0;
    trie *p = root;
    for(int i = l; i <= r; i++)
    {
        int j = s[i] - 'a';
        if(p->next[j] == NULL) return 0.0;
        p = p->next[j];
    }
    return p->val;
}
void trie_del(trie *p)
{
    for(int i = 0; i < 26; i++)
        if(p->next[i] != NULL) trie_del(p->next[i]);
    delete p;
}
void work(char *str)
{
    int len = strlen(str + 1);
    for(int i = 1; i <= len; i++)
        tmp[i] = str[i], str[i] = tolower(str[i]);
    for(int i = 0; i <= len; i++)
        dp[i] = -1e8, pre[i] = -1, vis[i] = false;
    dp[0] = 0.0;
    for(int i = 1; i <= len; i++)
    {
        for(int j = max(i - 30, 0); j < i; j++)
        {
            double val = trie_query(j+1, i, str);
            if(dp[i] < dp[j] + val)
            {
                dp[i] = dp[j] + val;
                pre[i] = j;
            }
        }
    }
    printf("%.6f\n", dp[len]);
    int id = len;
    while(id != -1) vis[pre[id]] = true, id = pre[id];//从末尾开始寻找前驱并记录
    for(int i = 1; i <= len; i++)
    {
        printf("%c", tmp[i]);
        if(vis[i]) printf(" ");
    }
    printf("\n");
}
int main()
{
    int n, m;
    while(~ scanf("%d", &n))
    {
        root = new trie;
        char s[50];
        double val;
        for(int i = 1; i <= n; i++)
        {
            scanf("%s%lf", s, &val);
            int len = strlen(s);
            for(int j = 0; j < len; j++) s[j] = tolower(s[j]);
            trie_insert(s, len * len * log(val));
        }
        scanf("%d", &m);
        while(m--)
        {
            scanf("%s", str+1);
            work(str);
        }
        trie_del(root);
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值