原题链接:https://leetcode-cn.com/problems/word-ladder-ii/description/
题目描述:
写在前面:
这道题应该是LeetCode中最难的一道题,如果做此题之前没有做过LeetCode127题,请先去尝试着解决LeetCode127题(关于LeetCode127——单词接龙的解析,请见我的另一篇博文:https://blog.csdn.net/qq_41231926/article/details/81545756)。
知识点:队列,图的广度优先遍历,图的深度优先遍历
思路分析:
(1)思路一:
a.先广度优先遍历,求出endWord的所有前驱结点,以及前驱结点的前驱结点,直到beginWord。
在广度优先遍历时,由于要求得一个结点的所有前驱结点,因此一个结点的前驱结点是一个List列表而不是仅仅只有一个结点。这样的要求使得我们的广度优先遍历算法不能照抄LeetCode127题的算法。这个过程中我们需要注意如下图所示的一个问题:
在我们题127的广度优先遍历算法中,如果此时while循环中的队首元素是A,那么A的后继结点C和D将会被标记成已经访问过并入队。当B成为队首元素出队的时候,由于C和D已经被标记访问过了,我们不会再去访问C和D结点,因此我们也就无法找到C和D的前驱结点B,即题127中的方法使得我们对于每一个结点只能确定一个前驱结点,而不能确定一个前驱结点的List。
那么这个问题怎么解决呢?
上述问题出现的原因在于,对于A结点和B结点,它们都属于同一层,在同一层元素还没有全部出队列的时候我们就设置了下一层的元素C和D是否被访问这个属性,使得我们对于C和D结点,只能找到前驱结点A却找不到其前驱结点B。解决的方法其实很简单:我们只需要在每一层的元素都出队之后再来设定其后继结点是否被访问这个属性值即可。
b.再深度优先遍历这些前驱结点,得到所有的路径。
深度优先遍历的过程需要使用递归来实现,值得注意的是,在递归的过程中遇到要递归的情况,我们都需要新建一个List,而不能所有的递归情况都共用一个List。这是很简单的一个原理,如下图所示:
从a到d这个图中存在两条最短路径a->b->d和a->c->d,我们就需要新建两个List。
(2)思路二:
a.在广度优先遍历时不保存其各个结点的前驱结点,而是遍历所有结点,记录从到该结点的最短路径长度。
b.在深度优先遍历时也是遍历所有结点求其后继结点,只有最短路径长度+1且和当前结点相连的结点才是当前结点的后继结点。
思路的具体实现:
(1)思路一的具体实现:
a.设置一个函数hasPah()用来判断两个字符串之间是否能相互转换,即图中两个结点是否存在着路径。
b.在findLadders()函数里,新建一个List<List<String>>类型的变量retListList用来记录要返回的值。
c.我们首先判断endWord是否包含在wordList中。如果endWord没有包含在wordList中,我们直接返回retListList。
d.新建一个HashMap<String, List<String>>类型的变量from,用以保存每个结点的前驱结点。新建一个List<String>类型的变量visited,用以记录该结点是否已经被访问。
e.在我的LeetCode127解题中,对于图的实现我用的是邻接矩阵的形式,考虑到本题很容易超时,我们用邻接表的形式来实现图。新建一个HashMap<Integer, List<Integer>>类型的变量nextWords,用来记录各个结点是否相连。由于本题和LeetCode127一样是一个无向图,因此在双重循环遍历的时候我们也可以做一些小小的优化,在设置i结点与j结点相连的同时也可以设置j结点与i结点相连,因此在第二重循环遍历时我们可以只遍历比i值要大的那些结点。
f.新建一个Queue<String>类型的队列queue,并把第一个元素beginWord入队,同时在visited中标记beginWord元素已经被访问。
g.只要队列不为空,就进行以下操作循环。
g-1:获得队列中的元素个数,记录为levelCount变量。新建一个List<String>类型的变量tempVisited用以解决在思路分析中思路一中存在的问题。
g-2:再设置一层内循环,只要levelCount > 1,那么内循环就进行以下操作。这层内循环其实就是在循环同一层上的所有元素。
g-2-1:获取队首元素temp。
g-2-2:确定temp的后继结点,如果该后继结点还没有被访问过,即在visited中的标记没有被访问过,那么就在tempVisited中设置其被访问过,同时在该后继结点的前驱列表中添加temp元素。
h.根据tempVisited中的值来设置visited中的值,即在每一层循环结束后才来标记其下一层的所有元素是否被访问过。
i.当endWord被访问过时,break语句跳出循环。
j.将beginWord的前驱置为null。
k.进行深度优先遍历。
l.返回结果。
(2)思路二的具体实现:
步骤a、b、c同思路一的实现相同。
d.新建一个HashMap<Integer, Integer>类型的变量distance,用以保存到达某个结点的最短路径的长度。
步骤f、g同思路一的实现相同,但是g中的循环体内做的事情不同。
g-1:取出队首元素记为temp。
g-2:遍历与temp相连的所有结点,设置其distance值。
h.深度优先遍历。
i.返回结果。
JAVA代码:
(1)思路一:(会超时,期待有朋友能做更好的改进)
public List<List<String>> findLadders(String beginWord, String endWord, List<String> wordList) {
List<List<String>> retListList = new ArrayList<>();
if(!wordList.contains(endWord)) {
return retListList;
}
if(!wordList.contains(beginWord)) {
wordList.add(beginWord);
}
HashMap<String, List<String>> from = new HashMap<>();
List<String> visited = new ArrayList<>();
HashMap<Integer, List<Integer>> nextWords = new HashMap<>();
for (int i = 0; i < wordList.size(); i++) {
nextWords.put(i, new ArrayList<>());
}
for (int i = 0; i < wordList.size(); i++) {
for (int j = i + 1; j < wordList.size(); j++) {
if(hasPath(wordList.get(i).toCharArray(), wordList.get(j).toCharArray())) {
nextWords.get(i).add(j);
nextWords.get(j).add(i);
}
}
}
Queue<String> queue = new LinkedList<>();
queue.add(beginWord);
visited.add(beginWord);
while(!queue.isEmpty()) {
int levelCount = queue.size();
List<String> tempVisited = new ArrayList<>();
while(levelCount-- > 0) {
String temp = queue.poll();
int n = wordList.indexOf(temp);
List<Integer> nextWord = nextWords.get(n);
for (int i = 0; i < nextWord.size(); i++) {
String string = wordList.get(nextWord.get(i));
if(!visited.contains(string)) {
if(!from.containsKey(string)) {
tempVisited.add(string);
queue.add(string);
}
if(from.containsKey(string)) {
List<String> tempList = from.get(string);
tempList.add(temp);
from.put(string, tempList);
}else {
List<String> tempList = new ArrayList<>();
tempList.add(temp);
from.put(string, tempList);
}
}
}
}
for (String string : tempVisited) {
visited.add(string);
}
if(visited.contains(endWord)) {
break;
}
}
from.put(beginWord, null);
dfs(beginWord, endWord, new ArrayList<>(), from, retListList);
return retListList;
}
private void dfs(String beginWord, String curWord, List<String> tempList, HashMap<String, List<String>> from, List<List<String>> templistList) {
if(curWord.equals(beginWord)) {
tempList.add(curWord);
Collections.reverse(tempList);
templistList.add(tempList);
return;
}
tempList.add(curWord);
if(from.get(curWord) != null) {
for (String string : from.get(curWord)) {
dfs(beginWord, string, new ArrayList<>(tempList), from, templistList);
}
}
}
private boolean hasPath(char[] arr1, char[] arr2) {
int diff = 0;
for (int i = 0; i < arr1.length; i++) {
if(arr1[i] != arr2[i]) {
diff++;
}
}
if(diff == 1) {
return true;
}
return false;
}
(2)思路二:
public List<List<String>> findLadders(String beginWord, String endWord, List<String> wordList) {
List<List<String>> retListList = new ArrayList<>();
int end = wordList.indexOf(endWord);
if(end == -1) {
return retListList;
}
int begin = wordList.indexOf(beginWord);
if(begin == -1) {
wordList.add(beginWord);
begin = wordList.indexOf(beginWord);
}
int len = wordList.size();
//建立邻接表
HashMap<Integer, List<Integer>> nextWords = new HashMap<>();
for (int i = 0; i < len; i++) {
nextWords.put(i, new ArrayList<>());
}
for (int i = 0; i < len; i++) {
for (int j = i + 1; j < len; j++) {
if(hasPath(wordList.get(i), wordList.get(j))) {
nextWords.get(i).add(j);
nextWords.get(j).add(i);
}
}
}
HashMap<Integer, Integer> distance = new HashMap<>();
//广度优先遍历bfs
Queue<Integer> queue = new LinkedList<>();
queue.add(begin);
distance.put(begin, 0);
while(!queue.isEmpty()) {
Integer temp = queue.poll();
for (int i = 0; i < nextWords.get(temp).size(); i++) {
if(!distance.containsKey(nextWords.get(temp).get(i))) {
distance.put(nextWords.get(temp).get(i), distance.get(temp) + 1);
queue.add(nextWords.get(temp).get(i));
}
}
}
List<Integer> list = new ArrayList<>();
list.add(begin);
//深度优先遍历dfs
dfs(nextWords, begin, end, distance, wordList, list, retListList);
return retListList;
}
private void dfs(HashMap<Integer, List<Integer>> nextWords, Integer temp, Integer end,
HashMap<Integer, Integer> distance, List<String> wordList, List<Integer> list, List<List<String>> retListList) {
if(list.size() > 0 && list.get(list.size() - 1).equals(end)) {
retListList.add(getPath(list, wordList));
return;
}
for (int i = 0; i < nextWords.get(temp).size(); i++) {
if(distance.get(nextWords.get(temp).get(i)).equals(distance.get(temp) + 1)) {
list.add(nextWords.get(temp).get(i));
dfs(nextWords, nextWords.get(temp).get(i), end, distance, wordList, list, retListList);
int index = list.size() - 1;
list.remove(index);
}
}
return;
}
private List<String> getPath(List<Integer> list, List<String> wordList) {
List<String> retList = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
retList.add(wordList.get(list.get(i)));
}
return retList;
}
private boolean hasPath(String s1, String s2) {
int diff = 0;
char[] arr1 = s1.toCharArray();
char[] arr2 = s2.toCharArray();
for (int i = 0; i < arr1.length; i++) {
if(arr1[i] != arr2[i]) {
diff++;
if(diff > 1) {
return false;
}
}
}
return true;
}
复杂度分析:
(1)思路一
a.时间复杂度:
a-1:判断wordList各个字符串间是否有路径的复杂度是O(m * n ^ 2),n表示wordList中的元素个数,m表示wordList中字符串的长度。
a-2:在队列中进行的操作的复杂度是O(n * x),x为能与每个字符串相互转换的字符串数量,是一个未知值。
a-3:深度优先遍历的时间复杂度是O(p * q),其中p为广度优先遍历所得的结点数量,q为各个结点的前驱结点个数。
总的来说,时间复杂度是O(m * n ^ 2)。
b.空间复杂度:
b-1:需要存储一个邻接表用以判断wordList中各个字符串间是否有路径,该邻接表的空间为O(m * n),m为与结点相连的节点数,n为wordList中的结点数。
b-2:其他还有一些比如存放是否已访问visited变量等都是O(n)级别的复杂度。
总的来说,空间复杂度是O(m * n)。
(2)思路二
a.时间复杂度:
a-1:和思路一一样,我们需要判断wordList中各个字符串间是否有路径的复杂度是O(m * n ^ 2),n表示wordList中的元素个数,m表示wordList中字符串的长度。
a-2:在队列中进行的操作的复杂度是O(n * x),x为能与每个字符串相互转换的字符串数量,是一个未知值。
a-3:深度优先遍历由于要遍历所有的结点来判断是否是当前结点的下一个结点,因此其时间复杂度是O(p * n),其中p为广度优先遍历所得的结点数量。
总的来说,时间复杂度是O(m * n ^ 2)。
b.空间复杂度:
b-1:需要存储一个邻接表用以判断wordList中各个字符串间是否有路径,该邻接表的空间为O(m * n),m为与结点相连的节点数,n为wordList中的结点数。
b-2:其他还有一些比如存放是否已访问visited变量等都是O(n)级别的复杂度。
总的来说,空间复杂度是O(m * n)。