【提高+/省选−】洛谷P1127 ——词链
见:P1127 词链 - 洛谷
题目描述
如果单词 X 的末字母与单词 Y 的首字母相同,则 X 与 Y 可以相连成 X.Y。(注意:X、Y 之间是英文的句号 .
)。例如,单词 dog
与单词 gopher
,则 dog
与 gopher
可以相连成 dog.gopher
。
另外还有一些例子:
dog.gopher
gopher.rat
rat.tiger
aloha.aloha
arachnid.dog
连接成的词可以与其他单词相连,组成更长的词链,例如:
aloha.arachnid.dog.gopher.rat.tiger
注意到,.
两边的字母一定是相同的。
现在给你一些单词,请你找到字典序最小的词链,使得每个单词在词链中出现且仅出现一次。注意,相同的单词若出现了 k 次就需要输出 k 次。
输入格式
第一行是一个正整数 n(1≤n≤1000),代表单词数量。
接下来共有 n 行,每行是一个由 1 到 20 个小写字母组成的单词。
输出格式
只有一行,表示组成字典序最小的词链,若不存在则只输出三个星号 ***
。
输入输出样例
in:
6
aloha
arachnid
dog
gopher
rat
tigerout:
aloha.arachnid.dog.gopher.rat.tiger
说明/提示
- 对于 40% 的数据,有 n≤10;
- 对于 100% 的数据,有 n≤1000。
这道题好难啊╥﹏╥
整体思路
这个问题本质上是在有向图中寻找欧拉路径。每个字符串可以看作图中的一条边,边的起点是字符串的首字符,终点是字符串的尾字符。我们需要判断:
- 是否存在欧拉路径
- 如果存在,找出具体的路径
code
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
string a[maxn];
string ans[maxn];
string now[maxn];
int sum=0;
int len[maxn];
int book[maxn];
map<char,int> s1,s2;
int n;
int flag=0;
void dfs(int last,int step) {if(flag==1)return;if(step==n) {flag=1;for(int i=1; i<=sum; i++) {ans[i]=now[i];}return;}for(int i=1; i<=n; i++) {if(book[i]==1)continue;if(a[last][a[last].length()-1]==a[i][0]) {now[++sum]=a[i];book[i]=1;dfs(i,step+1);sum--;book[i]=0;}}
}
int main() {scanf("%d",&n);for(int i=1; i<=n; i++) {cin>>a[i];len[i]=a[i].length();s1[a[i][0]]++;s2[a[i][len[i]-1]]++;}int start=1;sort(a+1,a+1+n);char s,t;for(char c='a'; c<='z'; c++) {if(abs(s1[c]-s2[c])==1) {if(s1[c]-s2[c]==1)s=c;else if(s2[c]-s1[c]==1)t=c;}}int cnt=s2[t];for(int i=1; i<=n; i++) {if(a[i][0]==s && (a[i][len[i]-1]!=t || cnt!=1)) {start=i;break;}}book[start]=1;now[++sum]=a[start];dfs(start,1);if(flag==0) {printf("***\n");return 0;}for(int i=1; i<=n; i++) {if(i!=n)cout<<ans[i]<<".";elsecout<<ans[i];}return 0;
}
能理解吗?
不能的话来分析一下
——————————————————————————————————-———————
代码详细解析
#include<bits/stdc++.h>
using namespace std;
这两行代码包含了所有标准库头文件并使用标准命名空间,简化了后续代码的编写。
数据结构与全局变量
const int maxn=1e5+5;
string a[maxn]; // 存储输入的所有字符串
string ans[maxn]; // 存储最终的合法接龙序列
string now[maxn]; // 存储当前DFS搜索路径中的接龙序列
int sum=0; // 当前路径中的字符串数量
int len[maxn]; // 每个字符串的长度
int book[maxn]; // 标记字符串是否已被使用(1表示已使用,0表示未使用)
map<char,int> s1,s2;// 统计每个字符作为字符串开头和结尾的次数
int n; // 字符串数量
int flag=0; // 标记是否已找到合法解(1表示找到,0表示未找到)
输入处理与预处理
int main() {scanf("%d",&n); // 读取单词数量for(int i=1; i<=n; i++) {cin>>a[i]; // 读取每个单词len[i]=a[i].length(); // 记录单词长度s1[a[i][0]]++; // 统计首字母出现次数s2[a[i][len[i]-1]]++; // 统计尾字母出现次数}int start=1; // 默认从第一个单词开始搜索sort(a+1,a+1+n); // 对单词进行字典序排序,确保找到的解是字典序最小的
这里使用两个map来统计每个字符作为字符串开头和结尾的次数,
为后续判断欧拉路径做准备。
例如:
- 如果输入字符串为 ["abc", "cde", "efg"]
- 则
s1
为 {'a':1, 'c':1, 'e':1} s2
为 {'c':1, 'e':1, 'g':1}
欧拉路径判断
sort(a+1,a+1+n); // 先排序,确保字典序最小
char s,t;
for(char c='a';c<='z';c++)
{if(abs(s1[c]-s2[c])==1){if(s1[c]-s2[c]==1)s=c; // 起点字符(出度比入度多1)elseif(s2[c]-s1[c]==1)t=c; // 终点字符(入度比出度多1)}
}
int cnt=s2[t];
这部分代码判断是否存在欧拉路径:
- 欧拉路径条件:
- 有向图中,所有节点的入度等于出度,或者
- 存在一个节点的出度比入度多 1 (起点),一个节点的入度比出度多 1 (终点),其余节点的入度等于出度
s
是起点字符,t
是终点字符cnt
记录终点字符t
作为结尾的次数
确定搜索起点
int start=1;
for(int i=1;i<=n;i++)
{if(a[i][0]==s && (a[i][len[i]-1]!=t || cnt!=1)){start=i;break;}
}
book[start]=1;
now[++sum]=a[start];
这部分代码确定 DFS 的起始字符串:
- 优先选择以
s
开头且不以t
结尾的字符串作为起点 - 如果所有以
s
开头的字符串都以t
结尾,则选择其中任意一个 - 标记该字符串为已使用,并加入当前路径
深度优先搜索(核心逻辑)
void dfs(int last,int step)
{if(flag==1) // 已找到解,直接返回return;if(step==n) // 已找到n个字符串的合法序列{flag=1;for(int i=1;i<=sum;i++){ans[i]=now[i]; // 保存当前路径到结果数组}return;}for(int i=1;i<=n;i++){if(book[i]==1) // 跳过已使用的字符串continue;if(a[last][a[last].length()-1]==a[i][0]) // 当前字符串的结尾等于下一个的开头{now[++sum]=a[i]; // 加入当前路径book[i]=1; // 标记为已使用dfs(i,step+1); // 递归搜索sum--; // 回溯book[i]=0; // 撤销标记}}
}
这是典型的回溯 DFS 算法:
- 参数说明:
last
:当前路径中最后一个字符串的索引step
:当前路径的长度
- 终止条件:
- 当路径长度达到 n 时,表示找到合法解
- 将当前路径保存到
ans
数组中,并标记flag=1
- 递归过程:
- 遍历所有未使用的字符串
- 选择下一个能与当前字符串首尾相连的字符串
- 标记该字符串为已使用,加入路径,递归搜索
- 回溯操作:撤销当前选择,尝试其他可能性
输入处理与预处理
int main() {scanf("%d",&n); // 读取单词数量for(int i=1; i<=n; i++) {cin>>a[i]; // 读取每个单词len[i]=a[i].length(); // 记录单词长度s1[a[i][0]]++; // 统计首字母出现次数s2[a[i][len[i]-1]]++; // 统计尾字母出现次数}int start=1; // 默认从第一个单词开始搜索sort(a+1,a+1+n); // 对单词进行字典序排序,确保找到的解是字典序最小的
这里使用两个map来统计每个字符作为字符串开头和结尾的次数,
为后续判断欧拉路径做准备。
例如:
- 如果输入字符串为 ["abc", "cde", "efg"]
- 则
s1
为 {'a':1, 'c':1, 'e':1} s2
为 {'c':1, 'e':1, 'g':1}
欧拉路径判断
char s, t;for(char c='a'; c<='z'; c++) {if(abs(s1[c]-s2[c])==1) { // 找到入度和出度差为1的字母if(s1[c]-s2[c]==1)s=c; // 首字母次数比尾字母多1的字母,作为序列的起始字母else if(s2[c]-s1[c]==1)t=c; // 尾字母次数比首字母多1的字母,作为序列的结束字母}}int cnt=s2[t]; // 结束字母t作为尾字母的次数for(int i=1; i<=n; i++) {if(a[i][0]==s && (a[i][len[i]-1]!=t || cnt!=1)) {start=i; // 找到以s开头且不以t结尾的单词,或者t作为尾字母仅出现一次的单词,作为起始单词break;}}
这部分代码判断是否存在欧拉路径:
- 欧拉路径条件:
- 有向图中,所有节点的入度等于出度,或者
- 存在一个节点的出度比入度多 1 (起点),一个节点的入度比出度多 1 (终点),其余节点的入度等于出度
s
是起点字符,t
是终点字符cnt
记录终点字符t
作为结尾的次数
确定搜索起点
book[start]=1; // 标记起始单词已使用now[++sum]=a[start]; // 将起始单词加入当前路径dfs(start,1); // 从起始单词开始DFS搜索if(flag==0) { // 如果未找到合法解printf("***\n");return 0;}for(int i=1; i<=n; i++) { // 输出找到的合法解if(i!=n)cout<<ans[i]<<"."; // 单词之间用点连接elsecout<<ans[i];}printf("\n");return 0;
}
这部分代码确定 DFS 的起始字符串:
- 优先选择以
s
开头且不以t
结尾的字符串作为起点 - 如果所有以
s
开头的字符串都以t
结尾,则选择其中任意一个 - 标记该字符串为已使用,并加入当前路径
输出结果
if(flag==0)
{printf("***\n"); // 无法形成合法接龙return 0;
}
for(int i=1;i<=n;i++)
{if(i!=n)cout<<ans[i]<<"."; // 用点连接各个字符串elsecout<<ans[i];
}
算法关键点
-
欧拉路径判断:
- 通过统计字符的开头和结尾次数,判断是否满足欧拉路径条件
- 确定起点和终点字符
-
DFS 回溯搜索:
- 在满足欧拉路径条件的前提下,通过 DFS 找到具体的字符串序列
- 使用回溯确保所有可能性被探索
-
字典序优化:
- 预先对字符串数组排序,确保优先选择字典序小的字符串
- 起点选择策略保证了在多个可能的起点中选择字典序最小的路径
复杂度分析
- 时间复杂度:最坏情况下是 O (n!),但通过欧拉路径的判断和起点优化,实际运行效率会高很多
- 空间复杂度:O (n),主要用于存储字符串和递归调用栈
这个算法巧妙地将字符串接龙问题转化为图论中的欧拉路径问题,并通过 DFS 回溯搜索找到具体解,是一道非常经典的算法题。
———————————————————————————————————————————
好了
到此结束吧
———————————————————————————————————————————
THE END
对了
听说给点赞+关注+收藏的人会发大财哦(o゚▽゚)o