LeetCode 127.单词接龙

292 阅读3分钟

一、题目

字典 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,这道题目很明显没有给我们图,那我们就自己建一个。
建图原则:字典中每个单词是一个节点,可以相互转换的一对单词,它们之间就有一条双向边。

示例中的情况,我们就可以建图如下:

WX20210716-183954@2x.png 基于该图,以 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节点,那么它们就可以相互转化。

示例中的情况,也可以画成下图:

WX20210716-182512@2x.png 我们将起点加入队列,开始广度优先搜索,当搜索到终点的时候,就找到了最短路径的长度。
上图我们可以得到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数量与节点数量相同
        }
    }
}

qrcode2.png