9-今日一扣(leetcode)-227-广度优先搜索BFS/双向广度优先搜索*-M

题目:单词接龙

Leetcode
给定两个单词(beginWord 和 endWord)和一个字典,找到从 beginWord 到 endWord 的最短转换序列的长度。转换需遵循如下规则:

每次转换只能改变一个字母。
转换过程中的中间单词必须是字典中的单词。

说明:

如果不存在这样的转换序列,返回 0。
所有单词具有相同的长度。
所有单词只由小写字母组成。
字典中不存在重复的单词。
你可以假设 beginWord 和 endWord 是非空的,且二者不相同。

在这里插入图片描述

解决方案

广度优先搜索BFS

每次只能改变一个字符,且等长。那么每改变一个字符,就会产生一个新的单词,而这个单词又可以继续一个字符,直到与目标单词相同。显然这里就构成了一个单词的搜索树。搜索树的深度就是我们需要改变的次数。题目想要求最少的转换次数,我们应该使用广度优先遍历。

易得一下代码:

class Solution {
    public int ladderLength(String beginWord, String endWord, List<String> wordList) {
        //BFS
        //队列,同一层排除相同的
        Set<String> queue = new HashSet<> ();
        queue.add(beginWord);
        Set<String> visited = new HashSet<>();
        // 层数
        int level = 0;
        while(!queue.isEmpty()){
            level ++;
            Set<String> nextQueue = new HashSet<>();
            // 遍历这一层
            for(String str:queue){
                visited.add(str);
                int len = str.length();
                //如果就是目标字符串,就直接返回结果
                if (str.equals(endWord)){
                    return level;
                }
                //遍历每个位置字母
                for(int i=0; i<len; i++){
                	// 从字典中选取可以转换的单词。
                	//注意这里要优化!!!
                    for(String word:wordList){
                        //排除自身
                        if(visited.contains(word)){
                            continue;
                        }
                        // 变更一个字符后可能的结果
                        if(isMatch(i,str, word)){
                            nextQueue.add(word);
                        }
                    }
                }
            }
            queue = nextQueue;       
        }
        return 0;
    }
    
    //判断某个单词除了位置i不一样外,其他是否相同。
    // 即两个单词能否转换。
    private boolean isMatch(int pos, String src, String tar){
        int len = src.length();
        for(int i = 0 ; i<len; i++){
            if(i==pos){
                continue;
            }else if(src.charAt(i)!=tar.charAt(i)){
                return false;
            }
        }
        return true;
    }
}

注意:

  • 四层循环嵌套,这显然不是我们希望的到的时间复杂度。
  • 而事实上,这个算法会超出时间限制。这个的问题在于在对某个查找某个单词的第i个位置转换时,都需要遍历一次字典。字典越长、单词越长,所需要的便利时间便上升。
  • 对访问过的单词要记录到访问队列中,否则在无解的时候(不能通过return跳出循环),可能形成一个环无限循环、

优化
对word list只进行一次遍历,然后把具有通用性的单词放到一起。

class Solution {
    public int ladderLength(String beginWord, String endWord, List<String> wordList) {
        //字典预处理
        int len = beginWord.length();
        // 通配符的字典
        /*https://leetcode-cn.com/problems/word-ladder/solution/dan-ci-jie-long-by-leetcode/*/
        Map<String, List<String>> allComboDict = new HashMap<>();
        wordList.forEach(
        word -> {
          for (int i = 0; i < len; i++) {
            // *ot  *代表通配符,构造key
            String newWord = word.substring(0, i) + '*' + word.substring(i + 1, len);
            // 如果map里存在,就把队列拿出来,否则就创建一个。
            List<String> transformations = allComboDict.getOrDefault(newWord, new ArrayList<>());
            // 添加新的单词
            transformations.add(word);
            allComboDict.put(newWord, transformations);
          }
        });

        //BFS
        //队列,同一层排除相同的
        Set<String> queue = new HashSet<> ();
        queue.add(beginWord);
        // 记录已经访问过的单词,这样能够大大降低冗余操作。
        // 不是用这个超过时间限制或者形成环。无限循环
        Set<String> visited = new HashSet<>();
        // 层数
        int level = 0;
        while(!queue.isEmpty()){
            level ++;
            Set<String> nextQueue = new HashSet<>();
            // 遍历这一层
            for(String str:queue){
                //如果就是目标字符串,就直接返回结果
                if (str.equals(endWord)){
                    return level;
                }
                visited.add(str);
                //遍历每个位置字母
                for(int i=0; i<len; i++){
                    // 获取key
                    String key = str.substring(0, i) + '*' + str.substring(i + 1, len);
                    // 拿出统配的list
                    List<String> tarWordList = allComboDict.getOrDefault(key, new ArrayList<>());
                    // 遍历list
                    for (String word : tarWordList) {
                        // 如果就是当前的单词,则不加入下一层的队列中
                        if(visited.contains(word)){
                            continue;
                        }else{
                            nextQueue.add(word);
                        }
                    }
                }
            }
            queue = nextQueue;       
        }
        return 0;
    }
    
}

双向广度优先搜索

在这里插入图片描述


class Solution {
	private Map<String, List<String>> allComboDict = new HashMap<String, List<String>>();
	private int len;

	public int ladderLength(String beginWord, String endWord, List<String> wordList) {
		// 两个访问队列, 每个节点是word和level
		Queue<Pair<String, Integer>> beginQueue = new LinkedList<>();
		Queue<Pair<String, Integer>> endQueue = new LinkedList<>();

		// 已访问队列, 同时记录第几层访问了
		Map<String, Integer> beginVisited = new HashMap<>();
		Map<String, Integer> endVisited = new HashMap<>();
        //注意放置
        beginVisited.put(beginWord,1);
        endVisited.put(endWord,1);
		// 预处理wordList

		this.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 = allComboDict.getOrDefault(newWord, new ArrayList<>());
				transFormations.add(word);
				allComboDict.put(newWord, transFormations);
			}
		});

		// 添加起始结点.由于begin和end一定不同,故至少一次转换。
		beginQueue.add(new Pair<String, Integer>(beginWord, 1));
		endQueue.add(new Pair<String, Integer>(endWord, 1));

        // 注意:在双向BFS,即使endWord不在字典,仍可能通过变换一个字符和字典中出现的单词相同
        // 这样就会和beginWord相遇。所以要先排除endWord不可达的情况。
        if (!wordList.contains(endWord)) {
            return 0;
        }

		while (!beginQueue.isEmpty() && !endQueue.isEmpty()) {
			int ans = visitQueue(beginQueue, beginVisited, endVisited);
			if (ans > -1) {
				// 找到结果
				return ans;
			}
			ans = visitQueue(endQueue, endVisited, beginVisited);
			if (ans > -1) {
				// 找到结果
				return ans;
			}
		}
		return 0;
	}

	private int visitQueue(Queue<Pair<String, Integer>> queue, Map<String, Integer> visited1,
			Map<String, Integer> visited2) {
		int size = queue.size();
		// 获取节点
		Pair<String, Integer> curPair = queue.poll();
		// 获取单词
		String curWord = curPair.getKey();
		// 获取层级
		int level = curPair.getValue();
		

		for (int i = 0; i < this.len; i++) {
			// 拼接成通用Map的key
			String keyWord = curWord.substring(0, i) + '*' + curWord.substring(i + 1, this.len);

			// 遍历
			for (String word : allComboDict.getOrDefault(keyWord, new ArrayList<>())) {
				// 另一个BFS已经访问过该节点
				if (visited2.containsKey(word)) {
					// 返回curWord的层级+另一个bfs遍历的层
					return level + visited2.get(word);
				}
				// 本BFS没有访问过该节点
				if (!visited1.containsKey(word)) {
                    // 添加到访问中
		            visited1.put(word, level+1);
					queue.add(new Pair(word, level + 1));
				}
			}
		}
		return -1;
	}
}

注意:

  • 队列是节点里面包含了层级信息,所以就不需要像之前一样使用size来确定遍历某一层了。
  • 注意已访问队列的放置时机。将要访问的节点放入队列的同时,就要放到visited队列中。
  • 不要等着出队之后再放到visited节点中。例如1好节点出队,放入visited1队列中,然后2号节点出队,放入visited2中。接着3号节点,在4号节点时,发现要加入访问队列的a在visited1中包含了,这时返回的结果是3+4=7,而不是6。 也就是说在同层出现是不敏感,需要单独判断一下。
  • 在这里插入图片描述
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值