\(AcWing\) \(1117\). 单词接龙
一、题目大意
单词接龙是一个与我们经常玩的成语接龙相类似的游戏。
现在我们已知一组单词,且给定一个开头的字母,要求出以这个字母开头的最长的“龙”,每个单词最多被使用两次。
在两个单词相连时,其重合部分合为一部分,例如 \(beast\) 和 \(astonish\) ,如果接成一条龙则变为 \(beastonish\)。
我们可以任意选择重合部分的长度,但其长度必须大于等于\(1\),且 严格小于两个串的长度 ,例如 \(at\) 和 \(atide\) 间不能相连。
输入格式
输入的第一行为一个单独的整数 \(n\) 表示单词数,以下 \(n\) 行每行有一个单词(只含有大写或小写字母,长度不超过\(20\)),输入的最后一行为一个单个字符,表示 龙 开头的字母。
你可以假定以此字母开头的 龙 一定存在。
输出格式
只需输出以此字母开头的最长的 龙 的长度。
二、解题思路
先来思考正常思路怎么写代码,再来思考能不能进化:
-
遍历所有单词,找出 开头字母 是 给定开始字母 的单词,它们都有可能是第一个单词
-
假设选择的第一个单词是\(A\),那么需要逐个考查所有单词(也包括\(A\)自己),是不是可以拼接在当前单词\(A\)的后面,也就是 开头 与 结尾 存在 相同的子串。如果存在不同长度的子串,比如\(abdcd\)与\(dcdab\),那么我们需要选择最短的子串,因为这样才能保证最后拼接的长度最长(贪心)
-
我们需要记录 每个单词 的 出现次数,因为题目要求最多不能超过两次
-
由于每个单词都需要出现两次,也就是\(A\)和\(B\)需要重复匹配最小重合长度,这样有点浪费,如果我们把这段匹配重合的代码抽取出来,只匹配一次,并且把结果记录下来的话,可以节省计算量,也就是 预处理
三、朴素\(dfs\)
#include <bits/stdc++.h>
using namespace std;
const int N = 30;
int n;
string word[N];
int cnt[N];
int ans;
int getSameLength(string a, string b) {
for (int i = 1; i < min(a.size(), b.size()); i++)
if (a.substr(a.size() - i, i) == b.substr(0, i))
return i;
return 0;
}
void dfs(string dragon, int last) {
ans = max((int)dragon.size(), ans);
cnt[last]++;
for (int i = 0; i < n; i++) {
int len = getSameLength(word[last], word[i]);
if (len && cnt[i] < 2)
dfs(dragon + word[i].substr(len), i);
}
cnt[last]--;
}
int main() {
cin.tie(0), ios::sync_with_stdio(false);
cin >> n;
for (int i = 0; i < n; i++) cin >> word[i];
char start;
cin >> start;
for (int i = 0; i < n; i++)
if (word[i][0] == start)
dfs(word[i], i);
printf("%d\n", ans);
return 0;
}
四、实现代码 【\(for\)循环中回溯+预处理】
#include <bits/stdc++.h>
using namespace std;
const int N = 30;
int n; //可以拼接的单词个数
string word[N]; //单词表
int g[N][N]; //代表编号i的可以被j拼接 如i:asd,j:sdf,拼接长度为最小值g[i][j] = 2,i从0开始记位
int cnt[N]; //编号为i的单词使用次数
int ans; //记录最长的龙长度
// dragon:已经拼接出的 龙 字符串
// last:最后一个参加拼接的单词是几号单词
void dfs(string dragon, int last) {
//更新答案
ans = max((int)dragon.size(), ans); // dragon.size()为当前合并的长度
for (int i = 0; i < n; i++)
if (g[last][i] && cnt[i] < 2) { // 如果最后一个拼接单词与当前单词存在重合部分,并且,当前单词使用次数小于2次
cnt[i]++; //整一下试试
dfs(dragon + word[i].substr(g[last][i]), i); //编号为last的可以被i拼接现在尾巴为i号
cnt[i]--; //回溯,还原现场
}
}
int main() {
//加快读入
cin.tie(0), ios::sync_with_stdio(false);
//用来拼接龙的单词
cin >> n;
for (int i = 0; i < n; i++) cin >> word[i];
//首字母
char start;
cin >> start;
//预处理得到g[i][j],描述:
// (1)a串与b串是否能够接龙,g[a][b]>0表示可以接龙,(2)g[a][b]=0表示接不上
// (2) g[a][b]>0时,值描述的是当a串与b串接龙时,中间重复的部分有多少个字符
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++) { //注意:a串与a串是可以连接的,不需要判断 i!=j
string a = word[i], b = word[j];
for (int k = 1; k < min(a.size(), b.size()); k++) //枚举中间重复的字符数量,注意两种母串不能完全被吸收,所以k不能取到 a.size()或b.size()
if (a.substr(a.size() - k, k) == b.substr(0, k)) {
g[i][j] = k;
break; //首次找到的就是最短的匹配字符块,需要break,如果继续,则龙就变短了,不划算,贪心
}
//黄海原来以为这里如果发现不相等,就需要break,其实不是的,比如:a=touch,b=cheat,只检查了h和c是不相等的,但不影响拼接成toucheat!
}
//重点:
//(1)预处理出字符串之间的二维关系表
//(2)枚举出重复部分的长度,符合就停止,不符合就继续,因为后面长一点的子串可能重合
//找合适的起点
for (int i = 0; i < n; i++)
if (word[i][0] == start) {
cnt[i]++; // i号单词用了一次,记录成本
dfs(word[i], i); //从word[i]开始遍历,i代表现在是第几个单词
cnt[i]--; // i号单词还原现场,我没有使用,我还有两次机会,你们先来
}
printf("%d\n", ans);
return 0;
}
五、实现代码 【\(dfs\)主函数中回溯+预处理】
#include <bits/stdc++.h>
using namespace std;
const int N = 30;
int n;
string word[N];
int g[N][N];
int cnt[N];
int ans;
void dfs(string dragon, int last) {
ans = max((int)dragon.size(), ans);
cnt[last]++;
for (int i = 0; i < n; i++)
if (g[last][i] && cnt[i] < 2)
dfs(dragon + word[i].substr(g[last][i]), i);
cnt[last]--;
}
int main() {
cin.tie(0), ios::sync_with_stdio(false);
cin >> n;
for (int i = 0; i < n; i++) cin >> word[i];
char start;
cin >> start;
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++) {
string a = word[i], b = word[j];
for (int k = 1; k < min(a.size(), b.size()); k++)
if (a.substr(a.size() - k, k) == b.substr(0, k)) {
g[i][j] = k;
break;
}
}
for (int i = 0; i < n; i++)
if (word[i][0] == start)
dfs(word[i], i);
printf("%d\n", ans);
return 0;
}