给定两个单词(beginWord 和 endWord)和一个字典,找到从 beginWord 到 endWord 的最短转换序列的长度。转换需遵循如下规则:
- 每次转换只能改变一个字母。
- 转换过程中的中间单词必须是字典中的单词。
说明:
- 如果不存在这样的转换序列,返回 0。
- 所有单词具有相同的长度。
- 所有单词只由小写字母组成。
- 字典中不存在重复的单词。
- 你可以假设 beginWord 和 endWord 是非空的,且二者不相同。
这道题没思路,看了weiwei哥的题解学会的,这道题用到bfs+回溯,优化用了双向bfs,又学到新知识,赚到了。
weiwei哥的题解:https://leetcode-cn.com/problems/word-ladder/solution/yan-du-you-xian-bian-li-shuang-xiang-yan-du-you-2/
解法一
bfs + 回溯
class Solution {
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
// bfs + 回溯
// 把wordList存到wordSet中,如果wordSet为空,或者不存在endWord,返回0
Set<String> wordSet = new HashSet<>(wordList);
if (wordSet.size() == 0 || !wordSet.contains(endWord)) {
return 0;
}
// 移除wordSet中的beginWord,如果有的话
wordSet.remove(beginWord);
// bfs使用队列,访问过的单词需要保存到visited中
// 创建队列,并把beginWord保存到队列中,起始位
Queue<String> queue = new LinkedList<>();
queue.offer(beginWord);
// 创建名为visited的set,用于保存已访问的单词
Set<String> visited = new HashSet<String>();
visited.add(beginWord);
// 转换次数从1开始,因为题意中说明beginWord和endWord是不同的
int step = 1;
while (!queue.isEmpty()) {
// 逐层遍历
int size = queue.size();
for (int i = 0; i < size; i++) {
// 如果当前单词转换后为endWord,那么返回转换次数加1
String word = queue.poll();
if (changeWordEveryOneLetter(word, endWord, queue, visited, wordSet)) {
return step + 1;
}
}
step++;
}
return 0;
}
// 回溯
// 用回溯去改变当前单词,如果改变后为endWord,返回true
private boolean changeWordEveryOneLetter(String word, String endWord, Queue<String> queue, Set<String> visited, Set<String> wordSet) {
// 把当前单词转为char数组,遍历修改每个字符
char[] ch = word.toCharArray();
for (int i = 0; i < ch.length; i++) {
// 保存当前单词,用于跳过和回溯使用
char c = ch[i];
// 每个字符从a修改到z
for (char k = 'a'; k <= 'z'; k++) {
// 如果与当前字符相同,跳过
if (k == c) {
continue;
}
// 修改字符
ch[i] = k;
// 如果转换后的单词存在wordSet中
String changeWord = String.valueOf(ch);
if (wordSet.contains(changeWord)) {
// 如果转换后的单词为endWord,返回true
if (changeWord.equals(endWord)) {
return true;
}
// 如果转换后的单词不在visited中,保存到队列中,并保存到visited中
if (!visited.contains(changeWord)) {
queue.offer(changeWord);
visited.add(changeWord);
}
}
}
// 恢复,变为原来的值
ch[i] = c;
}
return false;
}
}
解法二
双向bfs + 回溯,双向bfs把只有一个结果变为一个结果集,效率更高,最优解。
class Solution {
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
// 双向bfs + 回溯
// 把wordList存到wordSet中,如果wordSet为空,或者不存在endWord,返回0
Set<String> wordSet = new HashSet<>(wordList);
if (wordSet.size() == 0 || !wordSet.contains(endWord)) {
return 0;
}
// 创建名为visited的set,用于保存已访问的单词
Set<String> visited = new HashSet<String>();
// 用两个set代替队列,这里我觉得用work和idle来命名比较好,weiwei哥那么命名我觉得会难理解些
// 因为两个会交换,只有一个在工作中,beginVisited和endVisited命名会难理解它们会交换
Set<String> work = new HashSet<String>();
work.add(beginWord);
Set<String> idle = new HashSet<String>();
idle.add(endWord);
int step = 1;
while (!work.isEmpty() && !idle.isEmpty()) {
// 需要保证每个进行替换的set个数是最少的,减少运算次数
if (work.size() > idle.size()) {
Set<String> temp = work;
work = idle;
idle = temp;
}
// 符合条件的单词会保存到newSet
Set<String> newSet = new HashSet<>();
for (String word : work) {
if (changeWordEveryOneLetter(word, idle, visited, wordSet, newSet)) {
return step + 1;
}
}
// 替换为新的set,因为work中的单词都访问过了
work = newSet;
step++;
}
return 0;
}
// 用回溯去改变当前单词,如果改变后的单词存在于idle中,返回true
private boolean changeWordEveryOneLetter(String word, Set<String> idle,
Set<String> visited, Set<String> wordSet,
Set<String> newSet) {
// 把当前单词转为char数组,遍历修改每个字符
char[] ch = word.toCharArray();
for (int i = 0; i < ch.length; i++) {
// 保存当前单词,用于跳过和回溯使用
char c = ch[i];
// 每个字符从a修改到z
for (char k = 'a'; k <= 'z'; k++) {
// 如果与当前字符相同,跳过
if (k == c) {
continue;
}
// 修改字符
ch[i] = k;
// 如果转换后的单词存在wordSet中
String changeWord = String.valueOf(ch);
if (wordSet.contains(changeWord)) {
// 如果转换后的单词为endWord,返回true
if (idle.contains(changeWord)) {
return true;
}
// 如果转换后的单词不在visited中,保存到newSet中,并保存到visited中
if (!visited.contains(changeWord)) {
newSet.add(changeWord);
visited.add(changeWord);
}
}
}
// 恢复,变为原来的值
ch[i] = c;
}
return false;
}
}