leetcode126:Word Ladder II 详细结题报告 以及java实现源代码

Given two words (beginWord and endWord), and a dictionary's word list, find all shortest transformation sequence(s) from beginWord toendWord, such that:

  1. Only one letter can be changed at a time
  2. Each intermediate word must exist in the word list

For example,

Given:
beginWord = "hit"
endWord = "cog"
wordList = ["hot","dot","dog","lot","log"]

Return

  [
    ["hit","hot","dot","dog","cog"],
    ["hit","hot","lot","log","cog"]
  ]

Note:

  • All words have the same length.

  • All words contain only lowercase alphabetic characters.
题目思考:

必须要遍历整棵搜索树,记录所有可能的解路径,然后比较最短的输出,重复节点很多,时间复杂度相当大。有人问可以剪枝么,答案是这里没法剪。如果把已经访问过的剪掉,那么就会出现搜索不完全的情况。

看来直接上来爆搜是不行的。效率低的不能忍。

这样看,如果将相邻的两个单词(即只差一个字母的单词)相互连在一起,这就是一个图嘛。经典的图算法,dijiska算法不就是求解最短路径的算法么。

那么就说直接邻接表建图,然后dijkstra算法求解咯,当然是可以的,边缘权值设为1就行。而且这种思路工程化,模块化思路很明显,比较不容易出错。但此种情况下时间需建图,然后再调用dijkstra,光是后者复杂度就为o(n^2),所以仍有可能超时,或者说,至少还不是最优方法。

建图后进行DFS呢。很可惜,对于一个无向有环图,DFS只能遍历节点,求最短路径什么的还是别想了。(注意,这里对图进行DFS搜索也会生成一颗搜索树,但是与上文提到的递归爆搜得到的搜索树完全不一样哦,主要是因为对图进行DFS得不到严谨的前后关系,而这是最短路径必须具备的)

好了,我们来看看一个例子

如何对这个图进行数据结构上的优化,算法上的优化是解决问题的关键。

通过观察,容易发现这个图没有边权值,也就是所用dijkstra算法显得没必要了,简单的BFS就行,呵呵,BFS是可以求这类图的最短路径的,

正如wiki所言:若所有边的长度相等,广度优先搜索算法是最佳解——亦即它找到的第一个解,距离根节点的边数目一定最少

所以,从出发点开始,第一次"遍历"到终点时过的那条路径就是最短的路径。而且是时间复杂度为O(|V|+|E|)。时间复杂度较dijkstra小,尤其是在边没那么多的时候。

到此为止了么。当然不是,还可以优化。

回到最原始的问题,这个图够好么?它能反映问题的本质么。所谓问题的本质,有这么两点,一是具有严格的前后关系(因为要输出所有变换序列),二是图中的边数量是否过大,能够减小一些呢?

其实,一个相对完美的图应该是这样的

这个图有两个很明显的特点,一是有向图,具有鲜明的层次特性,二是边没有冗余。此图完美的描述了解的结构。

所以,我们建图也要有一定策略,也许你们会问,我是怎么想出来的。

其实,可以这样想,我们对一个单词w进行单个字母的变换,得到w1 w2 w3...,本轮的这些替换结果直接作为当前单词w的后继节点,借助BFS的思想,将这些节点保存起来,下一轮开始的时候提取将这些后继节点作为新的父节点,然后重复这样的步骤。

这里,我们需要对节点“分层”。上图很明显分为了三层。这里没有用到队列,但是思想和队列一致的。因为队列无法体现层次关系,所以建图的时候,必须设立两个数据结构,用来保存当前层和下层,交替使用这两个数据结构保存父节点和后继节点。

同时,还需要保证,当前层的所有节点必须不同于所有高层的节点。试想,如果tot下面又接了一个pot,那么由此构造的路径只会比tot的同层pot构造出的路径长。如何完成这样的任务呢?可以这样,我们把所有高层节点从字典集合中删除,然后供给当前层选取单词。这样,当前层选取的单词就不会与上层的重复了。注意,每次更新字典的时候是在当前层处理完毕之后在更新,切不可得到一个单词就更新字典。例如我们得到了dog,不能马上把dog从待字典集合中删除,否则,下次hog生成dog时在字典中找不到dog,从而导致结果不完整。简单的说,同层的节点可以重复。上图也可以把dog化成两个节点,由dot和hog分别指向。我这里为了简单就没这么画了。

最后生成的数据结构应该这样,类似邻接表

hot---> hop, tot, dot, pot, hog

dot--->dog

hog--->dog, cog

 ok。至此,问题算是基本解决了,剩下的就是如何生成路径。其实很简单,对于这种“特殊”的图,我们可以直接DFS搜索,节点碰到目标单词就返回。

这就完了,不能优化了?不,还可以优化。

可以看到,在生成路径的时候,如果能够从下至上搜索的话,就可以避免那些无用的节点,比如hop pot tot这类的,大大提升效率。其实也简单,构造数据结构时,交换一下节点,如下图

dog--->dot, hog

cog--->hog

hop--->hot

tot--->hot

dot--->hot

pot--->hot

hog--->hot

说白了,构造一个反向邻接表即可。

对了,还没说整个程序的终止条件。如果找到了,把当前层搜完就退出。如果没找到,字典迟早会被清空,这时候退出就行。

说了这么多,上代码吧


/**用来存储最终结果*/
	List<List<String>> result;
	
	/**用来存储某一条结果*/
    List<String> tmp;
    
	 public List<List<String>> findLadders(String beginWord, String endWord, Set<String> wordList) {
		 /**用来存储一个单词的相邻单词*/
		 Map<String,Set<String>> map=new HashMap<String,Set<String>>();
		 
		 /**初始化结果集*/
		 result=new ArrayList<List<String>>();
		 tmp=new ArrayList<String>();
		 
		 /**存储当前层的结果*/
		 Set<String> current=new HashSet<String>();
		 /**存储没有的遍历到的单词*/
		 Set<String>  unvisited=wordList;
		 
		 /**首先遍历到的便是开始的单词*/
		 if(unvisited.contains(beginWord))
		 {
			 unvisited.remove(beginWord);
		 }
		 
		 /**未遍历的节点中包含结束节点*/
		 unvisited.add(endWord);
		 /**初始化当前层位开始节点*/
		 current.add(beginWord);
		 
		 while((!current.contains(endWord))&&unvisited.size()>0)
		 {
			 /**初始化下一层节点*/
			 Set<String> nextstep=new HashSet<String>();
			 
			 /**遍历当前层节点*/
			 for(String word:current)
			 {
				 /**暴力搜索当前节点相近单词*/
				 for(int i=0;i<word.length();i++)
				 {
					 char ch=word.charAt(i);
					 for(int j=0;j<26;j++)
					 {
						 if(ch=='a'+j)
							 continue;
						 else
						 {
							 char c=(char) ('a'+j);
							 String tmpword="";
							 if(i==0)
							 {
								 tmpword=c+word.substring(i+1);
							 }
							 else
							 {
								 tmpword=word.substring(0,i)+c+word.substring(i+1);
							 }
							 if(unvisited.contains(tmpword))
							 {
								 nextstep.add(tmpword);
								 if(map.containsKey(tmpword))
								 {
									 Set<String> set=map.get(tmpword) ;
									 set.add(word);
									 /**为了优化搜索,构建的是反向图*/
									 map.put(tmpword, set);
								 }
								 else
								 {
									 Set<String> set=new HashSet<String>();
									 set.add(word);
									 map.put(tmpword, set);
								 }
							 }
						 }
						 
					 }
				 }
				 
				 
			 }
			 /**如果没有下一层节点,直接返回*/
			 if(nextstep.size()==0) return result;
			 
			 /**找完一层,去掉该层节点。而不是每找到一个节点就去掉,避免漏掉可能的路径*/
			 for(String news:nextstep)
			 {
				 unvisited.remove(news);
			 }
			 /**继续下一层的遍历*/
			 current=nextstep;
		 }
		 findPath(map,endWord,beginWord);
		 return result;
	    }
	 
	 /**根据反向邻接表寻找遍历路径*/
	 public void findPath(Map<String,Set<String>> map,String start,String end)
	 {
		 
		 tmp.add(start);
		 if(start.equals(end))
		 {
			 List<String> ret=new ArrayList<String>(tmp);
			 Collections.reverse(ret);
			 result.add(ret);
			 return;
		 }
		 Set<String> set=map.get(start);
		 for(String s:set)
		 {
			 findPath(map,s,end);
			 /**递归的后续处理*/
			 tmp.remove(tmp.size()-1);
		 }
	 }


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值