题目:(来源:词链 - 洛谷)
如果单词 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个小写字母组成的单词。
输出:
只有一行,表示组成字典序最小的词链,若不存在则只输出三个星号***。
样例:
输入:
6 aloha arachnid dog gopher rat tiger
输出:
aloha.arachnid.dog.gopher.rat.tiger
思路:
首先我们知道:如果字符串A的尾部等于字符串B的头部,那么说明这两个字符串是有可能相连的。
数据范围并不是很大,我们可以考虑用搜索的策略。
关键是要找到一个准确的起点来搜索
那么起点是哪个字符串呢?
我们可以来仔细分析一下:如果假设有字符串abs和字符串sto这两个字符串。他们相连后就是abs.sto。
我们发现,每个点两边的字母一定是相同的。那对于整条链来说,如果除去最左端和最右端的两个字母后。中间的每个点两边的字母都相同,也就有了以下两个很重要的性质:
1.每个字母出现的次数一定是偶数次
2.每个字符出现在字符串首位的个数一定等于出现在字符串末尾的个数(因为对于任何一个点,都能满足上面的条件)
现在将左端和右端的两个字母考虑进去,一定会出现以下两种情况中的一种:
1.左端和右端的两个字母相同,上面的两个性质仍然成立,此时我们将字典序最小的那个字符串作为起点。
2.左端和右端的两个字符不同,上面两个性质不再成立,但是又有了新的性质:
(1)一定有一个字母,其出现在字符串首位的个数等于出现在字符串末尾的个数+1,这个字母一定作起点(设为c)。(该性质可以通过举例进行验证。首先考虑参与单词连接的字母包含了左端的字母,则可知该字母出现在左端的这一次首位的次数由于没有对应的出现在末端的次数与之对应,因此,出现在首位的次数比出现在末尾的次数多一次。然后考虑参与单词连接的字母没有包含左端的字母,那么左端字母在全过程中,出现在首端一次,出现在末端零次,依然满足该性质。)
(2)一定还有一个字母,其出现在字符串首位的个数等于出现在字符串末尾的个数-1(同理可证末端性质),这个字母一定作终点(设为d)。我们在以c为首的字符串中找到字典序小的,让它作为起点即可。但是同时我们得保证该字符串的终点不为d,或者以d为结尾的个数不为1,要加上这个判断
现在我们就知道了起点start,我们就从这里出发,开始搜索。
搜索的注意事项:
1.我们需要定义一个flag标记是否已经找到答案。由于是按字典序搜索,第一个找到的答案一定是最优答案。这时候要立即return,在之后的回溯过程中,如果flag==1,也要立即回溯,不然有可能是正确答案被更换掉。
2.记录最终答案和在搜索过程中需要定义两个字符串数组。一个负责在搜索过程中不断更新,另一个负责找到答案后转移。
3.还需要一个book[]数组,用来判断当前的字符串是否已经用过。搜索前标记成book[]=1;回溯时要记得book[]标记回0;
4.一开始的起点一定要先标记book[]=1;
然后搜索部分就自然地解决啦,我们维护上一个字符串是谁, 在枚举当前字符串的时候,如果该字符串的首字符等于上一个字符串的尾字符,就可以继续往下搜。
下面是一个初步的代码:
#include<cmath>
#include<cstring>
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<map>
using namespace std;
const int maxn=1e5+5; //规定一个很大的数防止数组越界
string a[maxn]; //定义存储字符串的数组a,用来存储输入的所有单词
string ans[maxn]; //定义存储字符串的数组ans,用来存储最后的结果
string now[maxn]; //定义存储字符串的数组now,在程序运行中临时存储一定量的单词
int sum=0; //参与词链构建的单词数,初始化为0
int len[maxn]; //整型数组记录每个单词的长度
int book[maxn]; //记录每个单词是否被才用过,也可用bool型进行替换
map<char,int> s1,s2; //s1字典:记录单个字母出现在首端的次数 //s2字典:记录单个字母
出现在末端的次数
int n;
int flag=0;
void dfs(int last,int step) //深度优先遍历算法,last表示末端出现的单词在数组a中的下标,
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;
}
}
}
int main()
{
scanf("%d",&n); //输入单词数n
for(int i=1;i<=n;i++)
{
cin>>a[i]; //输入n个单词
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; //s表示首字母,t表示尾字母
for(char c='a';c<='z';c++)
{
if(abs(s1[c]-s2[c])==1)
{
if(s1[c]-s2[c]==1) //首端比末端出现次数多1,则为首字母
s=c;
else
if(s2[c]-s1[c]==1) //末端比首端出现次数多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)) /*条件成立前提:1.单词的首字母与
s相同
2.单词的尾字母和t不同, 或者t多
次出现*/
{
start=i; //将符合规定的单词记录为start
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]<<"."; //若符合题意,则输出对应结果
else
cout<<ans[i];
}
printf("\n");
return 0;
}
这个代码在洛谷上运行是可以AC的,但是在细节上还存在一些问题。
如果直接把这个代码上机运行,仅输入一个单词aba,会因为s和t没有被初始化而报错。这是为什么呢?这是因为在最初给s和t选取初始值的时候,就已经默认了大前提是词链的左端和右端的字母不同。如果词链的左端和右端字母相同,那么就无法执行初始化语句,进而导致报错。本质上是选取词链的左端字母和右端字母只考虑了左右两端字母不同的情况,而忽略了左右两端字母相同的情况。
首先我们要考虑只有一个单词的特殊情况,默认一个单词在任何情况下都能符合题意,直接输出即可。
然后再对存在多个单词情况下的选取条件进行分析。
大前提是考虑到词链的左端字母和右端字母相同和不同两种情况。当词链的左端字母和右端字母相同时,此时所有在单词的首部和尾部出现过的字母实际上都可以作为词链的首字母。但由于遵从字典序从小到大的规则,因此从已经排序好的第一个单词开始,由于左右两端字母相同,因此s和t均为第一个单词的第一个字母。然后开始选取第二个单词,每往后一个单词,都要求这个单词的首字母和上一个单词的尾字母相同,如果符合条件,就将这个单词存入临时数组。在这个基础上不断递归深度优先算法即可得到最终结果。当词链的左端字母和右端字母不同时,此时需要先遍历所有单词找到第一个符合规定的单词。这个单词应该满足首字母和左端字母相同,但同时对尾字母又有一定的限制。设想词链的右端字母仅在最右端出现过一次,则在遍历所有的单词中,只要某个单词满足尾字母和最右端字母相同,则能立即确定该单词一定是词链最右边的一个单词。由于此时至少存在两个单词,为了保证某个单词能作为第一个单词,则这个单词自身必定满足其尾字母和词链右端字母不同。而假如词链的右端字母出现过多次,则其他单词可以不受此限制。
#include<cmath>
#include<cstring>
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<map>
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,flag1=0; //flag1反映词链两端字母是否相同
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_s("%d", &n);
if (n == 1) //一个单词一定符合题意,直接输出
{
int i = 1;
cin >> a[i];
cout << a[i] << endl;
}
else
{
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) //词链左右两端字母不同,则s和t被初始化
{
if (s1[c] - s2[c] == 1)
{
s = c;
flag1 = 1;
}
else
if (s2[c] - s1[c] == 1)
{
t = c;
flag1 = 1;
}
}
}
if (flag1 == 0) //词链左右两端字母相同,以第一个单词的首字母为准
{
s = a[1][0];
t = a[1][0];
}
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] << ".";
else
cout << ans[i];
}
printf("\n");
return 0;
}
}
运行结果:
本题首先要观察词链中,每个单词首字母和尾字母的特点,初步推导出其性质,然后再根据其给定的条件进行相应判断并通过不断调用递归函数得到结果。在AC的基础上还要注重对一个单词,以及是否能够真正上机运行的细节处理。