一、题目
字典 wordList 中从单词 beginWord 和 endWord 的 转换序列 是一个按下述规格形成的序列:
- 序列中第一个单词是 beginWord 。
- 序列中最后一个单词是 endWord 。
- 每次转换只能改变一个字母。
- 转换过程中的中间单词必须是字典 wordList 中的单词。 给你两个单词 beginWord 和 endWord 和一个字典 wordList ,找到从 beginWord 到 endWord 的 最短转换序列 中的 单词数目 。如果不存在这样的转换序列,返回 0。
示例:
输入:beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"]
输出:5
解释:一个最短转换序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog", 返回它的长度 5。
二、思路
首先能想到的就是将字典中的每一对单词进行比较,如果它们相差一个字符,就代表它们可以相互转换。
要找到最短转换序列,能想到的就是图论中的广度优先搜索BFS,这道题目很明显没有给我们图,那我们就自己建一个。
建图原则:字典中每个单词是一个节点,可以相互转换的一对单词,它们之间就有一条双向边。
示例中的情况,我们就可以建图如下:
基于该图,以 beiginWord 为起点,以 endWord 为终点,进行广度优先搜索,就可以得到最短路径。
从中我们可以很轻易看出
hit->hot->dot->dog->cog就是其中一个最短换换序列。
有一个问题,这张图画起来简单,但是实际上我们处理的时候,是需要枚举每一对单词组合,判断他们是否恰好相差一个字符,很明显耗时比较长。
如果字典长度较大(1 <= wordList.length <= 5000),例如wordList.length=5000,我们就需要比较5000x4999/2=12497500次,在建图上就花费了很多时间。
三、优化
我们不再去比较每一对组合,而是采用虚拟辅助节点的办法。
例如对于单词 hit,我们可以创建三个虚拟节点*it,h*t,hi*,并且让这三个虚拟节点与单词 hit 相连(双向边)。
如果一个单词能够转化为 hit,那么它必然会连接到这三个节点之一。
同样,对于单词 hot,也有三个虚拟节点*ot,h*t,ho*,并且让这三个虚拟节点与单词 hot 相连。
从中可以看出 hot 和 hit 都连接了h*t节点,那么它们就可以相互转化。
示例中的情况,也可以画成下图:
我们将起点加入队列,开始广度优先搜索,当搜索到终点的时候,就找到了最短路径的长度。
上图我们可以得到hit -> h*t -> hot -> *ot -> dot -> do* -> dog -> *og -> cog就是其中的一个长度为8的最短转换序列。
但是因为我们加入了虚拟节点的原因,任何两个单词都不是直接相连的,实际的距离应该除以2。
题目中要求的是找到从 beginWord 到 endWord 的最短转换序列中的单词数目,所以我们应当返回距离的一半再加一的结果。
四、代码
public class Problem127 {
//为了方便,我们将每个单词都给定一个id
HashMap<String, Integer> wordIdMap=new HashMap<>();
//初始化节点数量
int nodeNum=0;
//对于每个节点,都有一个List来存储与它相连的节点,就是一条条边
List<ArrayList<Integer>> edges=new ArrayList<>();
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
//构建图,图是由节点和边来组成的
for(String word:wordList) {
addEdge(word);
}
addEdge(beginWord);
if(!wordIdMap.containsKey(endWord))
return 0;
//进行BFS
Deque<Integer> queue=new LinkedList<>();
//记录遍历的每个节点离beiginWord的距离
int[] dis=new int[nodeNum];
Arrays.fill(dis, Integer.MAX_VALUE);
//id与每个单词相对应,dis[],edges也都是通过id来记录的
int beginId=wordIdMap.get(beginWord);
int endId=wordIdMap.get(endWord);
queue.offer(beginId);
dis[beginId]=0;
while(!queue.isEmpty()) {
int pollId=queue.poll();
if(pollId==endId) {
return dis[endId]/2+1;
}
//将与“出列”单词相连的其他单词“入列”
for(int id:edges.get(pollId)) {
if(dis[id]==Integer.MAX_VALUE) {
//还没遍历过这个节点
queue.offer(id);
dis[id]=dis[pollId]+1;
}
}
}
return 0;
}
public void addEdge(String word) {
addNode(word);
int id1=wordIdMap.get(word);
char[] array=word.toCharArray();
for(int i=0;i<word.length();i++) {
char temp=array[i];
array[i]='*';
String newWord=new String(array);
addNode(newWord);
int id2=wordIdMap.get(newWord);
//将每个单词与它的虚拟节点相连
edges.get(id1).add(id2);
edges.get(id2).add(id1);
array[i]=temp;
}
}
public void addNode(String word) {
//还未存储过这个节点,我们才将它添加进去
if(!wordIdMap.containsKey(word)) {
//每个单词都对应一个id
wordIdMap.put(word, nodeNum++);
//每个单词都对应一个ArrayList,来存储与它相连的单词
edges.add(new ArrayList<>());
//保证了edges数量与节点数量相同
}
}
}