Loading

[20052006-ptz] Decoding Martian Messages

前言

有点神奇的一个题, 不太会做

题面

输入文件:\(\textrm{decoding.in}\)
输出文件:\(\textrm{decoding.out}\)
时间限制:\(1\)
内存限制:\(64\) 兆字节

在有噪声的信道上传输信息一直是一个难题。各种方法被用来检测传输错误并修复它们。彼得在一家开发用于火星探测模块通信系统的编程环境的公司工作。

通信中使用的消息可以被视为英文字母的序列。为了帮助检测和修复错误,所有传输的消息都满足以下条件:消息中每 $ k $ 个连续字母形成一个属于给定单词集合 $ D $ 的单词。

例如,如果 $ k = 2 $ 且 \(D = \{ "ab", "ba", "aa" \}\),则单词 "abaab" 可以传输,但单词 "abbab" 不能。

当消息接收时,由于信号中添加了随机噪声,很难单独解码每个字符。对于消息的每个位置 $ i $ 和每个字符 $ c $,已知该字符在该位置出现的概率 $ p_{i,c} $。解码器必须找到最有可能被传输的消息,即有效的消息 $ m_1m_2 \dots m_n $,使得乘积 $ \prod_{i=1}^{n} p_{i,m_i} $ 最大。

编写这样的解码器正是你的任务。

输入

输入文件的第一行包含 \(d\)\(k\)\(t\)——单词集合 \(D\) 中的单词数量、所有单词的长度以及单词中可以使用的字符数量,分别($1 \leq d \leq 200 $,\(1 \leq k \leq 10\)\(2 \leq t \leq 26\))。接下来的 \(d\) 行各包含一个单词。所有单词仅包含英文字母表中的前 \(t\) 个小写字母。

下一行包含 \(n\)——消息的长度(\(k \leq n \leq 1000\))。接下来的 $ n $ 行各包含 $ t $ 个实数——第 $ i $ 行中的第 $ j $ 个数字是消息的第 $ i $ 个字符等于字母表中的第 $ j $ 个字符的概率。每行的概率之和等于 \(1\)

输出

如果不存在长度为 $ n $ 的消息,使得其所有子单词都属于 $ D $,或者没有单词具有正概率,则在输出文件中打印 “---”(三个减号)。否则,输出最有可能的消息。如果有多个解,输出其中任意一个。

示例

\(\textrm{decoding.in}\)

3 2 2  
ab  
ba  
aa  
5  
0.8 0.2  
0.2 0.8  
0.2 0.8  
0.8 0.2  
0.2 0.8  

\(\textrm{decoding.out}\)

abaab  

思路

给定 \(p_{i, c}\) 表示位置 \(i\) 是字符 \(c\) 的概率, 确定 \(\displaystyle\sum_{c = 1}^{t} p_{i, c} = 1\)
一个有效的信息被定义为任意长度为 \(k\) 的子序列都在集合 \(\mathbb{D}\) 中出现
求一个有效的信息 \(c_1c_2c_3c_4\cdots c_n\) , 使得 \(\displaystyle \prod_{i = 1}^{n} p_{i, c_i}\) 最大

首先我们把 \(\mathbb{D}\) 中的串串丢进字典树上, 对于每一个串串, 我们不难处理出其后缀串串的下一个位置, \(\rm{belike}\):
pEm5rE4.png

我们简单搞一组数据

aba
bab
abb
bba
baa

放到字典树上, 并且把跳跃关系 \((\)\(\color{red}{红边}\)\()\) 连上

pEmvzZR.png

连跳跃关系是 \(\mathcal{O} (dt)\) 的, 反正不超时当常数

事实上我们可以把图简化成这样, 不影响答案
pEmxYes.png

考虑一组合法的解, 就是对这个图的一种长为 \(n\) 的遍历
把遍历顺序分层, 不难想到每一层都用概率最大的, 这样贪心下去, 但是是否正确
显然不正确, 但是我们可以考虑正确性更好的图上 \(\rm{dp}\)

\(dp_{i, j}\) 表示对于位置 \(i\) , 当前在字典树的 \(j\) 节点的最优方案, 每次转移显然可以跳不止一个位置
空间复杂度是 \(\mathcal{O} (dkn)\) 的, 一会看着优化

不难发现, 如果我们优化状态, 令 \(dp_{i, j}\) 表示对于位置 \(i\) , 当前在 \(j\) 个字符串的结尾, 一样可以转移
把字典树上的操作用数组映射好, 不影响转移

然后简单转移即可

实现

直接给出代码

#include <bits/stdc++.h>
const int MAXN = 1005;
const int MAXD = 205;
const int MAXT = 30;

int d, k, t, n;
long double pr[MAXN][MAXT];

std::string str[MAXD];
std::map<std::string, int> hash;

int go[MAXD][MAXT], last[MAXN][MAXD];
long double dp[MAXN][MAXD];

int main()
{
    freopen("decoding.in", "r", stdin);
    freopen("decoding.out", "w", stdout);
    scanf("%d %d %d", &d, &k, &t);
    for (int i = 0; i < d; i++) std::cin >> str[i], hash[str[i]] = i;
    
    memset(go, -1, sizeof(go));
    for (int i = 0; i < d; i++) {
        std::string tmp = str[i].substr(1);
        for (int j = 0; j < t; j++) {
            tmp += (char)('a' + j);
            if (hash.find(tmp) != hash.end()) go[i][j] = hash[tmp];
            tmp.pop_back();
        }
    }

    scanf("%d", &n);
    for (int i = 0; i < n; i++) for (int j = 0; j < t; j++) scanf("%Lf", &pr[i][j]);

    /*初始化*/
    for (int i = 0; i < d; i++) {
        dp[k - 1][i] = 1.0;
        for (int j = 0; j < k; j++) dp[k - 1][i] *= pr[j][str[i][j] - 'a'];
    }

    for (int i = k - 1; i < n - 1; i++) for (int j = 0; j < d; j++) for (int c = 0; c < t; c++) {
        if (!(~go[j][c])) continue;
        int to = go[j][c];
        if (dp[i + 1][to] < dp[i][j] * pr[i + 1][c]) dp[i + 1][to] = dp[i][j] * pr[i + 1][c], last[i + 1][to] = j;
    }
        
    
    long double ans = 0.0;
    int id = -1;
    for (int i = 0; i < d; i++) {
        if (dp[n - 1][i] > ans) {
            ans = dp[n - 1][i];
            id = i;
        }
    }

    if (ans <= 0) {
        puts("---");
        return 0;
    }

    int cur = n - 1;
    std::string res;
    while (cur > k - 1) {
        res = str[id].back() + res;
        id = last[cur][id];
        cur--;
    }
    res = str[id] + res;
    std::cout << res << '\n';
    return 0;
}

总结

图上的 最小 / 最大 问题, 尝试使用图上 \(\rm{dp}\) 解决
贪心不正确的时候, 考虑加一维上 \(\rm{dp}\)

\(last\) 数组是经典的回溯输出方案, 见得不多

本题中的节约空间方法:
注意到转移并不需要具体你在哪个点, 只需要知道下一个地方在哪里

不难发现两种转移

  • 去这个点在字典树中的下一个点 \((\)\(\color{black}{黑边}\)\()\)
  • 去这个点跳跃后的下一个点 \((\)\(\color{red}{红边}\)\()\)

本质上, 对于一个字符串, \(\color{red}{红边}\)只会从一个字符串的倒数第一个位置连向一个字符串的倒数第二个位置, 两个字符串可能是相同的, 我们利用这个性质

把记录字典树上的点变成记录第 \(i\) 个字符串, 然后转移就直接跳到下一个字符串的倒数第二个位置, 然后简单的走一条黑边到倒数第一个位置即可

posted @ 2025-02-09 16:34  Yorg  阅读(52)  评论(0)    收藏  举报