前言
有点神奇的一个题, 不太会做
思路
给定 \(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}\):
我们简单搞一组数据
aba
bab
abb
bba
baa
放到字典树上, 并且把跳跃关系 \((\)\(\color{red}{红边}\)\()\) 连上
连跳跃关系是 \(\mathcal{O} (dt)\) 的, 反正不超时当常数
事实上我们可以把图简化成这样, 不影响答案
考虑一组合法的解, 就是对这个图的一种长为 \(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\) 个字符串, 然后转移就直接跳到下一个字符串的倒数第二个位置, 然后简单的走一条黑边到倒数第一个位置即可