127、单词接龙
给定两个单词(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" 不在字典中,所以无法进行转换。
题解
构造无向无权图,将文物转化成两个节点的最短路径。
数据处理:将单词数组,构造成邻接表,即对于开始的单词,将其每个位置的字母通用化,即可以为任意字母,这样变化的单词是相连的。
广度优先搜索
- Map字典,通用字符串为key,具体字符串加入value列表
- 建立字符串访问Map
- 用队列存储搜索的节点
class Solution {
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
int len = beginWord.length();
Map<String, List<String>> allComboDict = new HashMap<>();
// 构造邻接表
wordList.forEach(
word -> {
for (int i = 0;i < len;i++) {
String newWord = word.substring(0, i) + '*' + word.substring(i + 1, len);
List<String> transformations = allComboDict.getOrDefault(newWord, new ArrayList<>());
transformations.add(word);
allComboDict.put(newWord,transformations);
}
});
// 广度优先搜搜辅助队列
Queue<Pair<String, Integer>> q = new LinkedList<>();
q.add(new Pair(beginWord, 1));
// 访问记录Map
Map<String, Boolean> visited = new HashMap<>();
visited.put(beginWord, true);
while(!q.isEmpty()) {
Pair<String, Integer> node = q.remove();
String word = node.getKey();
int level = node.getValue();
// 对于每个单词,访问其邻接表
for (int i = 0;i < len;i++) {
String newWord = word.substring(0,i) + '*' + word.substring(i + 1, len);
for (String adjacentWord : allComboDict.getOrDefault(newWord, new ArrayList<>())) {
// 到达目标字符串,直接退出
if (adjacentWord.equals(endWord)) {
return level + 1;
}
// 如果adjacentWord没有被访问,加入访问数组,加入队列中
if (!visited.containsKey(adjacentWord)) {
visited.put(adjacentWord, true);
q.add(new Pair(adjacentWord, level + 1));
}
}
}
}
return 0;
}
}
双向广度优先搜索
- 从两端开始广度优先搜索
class Solution {
private int len;
private Map<String, List<String>> allComboDict;
public Solution() {
this.allComboDict = new HashMap<>();
}
private int visitedWordNode(Queue<Pair<String, Integer>> q, Map<String, Integer> visited, Map<String, Integer> othersVisited) {
Pair<String, Integer> node = q.remove();
String word = node.getKey();
int level = node.getValue();
for (int i = 0;i < len;i++) {
String newWord = word.subString(0,i) + '*' + word.substring(i + 1,len);
for (String adjacentWord : allComboDict.getDefault(newWord, new ArrayList<>())) {
if (othersVisited.containsKey(adjacentWord)) {
return level + othersVisited.get(adjacentWord);
}
if (!visited.containsKey(adjacentWord)) {
visited.put(adjacentWord, level + 1);
q.add(new Pair<String, Integer>(adjacentWord, level + 1));
}
}
}
return -1;
}
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
if (!wordList.contains(endWord)) {
return 0;
}
len = beginWord.length();
wordList.forEach(
word -> {
for (int i = 0; i < len; i++) {
String newWord = word.substring(0, i) + '*' + word.substring(i + 1, len);
List<String> transformations =
this.allComboDict.getOrDefault(newWord, new ArrayList<>());
transformations.add(word);
this.allComboDict.put(newWord, transformations);
}
});
Queue<Pair<String, Integer>> qBegin = new LinkedList<>();
Queue<Pair<String, Integer>> qEnd = new LinkedList<>();
qBegin.add(new Pair(beginWord, 1));
qEnd.add(new Pair(endWord, 1));
Map<String, Integer> visitedBegin = new HashMap<>();
Map<String, Integer> visitedEnd = new HashMap<>();
visitedBegin.put(beginWord, 1);
visitedEnd.put(endWord, 1);
while (!qBegin.isEmpty() && !qEnd.isEmpty()) {
// 前端广度优先搜索
int ans = visitedWordNode(qBegin, visitedBegin, visitedEnd);
if (ans > -1) {
return ans;
}
// 后端广度优先搜索
ans = visitedWordNode(qEnd, visitedEnd, visitedBegin);
if (ans > -1) {
return ans;
}
}
return 0;
}
}
- 最快的一个实现
class Solution {
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
if (wordList == null || wordList.size() == 0) return 0;
//hashset的好处:去重也完成了
//开始端
HashSet<String> start = new HashSet<>();
//结束端
HashSet<String> end = new HashSet<>();
//所有字符串的字典
HashSet<String> dic = new HashSet<>(wordList);
start.add(beginWord);
end.add(endWord);
if (!dic.contains(endWord)) return 0;
//经历过上面的一系列判定,到这里的时候,若是有路径,则最小是2,所以以2开始
return bfs(start, end, dic, 2);
}
public int bfs(HashSet<String> st, HashSet<String> ed, HashSet<String> dic, int l) {
//双端查找的时候,若是有任意一段出现了“断裂”,也就是说明不存在能够连上的路径,则直接返回0
if (st.size() == 0) return 0;
if (st.size() > ed.size()) {
//双端查找,为了优化时间,永远用少的去找多的,比如开始的时候塞进了1000个,而结尾只有3个,则肯定是从少的那一端开始走比较好
return bfs(ed, st, dic, l);
}
//BFS的标记行为,即使用过的不重复使用
dic.removeAll(st);
//收集下一层临近点
HashSet<String> next = new HashSet<>();
for (String s : st) {
char[] arr = s.toCharArray();
for (int i = 0; i < arr.length; i++) {
char tmp = arr[i];
//变化
for (char c = 'a'; c <= 'z'; c++) {
if (tmp == c) continue;
arr[i] = c;
String nstr = new String(arr);
if (dic.contains(nstr)) {
if (ed.contains(nstr)) return l;
else next.add(nstr);
}
}
//复原
arr[i] = tmp;
}
}
return bfs(next, ed, dic, l + 1);
}
}
126、单词接龙II
给定两个单词(beginWord 和 endWord)和一个字典 wordList,找出所有从 beginWord 到 endWord 的最短转换序列。转换需遵循如下规则:
- 每次转换只能改变一个字母。
- 转换过程中的中间单词必须是字典中的单词。
说明:
- 如果不存在这样的转换序列,返回一个空列表。
- 所有单词具有相同的长度。
- 所有单词只由小写字母组成。
- 字典中不存在重复的单词。
- 你可以假设 beginWord 和 endWord 是非空的,且二者不相同。
示例 1:
输入:
beginWord = "hit",
endWord = "cog",
wordList = ["hot","dot","dog","lot","log","cog"]
输出:
[
["hit","hot","dot","dog","cog"],
["hit","hot","lot","log","cog"]
]
示例 2:
输入:
beginWord = “hit”
endWord = “cog”
wordList = [“hot”,”dot”,”dog”,”lot”,”log”]
输出: []
解释: endWord “cog” 不在字典中,所以不存在符合要求的转换序列。
题解
1、广度优先搜索
class Solution {
private static final int INF = 1 << 20;
private Map<String, Integer> wordId; // 单词到id的映射
private ArrayList<String> idWord; // id到单词的映射
private ArrayList<Integer>[] edges; // 图的边
public Solution() {
wordId = new HashMap<>();
idWord = new ArrayList<>();
}
public List<List<String>> findLadders(String beginWord, String endWord, List<String> wordList) {
int id = 0;
// 将wordList所有单词加入wordId中 相同的只保留一个 // 并为每一个单词分配一个id
for (String word : wordList) {
if (!wordId.containsKey(word)) {
wordId.put(word, id++);
idWord.add(word);
}
}
// 若endWord不在wordList中 则无解
if (!wordId.containsKey(endWord)) {
return new ArrayList<>();
}
// 把beginWord也加入wordId中
if (!wordId.containsKey(beginWord)) {
wordId.put(beginWord, id++);
idWord.add(beginWord);
}
// 初始化存边用的数组
edges = new ArrayList[idWord.size()];
for (int i = 0; i < idWord.size(); i++) {
edges[i] = new ArrayList<>();
}
// 添加边
for (int i = 0; i < idWord.size(); i++) {
for (int j = i + 1; j < idWord.size(); j++) {
// 若两者可以通过转换得到 则在它们间建一条无向边
if (transformCheck(idWord.get(i), idWord.get(j))) {
edges[i].add(j);
edges[j].add(i);
}
}
}
int dest = wordId.get(endWord); // 目的ID
List<List<String>> res = new ArrayList<>(); // 存答案
int[] cost = new int[id]; // 到每个点的代价
for (int i = 0; i < id; i++) {
cost[i] = INF; // 每个点的代价初始化为无穷大
}
// 将起点加入队列 并将其cost设为0
Queue<ArrayList<Integer>> q = new LinkedList<>();
ArrayList<Integer> tmpBegin = new ArrayList<>();
tmpBegin.add(wordId.get(beginWord));
q.add(tmpBegin);
cost[wordId.get(beginWord)] = 0;
// 开始广度优先搜索
while (!q.isEmpty()) {
ArrayList<Integer> now = q.poll();
int last = now.get(now.size() - 1); // 最近访问的点
if (last == dest) { // 若该点为终点则将其存入答案res中
ArrayList<String> tmp = new ArrayList<>();
for (int index : now) {
tmp.add(idWord.get(index)); // 转换为对应的word
}
res.add(tmp);
} else { // 该点不为终点 继续搜索
for (int i = 0; i < edges[last].size(); i++) {
int to = edges[last].get(i);
// 此处<=目的在于把代价相同的不同路径全部保留下来
if (cost[last] + 1 <= cost[to]) {
cost[to] = cost[last] + 1;
// 把to加入路径中
ArrayList<Integer> tmp = new ArrayList<>(now); tmp.add(to);
q.add(tmp); // 把这个路径加入队列
}
}
}
}
return res;
}
// 两个字符串是否可以通过改变一个字母后相等
boolean transformCheck(String str1, String str2) {
int differences = 0;
for (int i = 0; i < str1.length() && differences < 2; i++) {
if (str1.charAt(i) != str2.charAt(i)) {
++differences;
}
}
return differences == 1;
}
}
2、双向广度优先搜索
- 耗时最短
class Solution {
public List<List<String>> findLadders(String beginWord, String endWord, List<String> wordList) {
// 结果集
List<List<String>> res = new ArrayList<>();
Set<String> words = new HashSet<>(wordList);
// 字典中不包含目标单词
if (!words.contains(endWord)) {
return res;
}
// 存放关系:每个单词可达的下层单词
Map<String, List<String>> mapTree = new HashMap<>();
Set<String> begin = new HashSet<>(), end = new HashSet<>();
begin.add(beginWord);
end.add(endWord);
if (buildTree(words, begin, end, mapTree, true)) {
dfs(res, mapTree, beginWord, endWord, new LinkedList<>());
}
return res;
}
// 双向BFS,构建每个单词的层级对应关系
private boolean buildTree(Set<String> words, Set<String> begin, Set<String> end, Map<String, List<String>> mapTree, boolean isFront){
if (begin.size() == 0) {
return false;
}
// 始终以少的进行探索
if (begin.size() > end.size()) {
return buildTree(words, end, begin, mapTree, !isFront);
}
// 在已访问的单词集合中去除
words.removeAll(begin);
// 标记本层是否已到达目标单词
boolean isMeet = false;
// 记录本层所访问的单词
Set<String> nextLevel = new HashSet<>();
for (String word : begin) {
char[] chars = word.toCharArray();
for (int i = 0; i < chars.length; i++) {
char temp = chars[i];
for (char ch = 'a'; ch <= 'z'; ch++) {
chars[i] = ch;
String str = String.valueOf(chars);
if (words.contains(str)) {
nextLevel.add(str);
// 根据访问顺序,添加层级对应关系:始终保持从上层到下层的存储存储关系
// true: 从上往下探索:word -> str
// false: 从下往上探索:str -> word(查找到的 str 是 word 上层的单词)
String key = isFront ? word : str;
String nextWord = isFront ? str : word;
// 判断是否遇见目标单词
if (end.contains(str)) {
isMeet = true;
}
if (!mapTree.containsKey(key)) {
mapTree.put(key, new ArrayList<>());
}
mapTree.get(key).add(nextWord);
}
}
chars[i] = temp;
}
}
if (isMeet) {
return true;
}
return buildTree(words, nextLevel, end, mapTree, isFront);
}
// DFS: 组合路径
private void dfs (List<List<String>> res, Map<String, List<String>> mapTree, String beginWord, String endWord, LinkedList<String> list) {
list.add(beginWord);
if (beginWord.equals(endWord)) {
res.add(new ArrayList<>(list));
// list.removeLast();
// return;
}
if (mapTree.containsKey(beginWord)) {
for (String word : mapTree.get(beginWord)) {
dfs(res, mapTree, word, endWord, list);
}
}
list.removeLast();
}
}
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1056615746@qq.com