题目链接: Spy Syndrome 2
大致题意
给定一个长度为n的字符串str, 全部由小写字母组成.
接下来给出单词列表, 共包含m个单词, 每个单词由大写和小写字母组成.
字符串str可以保证是由若干个单词组成, 但是进行了加密.
加密操作: 每个单词的所有字母都被转换成小写字母, 并且进行了翻转.
要求你复原出str, 并且在单词与单词之间加上空格输出.
解题思路
本题不推荐看其他用Trie + dfs的题解, 理论上复杂度应该是爆炸了.
网上深搜的做法都是把单词翻转存入Trie, 但是我反过来把str翻转就T59了.
既然翻转str能T59, 那么把Test59的所有数据翻转输入, 理论上也可以T掉网上的dfs代码.
字典树
考虑字符串str中的每个单词都被翻转过, 因此我们不妨考虑把str进行翻转. 这样我们只需要检查str能否由若干个单词拼出即可.
这里相当于查找某个字符串是否已经出现过, 我们可以想到用Trie树来维护.
考虑到单词有大小写, 而str只有小写. 因此我们不妨对单词进行小写转换, 存入Trie中. 对于每个单词结束的位置, 进行标记.
接下来就是我们怎么去拆分str.
我的做法是: 维护一个dp数组, dp[i]存储字符串的前i个字符是否可以被拆成若干个单词表示, 若可以, 则记录当前位置为结束位置的单词编号(记录路径).
例如: 有长度为10的str = “abcdefghij” 单词有: ab fghij fau cde, 依次编号1~4.
那么 dp[2] = 1, dp[5] = 4, dp[10] = 2. 其余dp[i] = 0.
维护dp的方法: 首先我们对于str进行一次匹配, 把所有有单词结尾的位置进行标记(如上例, 我们会标记出dp[2] = 1). 然后进行每个字符位置遍历: 如果当前位置index可以成功匹配, 则对于index+1的位置再次进行匹配.
这样, 最坏的情况下是每一个位置都被标记, 并且每一个位置都能匹配到Trie树末端. 由于str最长的长度为1E4, 而单词的最大长度为1E3, 因此最坏的复杂度为1E7.
最后考虑如何输出这些单词, 由于我们dp数组内部记录了以第i个位置作为结束的字符串编号, 因此我们可以倒推出所有单词.
代码细节: string的下标从0开始, 而我的dp编号从1开始, 请大家注意区分.
AC代码
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 1; i <= (n); ++i)
using namespace std;
typedef long long ll;
const int N = 1E5 + 10, M = 1E6 + 10;
int t[M][26], ind;
int pos[M]; // 对应的字符串编号。
int n; string str;
string text[N]; //所有单词
void insert(const string& s, int id) {
int x = 0;
for (auto& op : s) {
char c = tolower(op) - 'a';
if (!t[x][c]) t[x][c] = ++ind;
x = t[x][c];
}
pos[x] = id;
}
int dp[N];
void ask(int index) {
int x = 0;
for (int i = index; i < n; ++i) {
char c = str[i] - 'a';
if (!t[x][c]) return;
x = t[x][c];
if (pos[x]) dp[i + 1] = pos[x];
}
}
vector<int> v;
void dfs(int now = n) {
if (!now) return;
v.push_back(dp[now]);
dfs(now - text[dp[now]].size());
}
int main()
{
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin >> n >> str;
reverse(str.begin(), str.end());
int m; cin >> m;
rep(i, m) {
cin >> text[i];
insert(text[i], i);
}
ask(0);
rep(i, n) {
if (!dp[i]) continue;
ask(i);
}
dfs();
for (auto& op : v) cout << text[op] << ' ';
puts("");
return 0;
}