【深度优先搜索/字符串】P1019 单词接龙
题目描述
单词接龙是一个与我们经常玩的成语接龙相类似的游戏,现在我们已知一组单词,且给定一个开头的字母,要求出以这个字母开头的最长的“龙”(每个单词都最多在“龙”中出现两次),在两个单词相连时,其重合部分合为一部分,例如 beast和astonish,如果接成一条龙则变为beastonish,另外相邻的两部分不能存在包含关系,例如 at 和 atide 间不能相连。
输入输出格式
输入格式:
输入的第一行为一个单独的整数n (n<=20)表示单词数,以下n 行每行有一个单词,输入的最后一行为一个单个字符,表示“龙”开头的字母。你可以假定以此字母开头的“龙”一定存在.
输出格式:
只需输出以此字母开头的最长的“龙”的长度。
输入样例: 5 at touch cheat choose tact a 输出样例: 23 解释:连成的“龙”为atoucheatactactouchoose
这是一道很好的练习dfs的题目。只是处理时会有一些麻烦。我们需要判断两个字符串是否能合并,然后进行在深搜中进行合并。
不过要注意,我一直没有看到题目中说“每个单词都最多在'龙'中出现两次”,众所周知,最重要的内容都是括号里的所以需要额外开一个数组记录每个单词出现次数(注意要回溯)。
如何合并两个字符串呢?
对于字符串,我的信条是:
能用string,就不用char!能用string,就不用char!能用string,就不用char!
在功能上,良心的string确实比凉心的char好了太多,十分方便,使代码易于理解。尤其是接续、分割、子串的操作,真是令我叹为观止不知道这个成语用没用对,我语文不好
在前一篇博客里,我粗略地说了一下string中的substr(子串)函数。这个题解还需要用到另外一个函数:append函数。
//append函数: //合并两个字符串,意即将一个字符串接到另一个字符串的后面。 string s1="花样",s2="年华"; s1.append(s2); cout<<s1<<endl; //输出:花样年华 //append有更多用法,可以接受更多参数,本题解中只需这一个最基本操作。
考虑先编写一个判断两个词是否能够拼接的函数。
bool ok(string s1,string s2) { for(int i=1;i<min(s1.length(),s2.length());i++)//先从s1最后一个字符和s2第一个字符比较,然后比较s1的后两个字符(即s1以length-2开头,长度为2的子串)和s2的前两个字符(即s2以0开头,长度为2的子串),以此类推。如果遍历完整个s1都没有发现相同子串,或是直到正好遍历到整个s1才发现相同子串,则不许连接。这也是为什么i<min(s1.length(),s2.length())而不是<=. { if(s1.substr(s1.length()-i,i)==s2.substr(0,i)) { return true; } } return false; }
然后是拼接函数。继承上面的思路,如果搜索到相同的子串,则需要从s1中减去它,再拼接上整个s2。
考虑到上面循环中的 i 表示目前所在的 s1 的子串的开头位置,那么若寻找到 i 满足上述条件,那么把以它开头的子串从 s1 删除以后,剩下的恰好就是s1从0开头,长度为 i 的子串,再将它拼接上 s2 即可。
string merge(string s1,string s2) { for(int i=1;i<s1.length();i++) { if(s1.substr(s1.length()-i,i)==s2.substr(0,i))//发现目标 { return s1.substr(0,s1.length()-i).append(s2);//拼接,先从s1中得到除去重复的子串的剩余部分。 } } }
当然,只有判断了ok(s1,s2)等于true后,才能执行merge(s1,s2)。把这两个函数写成一个也不是不可以,但是函数的返回值不太好处理,我也想过用一个特殊的字符串代替“false”,但还是不太方便,果断放弃。
剩下的思路,就和普通的dfs差不多了。如果一个自发出出现次数小于2并且能合并,就沿着它搜索并回溯。
但是,成功总不是一蹴而就的。我经历了几次Unaccepted,一步步走到AK。我在这里分享我的错误经历,Ce me plaît(法语:我很乐意)。
这是第一版(错误)代码:
int ans,n;//答案、字符串数量 string a[21];//字符串 int eachTime[21];//每个字符串出现次数 char begin;//指定的开头的字符 void search(string s,int foot)//当前接的“龙”及其长度 { for(int i=1;i<=n;i++) { if(eachTime[i]<2&&ok(s,a[i]))//判断 { eachTime[i]++;//搜索 // cout<<s<<endl; string down=merge(s,a[i]);//合并 // cout<<down<<endl; search(down,foot+down.length());//注意这里的第二个参数 eachTime[i]--;//回溯 } } ans=max(ans,foot);//更新最大值 return; }
提交了以后,全WA了。 o(╥﹏╥)o
怎么回事呢???我大体看了一下,答案都是过大导致错误,是不是我记录步数(foot)的时候发生了问题呢?
找了很久之后,我终于发现了。上面我写“注意”的哪一行,第二个参数应该改为down.length()而不加foot。因为down本身就是当前字符串连接下一个字符串产生的大字符串,下一步的foot就应该是这个大字符串本身的长度!所以不用再加当前长度foot了!
search(down,down.length());
修改了以后,以下是第二版主函数(当然也有错误):
int main() { cin>>n; for(int i=1;i<=n;i++) { cin>>a[i]; } cin>>begin; for(int i=1;i<=n;i++) { if(a[i][0]==begin) search(a[i],0);//注意这一行 } cout<<ans; return 0; }
乍一看没什么问题,不就是找到一个以给定字符开头的字符串然后搜索么?
然而,结果WA了4个点!
我又像雕塑《思想者》一样想了好久。猛然发现:发现一个以指定字符开头的串,它本身也是要算进整个搜索过程的!也就是说,它的出现次数首先要+1,然后以它的长度为第二个参数foot再开始搜索!
for(int i=1;i<=n;i++) { if(a[i][0]==begin) { eachTime[i]++;//本身就在最后的字符串里,出现了一次 search(a[i],a[i].length());//它的长度也要算进去 eachTime[i]--;//回溯。好像有没有都行。 } }
最后一版代码(全AC)
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #include<cmath> using namespace std; int ans,n; string a[21]; int eachTime[21]; char begin; bool ok(string s1,string s2) { for(int i=1;i<min(s1.length(),s2.length());i++) { if(s1.substr(s1.length()-i,i)==s2.substr(0,i)) { return true; } } return false; } string merge(string s1,string s2) { for(int i=1;i<s1.length();i++) { if(s1.substr(s1.length()-i,i)==s2.substr(0,i)) { return s1.substr(0,s1.length()-i).append(s2); } } } void search(string s,int foot) { for(int i=1;i<=n;i++) { if(eachTime[i]<2&&ok(s,a[i])) { eachTime[i]++; string down=merge(s,a[i]); search(down,down.length()); eachTime[i]--; } } ans=max(ans,foot); return; } int main() { cin>>n; for(int i=1;i<=n;i++) { cin>>a[i]; } cin>>begin; for(int i=1;i<=n;i++) { if(a[i][0]==begin) { eachTime[i]++; search(a[i],a[i].length()); eachTime[i]--; } } cout<<ans; return 0; }
完结撒花!
白嫖可以,但是要点赞哦!