DAY52

101 阅读9分钟

第十一章:图论part04

110.  字符串接龙

经过上面的练习,大家可能会感觉 广搜不过如此,都刷出自信了,本题让大家初步感受一下,广搜难不在广搜本身,而是如何应用广搜。

www.programmercarl.com/kamacoder/0…

类似 LeetCode 第 127 题 "Word Ladder",最常见的解法是使用 广度优先搜索(BFS)。由于每次转换只能改变一个字符,而我们需要找到最短路径,BFS 是很合适的选择。

解题思路

  1. 广度优先搜索(BFS)

    • beginWord 开始,尝试在字典 wordList 中找到与其只有一个字符不同的单词。
    • 每找到一个符合条件的单词,就将其加入到 BFS 队列,并将其标记为已访问,避免重复搜索。
    • 当我们找到 endWord 时,停止搜索,返回路径长度。
    • 如果在 BFS 结束前无法找到 endWord,则返回 0,表示不存在转换序列。
  2. 关键问题

    • 如何在字典中高效地找到只改变一个字符的单词?
    • 我们可以通过对单词的每一位进行逐字符替换,然后检查替换后的单词是否在字典中。
  3. 优化

    • 将字典 wordList 存入一个哈希集合(Set),使得查找操作为 O(1)。
    • 每次生成一个新单词时,我们需要确保该单词还未被访问过,以防止重复路径。

代码实现

var ladderLength = function(beginWord, endWord, wordList) {
    const wordSet = new Set(wordList); // 将字典列表转为 Set 提高查找效率
    if (!wordSet.has(endWord)) return 0; // 如果字典中没有 endWord,直接返回 0
    
    let queue = [[beginWord, 1]]; // 队列初始化,存放 [当前单词, 路径长度]
    const alphabet = 'abcdefghijklmnopqrstuvwxyz'; // 用于替换字符
    
    while (queue.length > 0) {
        let [word, length] = queue.shift(); // 从队列头取出当前单词和路径长度
        
        // 对当前单词的每一个字符进行替换
        for (let i = 0; i < word.length; i++) {
            for (let char of alphabet) {
                let newWord = word.slice(0, i) + char + word.slice(i + 1); // 替换第 i 个字符
                
                if (newWord === endWord) {
                    return length + 1; // 如果找到 endWord,返回路径长度
                }
                
                if (wordSet.has(newWord)) {
                    queue.push([newWord, length + 1]); // 将新单词加入队列
                    wordSet.delete(newWord); // 从字典中删除,防止重复访问
                }
            }
        }
    }
    
    return 0; // 如果 BFS 结束没有找到 endWord,返回 0
};

代码详解

  1. 初始化

    • wordList 转化为集合 wordSet,这样可以快速查找单词是否存在。
    • 如果 wordSet 中没有 endWord,直接返回 0,表示无法找到路径。
    • 使用队列 queue 进行 BFS,起始时将 [beginWord, 1] 放入队列,表示从 beginWord 开始,路径长度为 1。
  2. BFS 过程

    • 每次从队列中取出当前单词 word 和它对应的路径长度 length
    • word 的每个字符,尝试将其替换为 26 个英文字母中的每一个,生成新的单词 newWord
    • 如果 newWord 等于 endWord,则找到了转换路径,返回路径长度 length + 1
    • 如果 newWordwordSet 中,表示该单词是有效的转换步骤,将其加入队列,并从 wordSet 中删除,防止重复访问。
  3. 结束条件

    • 如果遍历整个字典后没有找到 endWord,返回 0,表示无法从 beginWord 转换到 endWord

示例

beginWord = "hit"
endWord = "cog"
wordList = ["hot","dot","dog","lot","log","cog"]

console.log(ladderLength(beginWord, endWord, wordList)); // 输出:5

解释

  • 最短的转换序列是 hit -> hot -> dot -> dog -> cog,长度为 5。

时间复杂度分析

  1. 时间复杂度

    • 假设单词的长度是 L,字典中有 N 个单词。对于每个单词,我们需要尝试替换每个位置的 L 个字符,每次替换有 26 个可能性,因此每次生成新单词的时间复杂度为 O(L * 26)
    • BFS 最坏情况下需要遍历整个字典中的 N 个单词,时间复杂度为 O(N * L * 26),简化为 O(N * L)
  2. 空间复杂度

    • 主要是队列和哈希集合 wordSet 的空间消耗,最坏情况下都是 O(N)

总结

  • 这个问题的核心是使用 BFS 进行逐步搜索,同时利用哈希集合加速查找是否存在可以转换的单词。
  • 通过这种方式,我们可以有效找到从 beginWordendWord 的最短转换序列。

105.  有向图的完全可达性

深搜有细节,同样是深搜两种写法的区别,以及什么时候需要回溯操作呢?

www.programmercarl.com/kamacoder/0…

这个问题可以理解为要判断节点 1 是否可以通过有向图中的边到达图中所有的节点。这可以通过 图遍历算法 来解决,例如 深度优先搜索(DFS)广度优先搜索(BFS)。基本思路是从 1 号节点出发,遍历图中的所有节点,最终判断是否能够访问到所有节点。如果能够访问所有节点,则输出 1;否则输出 -1。

解题思路

  1. 图表示

    • 可以用 邻接表 来表示有向图。邻接表是一个包含 N 个列表的数组,其中第 i 个列表包含所有从节点 i 出发的边连接到的目标节点。
  2. 图遍历

    • 从 1 号节点开始,使用 DFS 或 BFS 遍历整个图。
    • 在遍历过程中,记录每个被访问到的节点。
    • 最后检查是否所有节点都被访问到。如果是,则输出 1;否则输出 -1。
  3. 具体步骤

    1. 构建图的邻接表表示。
    2. 初始化一个数组 visited,用来记录节点是否被访问过。
    3. 使用 DFS 或 BFS 从 1 号节点开始遍历,标记所有访问到的节点。
    4. 检查 visited 数组,判断是否所有节点都被访问到。

代码实现(DFS)

function canVisitAllNodes(N, edges) {
    const graph = Array.from({ length: N + 1 }, () => []); // 构建邻接表
    for (const [u, v] of edges) {
        graph[u].push(v); // 添加有向边
    }

    const visited = Array(N + 1).fill(false); // 记录访问状态
    let visitCount = 0; // 记录被访问的节点数

    // 深度优先搜索函数
    function dfs(node) {
        visited[node] = true; // 标记为已访问
        visitCount++; // 更新已访问节点数量

        for (const neighbor of graph[node]) {
            if (!visited[neighbor]) {
                dfs(neighbor);
            }
        }
    }

    // 从节点 1 开始 DFS
    dfs(1);

    // 如果访问的节点数等于 N,说明可以访问所有节点
    return visitCount === N ? 1 : -1;
}

// 示例
const N = 4;
const edges = [
    [1, 2],
    [1, 3],
    [3, 4]
];

console.log(canVisitAllNodes(N, edges)); // 输出: 1

代码详解

  1. 邻接表构建

    • graph 是一个长度为 N + 1 的数组(因为节点编号从 1 开始),每个元素是一个数组,存储从该节点出发的所有邻接节点。
  2. DFS 函数

    • 从当前节点 node 出发,标记为已访问并递归访问相邻的未访问过的节点。
    • 每访问一个节点,visitCount 就加 1,用于统计已访问的节点数。
  3. 遍历和判断

    • 从节点 1 开始调用 dfs,遍历所有能够到达的节点。
    • 最后判断 visitCount 是否等于节点总数 N,如果等于,则返回 1;否则返回 -1。

复杂度分析

  • 时间复杂度:O(N + M),其中 N 是节点的数量,M 是边的数量。每个节点和每条边最多会被访问一次。
  • 空间复杂度:O(N + M),主要是邻接表和访问数组的空间消耗。

广度优先搜索(BFS)解法

同样的思路可以用 BFS 来解决,区别在于 BFS 使用队列逐层访问节点,适用于层次遍历。代码如下:

function canVisitAllNodesBFS(N, edges) {
    const graph = Array.from({ length: N + 1 }, () => []);
    for (const [u, v] of edges) {
        graph[u].push(v);
    }

    const visited = Array(N + 1).fill(false);
    let visitCount = 0;

    const queue = [1]; // 从节点 1 开始
    visited[1] = true;

    while (queue.length > 0) {
        const node = queue.shift();
        visitCount++;

        for (const neighbor of graph[node]) {
            if (!visited[neighbor]) {
                visited[neighbor] = true;
                queue.push(neighbor);
            }
        }
    }

    return visitCount === N ? 1 : -1;
}

总结

  • 该问题的核心是判断节点 1 是否能到达所有节点,这可以通过 图遍历算法(DFS 或 BFS) 来解决。
  • 关键是通过邻接表表示图,并利用图遍历算法从节点 1 开始,查看所有能访问到的节点。
  • 最后通过检查是否所有节点都被访问过来判断输出 1 还是 -1。

106.  岛屿的周长

简单题,避免大家惯性思维,建议大家先独立做题。

www.programmercarl.com/kamacoder/0…

方法一:遍历每个陆地单元

通过遍历矩阵中的每个陆地单元,计算其周长:

  1. 初始化周长为 0。
  2. 对于每个陆地单元,初始周长为 4。
  3. 检查四个方向的相邻单元,如果相邻单元是陆地,周长减 1。
  4. 返回总周长。
代码实现
var islandPerimeter = function(grid) {
    let perimeter = 0;
    const directions = [
        [-1, 0], // 上
        [1, 0],  // 下
        [0, -1], // 左
        [0, 1]   // 右
    ];

    for (let i = 0; i < grid.length; i++) {
        for (let j = 0; j < grid[0].length; j++) {
            if (grid[i][j] === 1) { // 如果是陆地
                let localPerimeter = 4; // 初始化周长为4
                for (const [dx, dy] of directions) {
                    const newX = i + dx;
                    const newY = j + dy;
                    
                    // 检查相邻单元
                    if (newX >= 0 && newX < grid.length && newY >= 0 && newY < grid[0].length) {
                        if (grid[newX][newY] === 1) {
                            localPerimeter--; // 相邻为陆地,周长减1
                        }
                    }
                }
                perimeter += localPerimeter; // 累加到总周长
            }
        }
    }
    
    return perimeter; // 返回总周长
};

方法二:计算陆地数量和相邻陆地数量

这个方法的思路是基于陆地单元的数量和相邻陆地的数量来直接计算周长:

  1. 计算总的陆地单元数(landCount)。
  2. 计算相邻陆地的数量(adjacentLandCount)。
  3. 最终的周长计算公式为:perimeter = landCount * 4 - adjacentLandCount * 2
代码实现
var islandPerimeterOptimized = function(grid) {
    let landCount = 0;
    let adjacentLandCount = 0;
    const directions = [
        [-1, 0], // 上
        [1, 0],  // 下
        [0, -1], // 左
        [0, 1]   // 右
    ];

    for (let i = 0; i < grid.length; i++) {
        for (let j = 0; j < grid[0].length; j++) {
            if (grid[i][j] === 1) { // 如果是陆地
                landCount++; // 增加陆地计数
                for (const [dx, dy] of directions) {
                    const newX = i + dx;
                    const newY = j + dy;
                    
                    // 检查相邻单元
                    if (newX >= 0 && newX < grid.length && newY >= 0 && newY < grid[0].length) {
                        if (grid[newX][newY] === 1) {
                            adjacentLandCount++; // 相邻为陆地,增加相邻陆地计数
                        }
                    }
                }
            }
        }
    }

    // 根据公式计算周长
    return landCount * 4 - adjacentLandCount * 2; 
};

// 示例
const grid = [
    [0, 1, 0, 0],
    [1, 1, 1, 0],
    [0, 1, 0, 0],
    [0, 0, 0, 0]
];

console.log(islandPerimeterOptimized(grid)); // 输出: 16

两种方法比较

  • 方法一:通过逐个计算每个陆地的周长,并检查相邻的陆地,适合简单直接的实现,但在大规模数据上可能会相对慢一些,因为需要多次检查相邻单元。
  • 方法二:通过计算总陆地数和相邻陆地数来获得周长,计算更加直接高效。

时间复杂度

  • 两种方法的时间复杂度都是 O(N * M),其中 N 是矩阵的行数,M 是列数,因为每个单元格只被访问一次。

总结

你可以根据需要选择任意一种方法来实现岛屿周长的计算。方法二在处理大规模数据时可能会表现得更好一些。