LeetCode126——单词接龙II

我的LeetCode代码仓:https://github.com/617076674/LeetCode

原题链接:https://leetcode-cn.com/problems/word-ladder-ii/description/

题目描述:

LeetCode126——单词接龙II_第1张图片

知识点:图的广度优先遍历、图的深度优先遍历、SPFA算法、回溯

思路一:图的广度优先遍历+图的深度优先遍历(LeetCode中提交会超时)

本题是LeetCode127——单词接龙的加强版。

(1)先广度优先遍历,求出endWord的所有前驱结点,以及前驱结点的前驱结点,直到beginWord。

在广度优先遍历时,由于要求得一个结点的所有前驱结点,因此一个结点的前驱结点是一个List列表而不是仅仅只有一个结点。这样的要求使得我们的广度优先遍历算法不能照抄LeetCode127——单词接龙的算法。这个过程中我们需要注意如下图所示的一个问题:

LeetCode126——单词接龙II_第2张图片

在我们题127的广度优先遍历算法中,如果此时while循环中的队首元素是A,那么A的后继结点C和D将会被标记成已经访问过并入队。当B成为队首元素出队的时候,由于C和D已经被标记访问过了,我们不会再去访问C和D结点,因此我们也就无法找到C和D的前驱结点B,即题127中的方法使得我们对于每一个结点只能确定一个前驱结点,而不能确定一个前驱结点的List。

那么这个问题怎么解决呢?

上述问题出现的原因在于,对于A结点和B结点,它们都属于同一层,在同一层元素还没有全部出队列的时候我们就设置了下一层的元素C和D是否被访问这个属性,使得我们对于C和D结点,只能找到前驱结点A却找不到其前驱结点B。解决的方法其实很简单:我们只需要在每一层的元素都出队之后再来设定其后继结点是否被访问这个属性值即可。

(2)再深度优先遍历这些前驱结点,得到所有的路径。

深度优先遍历的过程需要使用递归来实现,值得注意的是,在递归的过程中遇到要递归的情况,我们都需要新建一个List,而不能所有的递归情况都共用一个List。这是很简单的一个原理,如下图所示:

LeetCode126——单词接龙II_第3张图片

从a到d这个图中存在两条最短路径a->b->d和a->c->d,我们就需要新建两个List。

具体实现步骤:

a:设置一个函数hasPah()用来判断两个字符串之间是否能相互转换,即图中两个结点是否存在着路径。

b:在findLadders()函数里,新建一个List>类型的变量retListList用来记录要返回的值。

c:我们首先判断endWord是否包含在wordList中。如果endWord没有包含在wordList中,我们直接返回retListList。

d:新建一个HashMap>类型的变量from,用以保存每个结点的前驱结点。新建一个List类型的变量visited,用以记录该结点是否已经被访问。

e:在LeetCode127——单词接龙中,对于图的实现我用的是邻接矩阵的形式,考虑到本题很容易超时,我们用邻接表的形式来实现图。新建一个HashMap>类型的变量nextWords,用来记录各个结点是否相连。由于本题和LeetCode127一样是一个无向图,因此在双重循环遍历的时候我们也可以做一些小小的优化,在设置i结点与j结点相连的同时也可以设置j结点与i结点相连,因此在第二重循环遍历时我们可以只遍历比i值要大的那些结点。

f:新建一个Queue类型的队列queue,并把第一个元素beginWord入队,同时在visited中标记beginWord元素已经被访问。

g:只要队列不为空,就进行以下操作循环。

    g-1:获得队列中的元素个数,记录为levelCount变量。新建一个List类型的变量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:返回结果。

判断wordList各个字符串间是否有路径的时间复杂度是O(m * n ^ 2),n表示wordList中的元素个数,m表示wordList中字符串的长度。在队列中进行的操作的时间复杂度是O(n * x),x为能与每个字符串相互转换的字符串数量,是一个未知值。深度优先遍历的时间复杂度是O(p * q),其中p为广度优先遍历所得的结点数量,q为各个结点的前驱结点个数。

总的来说,时间复杂度是O(m * n ^ 2)。

需要存储一个邻接表用以判断wordList中各个字符串间是否有路径,该邻接表的空间为O(m * n),m为与结点相连的节点数,n为wordList中的结点数。其他还有一些比如存放是否已访问visited变量等都是O(n)级别的空间复杂度。

总的来说,空间复杂度是O(m * n)。

JAVA代码:

public class Solution {
    public List> findLadders(String beginWord, String endWord, List wordList) {
		List> retListList = new ArrayList<>();
		if(!wordList.contains(endWord)) {
			return retListList;
		}
		if(!wordList.contains(beginWord)) {
			wordList.add(beginWord);
		}
		HashMap> from = new HashMap<>();
		List visited = new ArrayList<>();
		HashMap> 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 queue = new LinkedList<>();
		queue.add(beginWord);
		visited.add(beginWord);
		while(!queue.isEmpty()) {
			int levelCount = queue.size();
			List tempVisited = new ArrayList<>();
			while(levelCount-- > 0) {
				String temp = queue.poll();
				int n = wordList.indexOf(temp);
				List 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 tempList = from.get(string);
							tempList.add(temp);
							from.put(string, tempList);
						}else {
							List 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 tempList, HashMap> from, List> 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;
	}

}

思路二:对思路一的改进(在LeetCode中提交会超时)

(1)在广度优先遍历时不保存其各个结点的前驱结点,而是遍历所有结点,记录从到该结点的最短路径长度

(2)在深度优先遍历时也是遍历所有结点求其后继结点,只有最短路径长度+1且和当前结点相连的结点才是当前结点的后继结点

具体实现步骤:

步骤a、b、c同思路一的实现相同。

d:新建一个HashMap类型的变量distance,用以保存到达某个结点的最短路径的长度。

步骤f、g同思路一的实现相同,但是g中的循环体内做的事情不同。

    g-1:取出队首元素记为temp。

    g-2:遍历与temp相连的所有结点,设置其distance值。

h:深度优先遍历。

i:返回结果。

和思路一一样,我们需要判断wordList中各个字符串间是否有路径的时间复杂度是O(m * n ^ 2),n表示wordList中的元素个数,m表示wordList中字符串的长度。在队列中进行的操作的时间复杂度是O(n * x),x为能与每个字符串相互转换的字符串数量,是一个未知值。深度优先遍历由于要遍历所有的结点来判断是否是当前结点的下一个结点,因此其时间复杂度是O(p * n),其中p为广度优先遍历所得的结点数量。

总的来说,时间复杂度是O(m * n ^ 2)。

需要存储一个邻接表用以判断wordList中各个字符串间是否有路径,该邻接表的空间为O(m * n),m为与结点相连的节点数,n为wordList中的结点数。其他还有一些比如存放是否已访问visited变量等都是O(n)级别的空间复杂度。

总的来说,空间复杂度是O(m * n)。

JAVA代码:

public class Solution {
    public List> findLadders(String beginWord, String endWord, List wordList) {
		List> 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> 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 distance = new HashMap<>();
		//广度优先遍历bfs
		Queue 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 list = new ArrayList<>();
		list.add(begin);
		//深度优先遍历dfs
		dfs(nextWords, begin, end, distance, wordList, list, retListList);
		
		return retListList;
	}

	private void dfs(HashMap> nextWords, Integer temp, Integer end, 
			HashMap distance, List wordList, List list, List> 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 getPath(List list, List wordList) {
		List 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;		
	}

}

思路三:SPFA算法+深度优先遍历(回溯)

在LeetCode127——单词接龙中思路二的基础上加一个深度优先遍历(回溯)过程即可。

期望时间复杂度是O(kM),其中k是一个常数,在很多情况下k不超过2,M是图的边数,可见这个算法异常高效,并且经常性地优于堆优化的Dijkstra算法。空间复杂度是O(n ^ 2),其中n为wordList中的单词数量。

JAVA代码:

public class Solution {
    private List> listList = new ArrayList<>();
    private List list = new ArrayList<>();
    private boolean[][] graph;
    private int[] d;
    private final int INF = 1000000000;
    private int[] countInq;
    private boolean[] inq;
    private int size;
    private ArrayList> pre;
    private List tempPath = new ArrayList<>();
    private int start = 0, end = 0;

    public List> findLadders(String beginWord, String endWord, List wordList) {
        HashSet hashSet = new HashSet<>();
        hashSet.addAll(wordList);
        hashSet.add(beginWord);
        if(!hashSet.contains(endWord)){
            return listList;
        }
        int index = 0;
        for(String s : hashSet){
            list.add(s);
            if(s.equals(beginWord)){
                start = index;
            }
            if(s.equals(endWord)){
                end = index;
            }
            index++;
        }
        size = list.size();
        graph = new boolean[size][size];
        for(int i = 0; i < size; i++){
            for(int j = i + 1; j < size; j++){
                if(hashPath(list.get(i), list.get(j))){
                    graph[i][j] = graph[j][i] = true;
                }
            }
        }
        d = new int[size];
        Arrays.fill(d, INF);
        countInq = new int[size];
        Arrays.fill(countInq, 0);
        inq = new boolean[size];
        pre = new ArrayList<>();
        for(int i = 0; i < size; i++){
            pre.add(new HashSet<>());
        }
        spfa(start);
        dfs(end);
        return listList;
    }

    private boolean hashPath(String s1, String s2){
        int count = 0;
        for(int i = 0; i < s1.length(); i++){
            if(s1.charAt(i) != s2.charAt(i)){
                count++;
            }
        }
        if(1 == count){
            return true;
        }
        return false;
    }

    private boolean spfa(int s){
        d[s] = 0;
        Queue queue = new LinkedList<>();
        queue.add(s);
        countInq[s]++;
        inq[s] = true;
        while(!queue.isEmpty()){
            int u = queue.poll();
            inq[u] = false;
            for(int v = 0; v < size; v++){
                if(graph[u][v]){
                    if(d[u] + 1 < d[v]) {
                        pre.get(v).clear();
                        pre.get(v).add(u);
                        d[v] = d[u] + 1;
                        if (!inq[v]) {
                            queue.add(v);
                            countInq[v]++;
                            inq[v] = true;
                            if (countInq[v] > size - 1) {
                                return false;
                            }
                        }
                    }else if(d[u] + 1 == d[v]){
                        pre.get(v).add(u);
                        if (!inq[v]) {
                            queue.add(v);
                            countInq[v]++;
                            inq[v] = true;
                            if (countInq[v] > size - 1) {
                                return false;
                            }
                        }
                    }
                }
            }
        }
        return true;
    }

    private void dfs(int nowVisit){
        tempPath.add(nowVisit);
        if(nowVisit == start){
            List path = new ArrayList<>();
            for(int i = tempPath.size() - 1; i >= 0; i--){
                path.add(list.get(tempPath.get(i)));
            }
            listList.add(path);
            tempPath.remove(tempPath.size() - 1);
            return;
        }
        for(Integer integer : pre.get(nowVisit)){
            dfs(integer);
        }
        tempPath.remove(tempPath.size() - 1);
    }
}

LeetCode解题报告:

LeetCode126——单词接龙II_第4张图片

 

你可能感兴趣的:(LeetCode题解)