力扣面试150题--单词接龙

Day 66

题目描述

思路

初次做法

直接把上一题最小基于变化的代码拿来修改后使用

java 复制代码
class Solution {
    public int ladderLength(String beginWord, String endWord, List<String> wordList) {
          Map<String,Integer> tes=new HashMap<String,Integer>();
         if(wordList.size()==0){
            if(beginWord.equals(endWord)){
                return 1;
            }
            return -1;
        }
        int min=10000;
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < wordList.size(); i++) {
            tes.put(wordList.get(i),i);//添加bank和i的映射
            list.add(wordList.get(i));
        }
        //单独处理startgene
        int start=0;
        int end=0;
        if(tes.containsKey(endWord)){
            end=tes.get(endWord);
        }
        else{
            return 0;
        }
        if(tes.containsKey(beginWord)){
            start=tes.get(beginWord);
        }
        else{
            tes.put(beginWord,list.size());
            start=list.size();
            list.add(beginWord);
        }
        boolean[]visited=new boolean[list.size()];//访问数组
        if(start==end){
            return 0;//说明就一个基因
        }
        else{
            //构建graph
            int[][] graph=new int[list.size()][list.size()];
            for (int i=0;i<list.size();i++){
                for(int j=i;j<list.size();j++){
                    String a=list.get(i);
                    String b=list.get(j);
                    int x=tes.get(a);
                    int y=tes.get(b);
                    int change=0;
                    for (int k=0;k<a.length();k++){
                        if(a.charAt(k)!=b.charAt(k)){
                            change++;
                        }
                        if(change>1){//说明一步到不了
                            break;
                        }
                    }
                    if(change==1){
                        graph[x][y]=1;
                        graph[y][x]=1;
                    }
                    else{
                        graph[x][y]=10;
                        graph[y][x]=10;
                    }
                }
            }
            for (int j = 0; j < graph.length; j++) {
                graph[j][j]=0;
            }
            //构造完成
            Queue<int[]>res=new LinkedList<int[]>();
            res.add(new int[]{start,0});
            while(!res.isEmpty()){
                int[] temp=res.poll();
                int x=temp[0];
                int y=temp[1];
                visited[x]=true;//访问过了
                for(int k=0;k<graph.length;k++){
                    if(graph[x][k]==1&&!visited[k]){//说明一步能达到
                        if(k!=end){
                            res.offer(new int[]{k,y+1});
                        }
                        else{
                            if(y+1<min){
                                min=y+1;
                            }
                        }
                    }
                }
            }
        }
        if(min!=10000){
            return min+1;
        }
        return 0;//变化不了
    }
    }

结果出现问题了,发现超时了,经过分析后,发现在于构造graph时,每个单词进行两两比较,花费时间过多,因此针对这个点进行优化。
优化后思路

分析一下,我们进行两两比较字符串的目的是为了找到能一步到达的字符串,那么换种思路,对于一个单词而言,从头到尾修改任意一个字母,都算一步能到达的字符串,于是想到了通配符。
优化 :传统方法需要比较 任意两个单词,时间复杂度为 O (n²L)。而通配符预处理通过哈希表快速匹配相邻单词,将时间复杂度降低到 O (n L²)。

举个例子单词 "hit",可以生成 3 种通配符模式:

!it(替换第 1 个字符为 ! )

h!t(替换第 2 个字符为 !)

hi!(替换第 3 个字符为 !)

于是产生以下做法:

  1. 对于每个单词,生成所有可能的通配符模式(每个位置替换为 *)。
  2. 使用哈希表 wildcardMap 记录每个模式对应的单词列表。
  3. 遍历 wildcardMap 中的每个模式,将共享同一模式的单词两两连接。
java 复制代码
class Solution {
    public List<String> generateWildcardPatterns(String word) {//生成通配符
        List<String> patterns = new ArrayList<>();
        char[] chars = word.toCharArray();
        for (int i = 0; i < chars.length; i++) {
            char original = chars[i];
            chars[i] = '*';
            patterns.add(new String(chars));
            chars[i] = original; // 恢复原字符
        }
        return patterns;
    }
    public int ladderLength(String beginWord, String endWord, List<String> wordList) {
        Map<String,Integer> tes=new HashMap<String,Integer>();
         if(wordList.size()==0){
            if(beginWord.equals(endWord)){
                return 1;
            }
            return -1;
        }
        int min=10000;
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < wordList.size(); i++) {
            tes.put(wordList.get(i),i);//添加bank和i的映射
            list.add(wordList.get(i));
        }
        //单独处理startgene
        int start=0;
        int end=0;
        if(tes.containsKey(endWord)){
            end=tes.get(endWord);
        }
        else{
            return 0;
        }
        if(tes.containsKey(beginWord)){
            start=tes.get(beginWord);
        }
        else{
            tes.put(beginWord,list.size());
            start=list.size();
            list.add(beginWord);
        }
        boolean[]visited=new boolean[list.size()];//访问数组
        if(start==end){
            return 0;//说明就一个基因
        }
        else{
            //构建graph
            int[][] graph=new int[list.size()][list.size()];
             // 2. 预处理通配符模式
            Map<String, List<Integer>> wildcardMap = new HashMap<>();
            for (int i = 0; i < list.size(); i++) {
            String word = list.get(i);
            for (String pattern : generateWildcardPatterns(word)) {
                wildcardMap.computeIfAbsent(pattern, k -> new ArrayList<>()).add(i);//在 Map 中查找或创建一个列表,并向列表添加元素
            }
            }
            // 3. 构建邻接矩阵(基于通配符模式)
            for (List<Integer> group : wildcardMap.values()) {
                for (int i = 0; i < group.size(); i++) {
                int u = group.get(i);
                    for (int j = i + 1; j < group.size(); j++) {
                    int v = group.get(j);
                    graph[u][v] = 1; // 无向图,双向标记
                    graph[v][u] = 1;
                    }
                }
            }
            //构造完成
            Queue<int[]>res=new LinkedList<int[]>();
            res.add(new int[]{start,0});
            while(!res.isEmpty()){
                int[] temp=res.poll();
                int x=temp[0];
                int y=temp[1];
                visited[x]=true;//访问过了
                for(int k=0;k<graph.length;k++){
                    if(graph[x][k]==1&&!visited[k]){//说明一步能达到
                        if(k!=end){
                            res.offer(new int[]{k,y+1});
                        }
                        else{
                            return y+2;
                        }
                    }
                }
            }
        }
        if(min!=10000){
            return min+1;
        }
        return 0;//变化不了
    }
    }

虽然通过了但是时间复杂度还是很高
题解思路

双向bfs算法,在之前代码的基础上,不仅从起点往终点搜索,同时从终点向起始搜索,记录变化的距离,直到两者变化到同一种情况,将距离相加即可,

java 复制代码
class Solution {
    // 生成单词的所有通配符模式(例如:"hit" -> ["*it", "h*t", "hi*"])
    public List<String> generateWildcardPatterns(String word) {
        List<String> patterns = new ArrayList<>();
        char[] chars = word.toCharArray();
        for (int i = 0; i < chars.length; i++) {
            char original = chars[i];
            chars[i] = '*';
            patterns.add(new String(chars));
            chars[i] = original; // 恢复原字符
        }
        return patterns;
    }
    
    public int ladderLength(String beginWord, String endWord, List<String> wordList) {
        // 检查终点是否存在于字典中,不存在则无法转换
        if (!wordList.contains(endWord)) {
            return 0;
        }
        
        // 预处理通配符模式
        Map<String, List<Integer>> wildcardMap = new HashMap<>();
        List<String> allWords = new ArrayList<>(wordList);
        // 确保起点在单词列表中
        if (!allWords.contains(beginWord)) {
            allWords.add(beginWord);
        }
        
        // 为每个单词生成通配符模式并记录映射关系
        for (int i = 0; i < allWords.size(); i++) {
            String word = allWords.get(i);
            for (String pattern : generateWildcardPatterns(word)) {
                // 将单词索引添加到对应通配符模式的列表中
                wildcardMap.computeIfAbsent(pattern, k -> new ArrayList<>()).add(i);
            }
        }
        
        // 构建邻接矩阵(表示单词间的连接关系)
        int n = allWords.size();
        int[][] graph = new int[n][n];
        // 遍历每个通配符模式,将共享同一模式的单词两两连接
        for (List<Integer> group : wildcardMap.values()) {
            for (int i = 0; i < group.size(); i++) {
                int u = group.get(i);
                for (int j = i + 1; j < group.size(); j++) {
                    int v = group.get(j);
                    graph[u][v] = 1; // 无向图,双向标记
                    graph[v][u] = 1;
                }
            }
        }
        
        // 获取起点和终点在单词列表中的索引
        int start = allWords.indexOf(beginWord);
        int end = allWords.indexOf(endWord);
        
        // 双向BFS初始化
        
        // 正向搜索相关数据结构
        boolean[] visitedBegin = new boolean[n]; // 记录正向已访问节点
        int[] distanceBegin = new int[n]; // 记录从起点到各节点的距离
        Arrays.fill(distanceBegin, Integer.MAX_VALUE); // 初始化为无穷大
        distanceBegin[start] = 0; // 起点距离为0
        visitedBegin[start] = true; // 标记起点已访问
        Queue<Integer> queueBegin = new LinkedList<>(); // 正向BFS队列
        queueBegin.offer(start); // 起点入队
        
        // 反向搜索相关数据结构
        boolean[] visitedEnd = new boolean[n]; // 记录反向已访问节点
        int[] distanceEnd = new int[n]; // 记录从终点到各节点的距离
        Arrays.fill(distanceEnd, Integer.MAX_VALUE); // 初始化为无穷大
        distanceEnd[end] = 0; // 终点距离为0
        visitedEnd[end] = true; // 标记终点已访问
        Queue<Integer> queueEnd = new LinkedList<>(); // 反向BFS队列
        queueEnd.offer(end); // 终点入队
        
        // 双向BFS搜索
        while (!queueBegin.isEmpty() && !queueEnd.isEmpty()) {
            // 从起点扩展一层
            
            // 保存当前层的节点数量
            int sizeBegin = queueBegin.size();
            for (int i = 0; i < sizeBegin; i++) {
                int curr = queueBegin.poll();
                
                // 检查是否与反向搜索相遇
                if (visitedEnd[curr]) {
                    // 返回总距离(正向距离 + 反向距离 + 1)
                    // +1是因为两个方向都计算了自己的步数
                    return distanceBegin[curr] + distanceEnd[curr] + 1;
                }
                
                // 扩展当前节点的所有邻居
                for (int neighbor = 0; neighbor < n; neighbor++) {
                    if (graph[curr][neighbor] == 1 && !visitedBegin[neighbor]) {
                        distanceBegin[neighbor] = distanceBegin[curr] + 1; // 更新距离
                        visitedBegin[neighbor] = true; // 标记已访问
                        queueBegin.offer(neighbor); // 邻居入队
                    }
                }
            }
            
            // 从终点扩展一层
            
            // 保存当前层的节点数量
            int sizeEnd = queueEnd.size();
            for (int i = 0; i < sizeEnd; i++) {
                int curr = queueEnd.poll();
                
                // 检查是否与正向搜索相遇
                if (visitedBegin[curr]) {
                    // 返回总距离
                    return distanceBegin[curr] + distanceEnd[curr] + 1;
                }
                
                // 扩展当前节点的所有邻居
                for (int neighbor = 0; neighbor < n; neighbor++) {
                    if (graph[curr][neighbor] == 1 && !visitedEnd[neighbor]) {
                        distanceEnd[neighbor] = distanceEnd[curr] + 1; // 更新距离
                        visitedEnd[neighbor] = true; // 标记已访问
                        queueEnd.offer(neighbor); // 邻居入队
                    }
                }
            }
        }
        
        return 0; // 无法从起点转换到终点
    }
}
相关推荐
西京刀客26 分钟前
Go语言json.Marshal多态机制
算法·golang·json
wusixuan1310041 小时前
最大闭合子图学习笔记 / P2805 [NOI2009] 植物大战僵尸
笔记·学习·算法·最大闭合子图
独行soc1 小时前
2025年渗透测试面试题总结-字节跳动[实习]安全研发员(题目+回答)
linux·科技·安全·面试·职场和发展·渗透测试
努力学习的明2 小时前
高并发场景下接口安全实现方案:全方位构建防护体系
面试·高并发·接口安全
孟大本事要学习2 小时前
算法第15天:继续二叉树|前序递归+回溯与前序递归的场景总结、最大二叉树、合并二叉树、二叉搜索树中的搜索、验证二叉搜索树
算法
GalaxyPokemon2 小时前
LeetCode - 76. 最小覆盖子串
运维·服务器·数据结构·算法·leetcode
手握风云-3 小时前
动态规划算法的欢乐密码(二):路径问题
算法·动态规划
前端小巷子3 小时前
IndexedDB:浏览器端的强大数据库
前端·javascript·面试
Raven100863 小时前
L1G2-OpenCompass 评测书生大模型实践
算法
NAGNIP4 小时前
RAG信息检索-如何让模型找到‘对的知识’
算法