首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐
2025年5月28日 星期三 2:33pm

最小操作数

  • 25-03-02 17:21
  • 4390
  • 7659
blog.csdn.net

最小操作数

题目大意: 给定一个字典和两个单词。问能否从给定的第一个单词开始,每次只改变一个字母,最终变为第二个单词。要求除第一个单词和最后一个单词外,中间的单词都在给定的字典内。给出最小的最短的变换序列。如果有多组解,给出全部的解。字典里的单词都同样长,全由小写字母组成。

 

分析:本题是一个广度优先搜索的典型问题。

简介:图的两种搜索方法:常见的图搜索算法有两种,分别简介如下:

(1) 深度优先搜索 (Depth First Search,DFS):

            比如走迷宫问题,我们从入口开始沿着边不断地向“深处”走,能走则走。当遇到岔路时,我们按一定顺序选择第一条岔路继续下去,当遇到死路时,我们退回到最后一个岔路的位置再选择第二条岔路继续下去,直到我们遍历了图的所有边。这种搜索方法包含了递归回溯的思想。DFS有很多应用,例如有向图的强连通分量问题、拓扑排序问题都可以利用该思想解决。

             在实现上,我们通常采取堆栈作为数据结构,堆栈里保存的是我们经过的节点。当走到死路回退时,我们从堆栈里弹出这个没用的节点而回到上级节点继续尝试搜索。Dfs的优点是比较省空间(因为只保存了一条路径),程序写起来简单(特别是可以利用系统自带的堆栈写递归程序,而不需要自己写堆栈时)。但是dfs也有缺点,最明显的一条是当它找到一条路径(一个解)时,是任意的一个解,如果我们需要找到一个最优解,通常的dfs算法一般要遍历所有的解才可能找出。于是,人们为了提出了很多改进,最简单的是所谓分支定界,在每一步时把不可能是最优解的那些路“堵死”。比较复杂的是迭代加深dfs等。

(2) 广度优先搜索,也叫宽度优先搜索 (Bredth First Search, BFS):

            BFS的思想不如DFS那么直观。还是以走迷宫问题为例子,我们从入口开始,先把只走一步能到达的位置进行标记,再从只走一步能达到的位置再走一步标记新的节点……最终看能不能标记到出口。

            BFS做了什么?BFS做的事情是把一个图进行分层。起点在第0层,从起点开始只走一步能达到的未知在第一层。我们把从每一层的位置开始,标记只走一步能达到的未知作为下一层的节点。重复的节点因为在本层或者更早的层出现过,我们不予标记。这样可以用诸如数学归纳法似的形式化方法证明从起点到第i层的点至少需要走i步才可以到达。

            可见bfs可以处理“最少步数”的问题,每到达一个未知,一定是经过从起点达到未知最少的步数。Bfs有这个优点,但是它的缺点是存储空间通常比dfs大很多。我们只要最优解,可是同时存了许多没必要出现的解。为此,后人提出了A*算法之类的作为改进。BFS通常采用队列实现,每层的节点按层的先后顺序加入队列,第i层的节点逐个出队,第(i+1)层的节点逐个入队。

 

言归正传,相信大家已经能想到了本题我们应该采用bfs算法。那么我们还需要解决两个问题:

(1) 本题的图是什么?

            BFS是图的搜索算法之一,而本题没有给定图,其实很少有题目会直接告诉你一张图,然后让你遍历。图是隐式的结构,需要我们自己建立。涉及到图就有这么几个问题要思考,节点是什么?边如何建立?图是有方向的还是无方向的?

            对于本题,我们的图的节点是字典里地单词。当然,我们可以把给定的两个单词也加入字典中进行统一处理。两个节点有连边,对应着我们可以把一个单词按照规则变为另外一个单词。比如我们有单词hat,它应该与单词cat有一条连边,因为我们可以把h变为c,同样我们也可以把c变为h,所以我们建立的连边应该是无向的。

            如何建图?有两种办法,第一我们可以把字典里的任意两个单词,通过循环判断一下这两个单词是否只有一个位置上的字母不同。我们简单分析一下这个方法的复杂度,假设字典里有n个单词,我们遍历任意两个单词的复杂度是O(n2),假设每个单词长度为length,我们判断两个单词是否连边的复杂度是O(length),所以这个建图的总复杂度是O(n2*length)。当n比较大时,这个复杂度非常高。

            第二种方法是,我们把字典里地每个单词的每个位置的字母修改一下,从字典里查找一下,修改后的单词是否在字典里出现过。这个算法的复杂度简单分析如下:我们需要遍历字典里地每一个单词O(n),尝试修改每个位置的每个字母,对每个位置我们需要尝试26个字母(其实是25个,因为要改得和原来不同),因此这部分复杂度是O(26*length),总复杂度是O(26 * n * length)。

            两种方法孰优孰劣,只能看一下字典里单词个数与长度之间的关系了。但是通常情况下,我们可认为字典里的单词个数非常多,也就是n比较大,因此可能第二种方法效果会好一些。

            如果我们选择第二种方法建图,我们对每个单词每个未知需要尝试26次修改,事实上我们可以利用图是无向的这一特点,我们对每个位置试图把该位置的字母变到字典序更大的字母。例如,我们只考虑cat变成hat,而不考虑hat变成cat,因为再之前已经把无向边建立了。这样,只进行一半的修改次数,从而减少程序的运行时间。当然这个优化从复杂度上来讲是常数的,因此称为常数优化(我们经常忽略O背后隐藏的常数)。

(2) 如何记录单词序列?

            对于最简单的bfs,我们是如何记录路径的?如果只需要记录一条最短路径的话,我们可以对每个走到的位置,记录走到它的前一个位置。这样到终点后,我们可以不断找到它的前一个位置。我们利用了最短路径的一个特点:即第二次经过一个节点的时候,路径长度不比第一次经过它时短。因此这样的路径是没有圈的。

            但是本题需要记录全部的路径。我们第二次经过一个节点时,路径长度可能会和第一次经过一个节点时路径长度一样。这是因为,我们可能在第i层中有多个节点可以到达第(i + 1)层的同一个位置,这样那个位置有多条路径都是最短路径。但是我们也有办法解决——我们记录经过这个位置的前面所有位置的集合。这样一个节点的前驱不是一个节点,而是一个节点的集合。当然这里,我们需要注意判断,第二次经过一个第(i+ 1)层的位置时是从第i层经过的还是从更靠后的层经过的,如果是后者,我们同样不能保留它前面那个位置作为前驱。

            解决了以上两个问题,我们最终得到的是什么?如果有解的话,我们最终得到的是从终点开始,的前一个可能单词的集合,对每个单词,我们都有能得到它的上一个单词的集合,直到起点。这就是前面bfs分层之后的图,我们从终点开始遍历这个图的到起点的所有路径,就得到了所有的解,这个遍历我们可以采用之前介绍的dfs方法(路径的数目可能非常多)。

            其实,为了简单起见,我们可以从终点开始bfs,因为记录路径记录的是之前的节点,也就是反向的。这样最终可以按顺序从起点遍历到终点的所有路径。

代码:


  1. class Solution
  2. {
  3. public:
  4. // help 函数负责找到所有的路径
  5. void help(intx,vector<int> &d, vector &word,vectorint> > &next,vector &path,vector > &answer) {
  6. path.push_back(word[x]);
  7. if (d[x] == 0) { //已经达到终点了
  8. answer.push_back(path);
  9. }
  10. else {
  11. int i;
  12. for (i = 0; i size(); ++i) {
  13. help(next[x][i],d, word, next,path,answer);
  14. }
  15. }
  16. path.pop_back(); //回溯
  17. }
  18. vector> findLadders(string start, string end, set& dict)
  19. {
  20. vector > answer;
  21. if (start == end) { //起点终点恰好相等
  22. return answer;
  23. }
  24. //把起点终点加入字典的map
  25. dict.insert(start);
  26. dict.insert(end);
  27. set::iterator dt;
  28. vector word;
  29. mapint>allword;
  30. //把set转换为map,这样每个单词都有编号了。
  31. for (dt = dict.begin(); dt!= dict.end(); ++dt) {
  32. word.push_back(*dt);
  33. allword.insert(make_pair(*dt, allword.size()));
  34. }
  35. //建立连边 邻接表
  36. vectorint> > con;
  37. int i,j,n =word.size(),temp,len = word[0].length();
  38. con.resize(n);
  39. for (i = 0; i < n; ++i){
  40. for (j = 0; j
  41. char c;
  42. for (c =word[i][j] + 1; c <= 'z'; ++c) { //每个单词每个位置变更大
  43. char last =word[i][j];
  44. word[i][j] =c;
  45. mapint>::iterator t = allword.find(word[i]);
  46. if (t !=allword.end()) {
  47. con[i].push_back(t->second);
  48. con[t->second].push_back(i);
  49. }
  50. word[i][j] =last;
  51. }
  52. }
  53. }
  54. //以下是标准bfs过程
  55. queue<int> q;
  56. vector<int> d;
  57. d.resize(n, -1);
  58. int from = allword[start],to = allword[end];
  59. d[to] = 0; //d记录的是路径长度,-1表示没经过
  60. q.push(to);
  61. vectorint> > next;
  62. next.resize(n);
  63. while (!q.empty()) {
  64. int x = q.front(), now= d[x] + 1;
  65. if ((d[from] >= 0)&& (now > d[from])) {
  66. break;
  67. }
  68. q.pop();
  69. for (i = 0; i size(); ++i) {
  70. int y = con[x][i];
  71. if (d[y] < 0) { //第一次经过
  72. d[y] = now;
  73. q.push(y);
  74. next[y].push_back(x);
  75. }
  76. else if (d[y] ==now) { //是从上一层经过的,所以要保存
  77. next[y].push_back(x);
  78. }
  79. }
  80. }
  81. if (d[from] >= 0) { //有解
  82. vectorpath;
  83. help(from, d,word,next, path,answer);
  84. }
  85. return answer;
  86. }
  87. };


 

 


文章知识点与官方知识档案匹配,可进一步学习相关知识
算法技能树首页概览49583 人正在系统学习中
注:本文转载自blog.csdn.net的cpcs的文章"http://blog.csdn.net/caopengcs/article/details/9919341"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

101
推荐
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2024 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top