请记住:广度优先遍历,离不开先入先出的队列!
本题为了能理解BFS机制,但不会写代码的朋友服务。
注:解法整理自lc jzj大佬。原链接如下:
https://leetcode-cn.com/problems/word-ladder/solution/suan-fa-shi-xian-he-you-hua-javashuang-xiang-bfs23/
给定两个单词(beginWord 和 endWord)和一个字典,找到从 beginWord 到 endWord 的最短转换序列的长度。转换需遵循如下规则:
每次转换只能改变一个字母。
转换过程中的中间单词必须是字典中的单词。
说明:
如果不存在这样的转换序列,返回 0。
所有单词具有相同的长度。
所有单词只由小写字母组成。
字典中不存在重复的单词。
你可以假设 beginWord 和 endWord 是非空的,且二者不相同。
示例 1:
输入:
beginWord = “hit”,
endWord = “cog”,
wordList = [“hot”,“dot”,“dog”,“lot”,“log”,“cog”]
输出: 5
解释: 一个最短转换序列是 “hit” -> “hot” -> “dot” -> “dog” -> “cog”,
返回它的长度 5。
示例2:
输入:
beginWord = “hit”
endWord = “cog”
wordList = [“hot”,“dot”,“dog”,“lot”,“log”]
输出: 0
解释: endWord “cog” 不在字典中,所以无法进行转换。
先列出运行时间的对比。
解法 | 运行时间 |
---|---|
BFS | 1272ms |
优化BFS | 556ms |
双向BFS | 667ms |
优化双向BFS | 229ms |
神仙优化 | 27ms |
//普通BFS
class Solution {
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
if(!wordList.contains(endWord)){
return 0;
}
//已经访问过的结点
Set<String> visited = new HashSet<>();
//当前要访问结点存进的队列
Queue<String> queue =new LinkedList<>();
queue.offer(beginWord);
visited.add(beginWord);
int count=0;
while(queue.size()>0){
int size=queue.size();
++count;
//在当前要访问结点存进的队列里开始逐个检验,并且把结点的下一层放进队列里,BFS
for(int i=0;i<size;i++){
//每次都是最新要比较的单词 start
String start=queue.poll();
//因为有的单词已经遍历过,有的单词不满足转换会跳过,所以整个wordList遍历一次
for(String s:wordList){
//已遍历过,跳过看下一个单词
if(visited.contains(s)){
continue;
}
//不能转换一个字母变成这个单词的,也跳过
if(!canConvert(start,s)){
continue;
}
//没遍历过,又能转换的,当是最后转换成endWord时
if(s.equals(endWord)){
return ++count;
}
//没遍历过,又能转换的,还没到最后的end的
//存进visited,放入队列之后搜索
visited.add(s);
queue.offer(s);
}
}
}
return 0;
}
//能否转换
public boolean canConvert(String s1,String s2){
int count=0;
for(int i=0;i<s1.length();i++){
if(s1.charAt(i)!=s2.charAt(i)){
count++;
if(count>1){
return false;
}
}
}
return true;
}
}
boolean数组要比hashmap快多了,对吧?
class Solution {
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
if(!wordList.contains(endWord)){
return 0;
}
//已经访问过的结点
boolean[] visited=new boolean[wordList.size()];
int index=wordList.indexOf(beginWord);
//假如beginword在wordList里,得标记一下
if(index!=-1){
visited[index]=true;
}
//当前要访问结点存进的队列
Queue<String> queue =new LinkedList<>();
queue.offer(beginWord);
int count=0;
while(queue.size()>0){
int size=queue.size();
++count;
//在当前要访问结点存进的队列里开始逐个检验,并且把结点的下一层放进队列里,BFS
for(int i=0;i<size;i++){
//每次都是最新要比较的单词 start
String start=queue.poll();
//因为有的单词已经遍历过,有的单词不满足转换会跳过,所以整个wordList遍历一次
for(int j=0;j<wordList.size();j++){
//已遍历过,跳过看下一个单词
if(visited[j]){
continue;
}
String s=wordList.get(j);
//不能转换一个字母变成这个单词的,也跳过
if(!canConvert(start,s)){
continue;
}
//没遍历过,又能转换的,当是最后转换成endWord时
if(s.equals(endWord)){
return ++count;
}
//没遍历过,又能转换的,还没到最后的end的
//存进visited,放入队列之后搜索
visited[j]=true;
queue.offer(s);
}
}
}
return 0;
}
//能否转换
public boolean canConvert(String s1,String s2){
int count=0;
for(int i=0;i<s1.length();i++){
if(s1.charAt(i)!=s2.charAt(i)){
count++;
if(count>1){
return false;
}
}
}
return true;
}
}
何为双向?一个从begin开始搜索,一个从end开始搜索,两头碰上了,就结束搜索。这样可以少搜索一些点。
class Solution {
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
//判断endWord是否在wordList中
int end = wordList.indexOf(endWord);
if (end == -1) {
return 0;
}
//把beginWord加在了list的末尾
wordList.add(beginWord);
int start = wordList.size() - 1;
// 用于BFS遍历的队列
Queue<Integer> queue1 = new LinkedList<>();
Queue<Integer> queue2 = new LinkedList<>();
// 用于保存已访问的单词
Set<Integer> visited1 = new HashSet<>();
Set<Integer> visited2 = new HashSet<>();
//存的是索引,不是单词
queue1.offer(start);
queue2.offer(end);
visited1.add(start);
visited2.add(end);
//前后各自的层次
int count1 = 0;
int count2 = 0;
while (!queue1.isEmpty() && !queue2.isEmpty()) {
count1++;
int size1 = queue1.size();
while (size1-- > 0) {
//队列里放的是索引
String s=wordList.get(queue1.poll());
for (int i = 0; i < wordList.size(); ++i) {
if (visited1.contains(i)) {
continue;
}
if (!canConvert(s, wordList.get(i))) {
continue;
}
if (visited2.contains(i)) {
return count1 + count2 + 1;
}
visited1.add(i);
queue1.offer(i);
}
}
count2++;
int size2 = queue2.size();
while (size2-- > 0) {
String s = wordList.get(queue2.poll());
for (int i = 0; i < wordList.size(); ++i) {
if (visited2.contains(i)) {
continue;
}
if (!canConvert(s, wordList.get(i))) {
continue;
}
if (visited1.contains(i)) {
return count1 + count2 + 1;
}
visited2.add(i);
queue2.offer(i);
}
}
}
return 0;
}
public boolean canConvert(String a, String b) {
int count = 0;
for (int i = 0; i < a.length(); ++i) {
if (a.charAt(i) != b.charAt(i)) {
if (++count > 1) return false;
}
}
return true;
}
}
优化的思路在于,每轮都从结点少的队列搜索,这样运行时间每次都偏少一点,总的来说节约的时间是非常可观的。
class Solution {
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
int end = wordList.indexOf(endWord);
if (end == -1) {
return 0;
}
wordList.add(beginWord);
int start = wordList.size() - 1;
Queue<Integer> queue1 = new LinkedList<>();
Queue<Integer> queue2 = new LinkedList<>();
Set<Integer> visited1 = new HashSet<>();
Set<Integer> visited2 = new HashSet<>();
queue1.offer(start);
queue2.offer(end);
visited1.add(start);
visited2.add(end);
int count = 0;
while (!queue1.isEmpty() && !queue2.isEmpty()) {
count++;
if (queue1.size() > queue2.size()) {
Queue<Integer> tmp = queue1;
queue1 = queue2;
queue2 = tmp;
Set<Integer> t = visited1;
visited1 = visited2;
visited2 = t;
}
int size1 = queue1.size();
while (size1-- > 0) {
String s = wordList.get(queue1.poll());
for (int i = 0; i < wordList.size(); ++i) {
if (visited1.contains(i)) {
continue;
}
if (!canConvert(s, wordList.get(i))) {
continue;
}
if (visited2.contains(i)) {
return count + 1;
}
visited1.add(i);
queue1.offer(i);
}
}
}
return 0;
}
public boolean canConvert(String a, String b) {
int count = 0;
for (int i = 0; i < a.length(); ++i) {
if (a.charAt(i) != b.charAt(i)) {
if (++count > 1) return false;
}
}
return count == 1;
}
}
这个思路很有意思,想一下,每个单词由26种英文字符组成,题目要求每次换一个字母,于是可以给出一个单词能转换的所有单词,再判断这些单词在不在wordList里。
Set allWordSet = new HashSet<>(wordList);
用Hashset代替HashMap,可以提升性能。
(HashMap、HashTable、HashSet、LinkedHashSet、TreeSet、ArrayList、LinkedList等是否傻傻分不清楚?博主之后会再写一篇关于它们的)
因为单词一般不会很长,但是有可能很多,那么这个思路就比之前遍历list每一个单词,去判断当前单词能否转换成这些单词要快得多。
class Solution {
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
int end = wordList.indexOf(endWord);
if (end == -1) {
return 0;
}
wordList.add(beginWord);
// 从两端BFS遍历要用的队列
Queue<String> queue1 = new LinkedList<>();
Queue<String> queue2 = new LinkedList<>();
// 两端已经遍历过的节点
Set<String> visited1 = new HashSet<>();
Set<String> visited2 = new HashSet<>();
queue1.offer(beginWord);
queue2.offer(endWord);
visited1.add(beginWord);
visited2.add(endWord);
int count = 0;
Set<String> allWordSet = new HashSet<>(wordList);
while (!queue1.isEmpty() && !queue2.isEmpty()) {
count++;
if (queue1.size() > queue2.size()) {
Queue<String> tmp = queue1;
queue1 = queue2;
queue2 = tmp;
Set<String> t = visited1;
visited1 = visited2;
visited2 = t;
}
int size1 = queue1.size();
while (size1-- > 0) {
String s = queue1.poll();
char[] chars = s.toCharArray();
for (int j = 0; j < s.length(); ++j) {
// 保存第j位的原始字符
char c0 = chars[j];
for (char c = 'a'; c <= 'z'; ++c) {
chars[j] = c;
String newString = new String(chars);
// 已经访问过了,跳过
if (visited1.contains(newString)) {
continue;
}
// 两端遍历相遇,结束遍历,返回count
if (visited2.contains(newString)) {
return count + 1;
}
// 如果单词在列表中存在,将其添加到队列,并标记为已访问
if (allWordSet.contains(newString)) {
queue1.offer(newString);
visited1.add(newString);
}
}
// 恢复第j位的原始字符
chars[j] = c0;
}
}
}
return 0;
}
}
看完这一篇,你是否会做BFS的题了呢?