Day 72 图论: 841. 钥匙和房间, 127. 单词接龙, 图论简介, 797. 所有可能的路径,785. 判断二分图, 886. 可能的二分法

841. 钥匙和房间

  • 思路
    • example

      • 有向图 (有起点和终点)



    • 连通分支 (但是这里还是有方向的区别)

  • 复杂度. 时间:O(n+m), 空间: O(n^2)

遍历O(n+m) 建一个n^2表格 (有向图表)
再判断哪个房间作为终点不到到达。

TBA
  • 记忆化 DFS (或者BFS)
    • 复杂度. 时间:O(n+m), 空间: O(n) (m 是钥匙数量总数)

从起点0出发,深搜,标记已访问目的地j。
最后遍历判断是否有目的地未标记。
不需要回溯,因为从0出发深搜能到达的点,标记后即可。
回溯:需要找可行解(线路)的时候考虑。

class Solution:
    def canVisitAllRooms(self, rooms: List[List[int]]) -> bool:
        def dfs(room, index):
            if visited[index] == True:
                return  
            visited[index] = True 
            size = len(rooms[index]) 
            for i in range(size):
                dfs(rooms, rooms[index][i])  
            return  
        n = len(rooms)
        visited = [False for _ in range(n)]
        # visited[0] = False  # 这里的dfs写法:cannot initialize this to be True 
        dfs(rooms, 0) # DFS starting from 0
        # check visited
        for i in range(n):
            if visited[i] == False:
                return False  
        return True  
class Solution:
    def canVisitAllRooms(self, rooms: List[List[int]]) -> bool:
        def dfs(index):
            if visit[index] == True:
                return 
            visit[index] = True 
            n = len(rooms[index]) 
            for i in range(n):
                dfs(rooms[index][i]) 
            return 
        n = len(rooms) 
        visit = [False for _ in range(n)] 
        dfs(0) 
        for i in range(n):
            if visit[i] == False:
                return False 
        return True 
  • BFS
class Solution:
    def canVisitAllRooms(self, rooms: List[List[int]]) -> bool:
        n = len(rooms) 
        que = collections.deque() 
        que.append(0) 
        visit = [False for _ in range(n)]
        while que:
            size = len(que) 
            for i in range(size):
                idx = que.popleft() 
                if visit[idx] == True:
                    continue 
                visit[idx] = True
                for j in range(len(rooms[idx])):
                    que.append(rooms[idx][j]) # !!! 
        for i in range(n):
            if visit[i] == False:
                return False 
        return True  
  • 并查集 (如何处理有向图?)

127. 单词接龙

  • 思路
    • example

    • 最短转换序列


      • 无向图,只要两个单词相差一个字符,就表示相连。
        • 需要建立图表(二维数组,或者用dict(list))
          • 暴力法添加会比较复杂麻烦
          • 用中间变量(虚拟节点)过渡,比如'hit' -> '@it', 'h@t', 'hi@', 所有跟'hit'相差一个字符的必定在跟前面三种情况之一相连。
      • 最短路径:用BFS (用deque容器, FIFO)
      • 标记单词,避免重复访问
        • distance数组,每个单词(ID)标记第几步到达(初始inf)

wordId['hit'] = 0, wordId['@it'] = 1, wordId['h@t'] = 2, wordId['hi@'] = 3 (每个单词对应唯一的编号)
wordId['hot'] = 4, wordId['@ot'] = 5, wordId['ho@'] = 6
edge[0] = [1,2,3]
edge[1] = [0]
edge[2] = [0,4]
edge[3] = [0]
edge[4] = [5,2,6]
‘hit'和'hot'的连接通过'h@t'实现

  • 复杂度. 时间:O(nC^2)?, 空间: O(nC^2) (列表单词长度)
class Solution:
    def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
        def addWord(word):
            nonlocal nodeNum  
            if word not in wordId:
                wordId[word] = nodeNum 
                nodeNum += 1

        def addEdge(word):
            addWord(word)
            id1 = wordId[word]
            chars = list(word)
            for i in range(len(chars)):
                temp = chars[i]
                chars[i] = '@'
                newWord = ''.join(chars)
                addWord(newWord)
                id2 = wordId[newWord]
                edge[id1].append(id2)  
                edge[id2].append(id1)
                chars[i] = temp  


        n = len(wordList)  
        wordId = collections.defaultdict(int)  
        edge = collections.defaultdict(list)
        nodeNum = 0

        addEdge(beginWord)
        for i in range(n):
            addEdge(wordList[i])
        # 没有必要加endWord进 edge  
        # 如果endWord不在wordList中,return 0
        if endWord not in wordId:
            return 0 

        distance = [float('inf') for _ in range(nodeNum)] 
        beginId, endId = wordId[beginWord], wordId[endWord]
        distance[beginId] = 0

        que = collections.deque([beginId])
        while que:
            Id = que.popleft()
            if Id == endId:
                return distance[endId]//2 + 1 
            for it in edge[Id]:
                if distance[it] == float('inf'):
                    distance[it] = distance[Id] + 1
                    que.append(it)
        return 0 
class Solution:
    def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
        # wordId['cook'] = 123
        # hit: 通过 *it, h*t, hi* 过渡
        # graph: edge[id1] = id2 
        # BFS, deque 
        # visit[id] = True or False 
        def addWord(word):
            nonlocal cnt 
            if word not in wordId:
                wordId[word] = cnt 
                cnt += 1

        def addEdge(word):
            addWord(word) 
            id1 = wordId[word]
            chs = list(word)  
            for i in range(len(chs)):
                ch = chs[i] 
                chs[i] = '*' 
                newWord = ''.join(chs) 
                addWord(newWord) 
                id2 = wordId[newWord] 
                edge[id1].append(id2) 
                edge[id2].append(id1) 
                chs[i] = ch # backtrack 

        # main function
        wordId = collections.defaultdict(int) 
        edge = collections.defaultdict(list) 
        n = len(wordList) 
        cnt = 0

        # Graph
        addEdge(beginWord) 
        for i in range(n):
            addEdge(wordList[i]) 
        if endWord not in wordId:
            return 0 
        
        beginId, endId = wordId[beginWord], wordId[endWord] 
        visit = [False for _ in range(len(wordId))] 

        # BFS
        que = collections.deque() 
        que.append(beginId) 
        distance = 0 
        while que:
            size = len(que) 
            for i in range(size):
                Id = que.popleft() 
                if Id == endId:
                    return distance // 2 + 1
                for idx in edge[Id]:
                    if visit[idx] == True:
                        continue 
                    visit[idx] = True 
                    que.append(idx) 
            distance += 1
        return 0 
  • 不需要使用wordId
TBA

图论

  • 图:节点, 边


  • 类似多叉树结构

/* 图节点的逻辑结构 /
class Vertex {
int id;
Vertex[] neighbors;
}
/
基本的 N 叉树节点 */
class TreeNode {
int val;
TreeNode[] children;
}

  • 邻接表, 邻接矩阵


// 邻接表
// graph[x] 存储 x 的所有邻居节点
List<Integer>[] graph;
// 邻接矩阵
// matrix[x][y] 记录 x 是否有一条指向 y 的边
boolean[][] matrix;

  • 邻接表,好处是占用的空间少; 但无法快速判断两个节点是否相邻

在常规的算法题中,邻接表的使用会更频繁一些,主要是因为操作起来较为简单

  • 入度(indegree)和出度(outdegree)

无向图



如果连接无向图中的节点 x 和 y,把 matrix[x][y] 和 matrix[y][x] 都变成 true 不就行了;邻接表也是类似的操作,在 x 的邻居列表里添加 y,同时在 y 的邻居列表里添加 x。

加权图
如果是邻接表,我们不仅仅存储某个节点 x 的所有邻居节点,还存储 x 到每个邻居的权重.
如果是邻接矩阵,matrix[x][y] 不再是布尔值,而是一个 int 值,0 表示没有连接,其他值表示权重.

图的遍历

  • 类似多叉树遍历,但是图可能包含
    • 使用visited标记已访问过的节点
// 记录被遍历过的节点
boolean[] visited;
// 记录从起点到当前节点的路径
boolean[] onPath;

/* 图遍历框架 */
void traverse(Graph graph, int s) {
    if (visited[s]) return;
    // 经过节点 s,标记为已遍历
    visited[s] = true;
    // 做选择:标记节点 s 在路径上
    onPath[s] = true;
    for (int neighbor : graph.neighbors(s)) {
        traverse(graph, neighbor);
    }
    // 撤销选择:节点 s 离开路径
    onPath[s] = false;
}

类比贪吃蛇游戏,visited 记录蛇经过的格子,而 onPath 仅仅记录蛇身。在图的遍历过程中,onPath 用于判断是否成环,类比当贪吃蛇自己咬到自己(成环)的场景.


  • DFS vs 回溯

回溯算法的「做选择」和「撤销选择」在 for 循环里面
onPath 数组的「做选择」和「撤销选择」操作在 for 循环外面

// DFS 算法,关注点在节点
void traverse(TreeNode root) {
    if (root == null) return;
    printf("进入节点 %s", root);
    for (TreeNode child : root.children) {
        traverse(child);
    }
    printf("离开节点 %s", root);
}

// 回溯算法,关注点在树枝
void backtrack(TreeNode root) {
    if (root == null) return;
    for (TreeNode child : root.children) {
        // 做选择
        printf("从 %s 到 %s", root, child);
        backtrack(child);
        // 撤销选择
        printf("从 %s 到 %s", child, root);
    }
}

797. 所有可能的路径

  • 思路
    • example
    • 有向无环图(DAG)
    • graph[i][j] != i(即不存在自环)
    • 给定邻接表

时间复杂度:O(n×2^n),其中 n 为图中点的数量。我们可以找到一种最坏情况,即每一个点都可以去往编号比它大的点。此时路径数为 O(2^n),且每条路径长度为 O(n),因此总时间复杂度为 O(n×2^n)。每个点(除了起点和终点)都有两种选择:选与不选,故最坏时间复杂度为O(2^n).
空间复杂度:O(n),其中 n 为点的数量。主要为栈空间的开销。注意返回值不计入空间复杂度。

  • 写法1: 选择撤销在循环内 (需要单独把初始起点单独加入path)
class Solution:
    def allPathsSourceTarget(self, graph: List[List[int]]) -> List[List[int]]:
        def traversal(graph, i): # 常规回溯框架, 选择|撤销操作 在循环内 
            if i == len(graph) - 1: # 节点判断以及处理
                res.append(path[:]) 
                return 
            for j in graph[i]: # 深搜, 无环:不需要去重, 否则要用visited 
                path.append(j) # 选择
                traversal(graph, j) # 递归回溯
                path.pop() # 撤销
        res = []
        path = [0] # 调用遍历函数前,先加入起点进path 
        traversal(graph, 0)
        return res 
class Solution:
    def allPathsSourceTarget(self, graph: List[List[int]]) -> List[List[int]]:
        def backtrack(idx):
            if idx == n - 1:
                res.append(path[:]) 
                return 
            for node in graph[idx]:
                path.append(node) 
                backtrack(node) 
                path.pop() 
            return 
        n = len(graph) 
        res = [] 
        path = [0] # !!!
        backtrack(0)
        return res 
  • 写法2: 选择撤销在循环外 (不需要单独把初始节点加入path)
class Solution:
    def allPathsSourceTarget(self, graph: List[List[int]]) -> List[List[int]]:
        def traversal(graph, i): # dfs框架?, 选择|撤销操作 在循环外 
            path.append(i) # 选择:把节点加入path; 循环外 
            if i == len(graph) - 1: # 节点判断以及处理
                res.append(path[:]) 
                path.pop() # 提早return, 所以要单独把最后加进的节点pop出来
                return # 提早return, 所以要单独把最后加进的节点pop出来
            for j in graph[i]: # 深搜, 无环:不需要去重, 否则要用visited 
                traversal(graph, j) # 递归dfs
            path.pop() # 撤销;循环外
        res = []
        path = [] # 调用遍历函数前,不需要单独加入起点进path 
        traversal(graph, 0)
        return res 
class Solution:
    def allPathsSourceTarget(self, graph: List[List[int]]) -> List[List[int]]:
        def dfs(idx):
            path.append(idx)
            if idx == n - 1:
                res.append(path[:]) 
                path.pop() # !!!!!!
                return 
            for node in graph[idx]:
                dfs(node) 
            path.pop()
            return 
        n = len(graph) 
        res = [] 
        path = [] # !!!
        dfs(0)
        return res 
  • BFS
TBA

二分图

二分图的顶点集可分割为两个互不相交的子集,图中每条边依附的两个顶点都分属于这两个子集,且两个子集内的顶点不相邻。



双色问题



应用:电影与演员的映射关系. 常规:两个hash表。用「图」结构存储,将电影和参演的演员连接,很自然地就成为了一幅二分图:

785. 判断二分图

  • 思路
    • example
    • 输入一个 邻接表 表示一幅无向图,请你判断这幅图是否是二分图
    • dfs

一边遍历(dfs)一边着色,注意相邻节点必须用不同颜色。

class Solution:
    def isBipartite(self, graph: List[List[int]]) -> bool:
        def dfs(graph, start, color_code):
            if visited[start]: # 已经着色
                return color[start] == color_code # 是否协调
            color[start] = color_code 
            visited[start] = True 
            for j in graph[start]:
                flag = dfs(graph, j, color_code^1)
                if flag == False:
                    return False 
            return True 
        n = len(graph)
        visited = [False for _ in range(n)] # 节点是否已着色
        color = [-1 for _ in range(n)] # 每个节点的着色,0 or 1
        for i in range(n): # 可能有多个连通分支,所以每个点都要当作起始点检查一遍
            if not visited[i]:
                flag = dfs(graph, i, 0) # color_code = 0 (1 也可以)
                if flag == False: # 已经找到冲突,提早返回False 
                    return False 
        return True 
class Solution:
    def isBipartite(self, graph: List[List[int]]) -> bool:
        def dfs(graph, x, status):
            new_status = (status + 1) % 2
            if mark[x] != -1:
                return mark[x] == new_status
            mark[x] = new_status 
            for y in graph[x]:
                if dfs(graph, y, new_status) == False:
                    return False 
            return True 
        n = len(graph)
        mark = [-1 for _ in range(n)] 
        for i in range(n): # !!!
            if mark[i] != -1:
                continue 
            if dfs(graph, i, 0) == False:
                return False 
        return True 
  • bfs
TBA

886. Possible Bipartition

  • 思路
    • example
class Solution:
    def possibleBipartition(self, n: int, dislikes: List[List[int]]) -> bool:
        def dfs(graph, i, color_code):
            if color[i] != -1:
                return color[i] == color_code 
            color[i] = color_code 
            for j in graph[i]:
                if dfs(graph, j, color_code^1) == False:
                    return False 
            return True 
        color = [-1 for _ in range(n+1)] # -1, 0, 1 
        graph = collections.defaultdict(list)
        for dislike in dislikes:
            u, v = dislike[0], dislike[1]
            graph[u].append(v)
            graph[v].append(u) 
        for i in range(1, n+1):
            if color[i] == -1:
                if dfs(graph, i, 0) == False:
                    return False 
        return True 
class Solution:
    def possibleBipartition(self, n: int, dislikes: List[List[int]]) -> bool:
        def dfs(graph, i, status):
            if color[i] != -1:
                return color[i] == status 
            color[i] = status 
            for j in graph[i]:
                if dfs(graph, j, status^1) == False:
                    return False 
            return True 
        graph = collections.defaultdict(list)
        color = [-1 for _ in range(n+1)] 
        for i in range(len(dislikes)):
            x, y = dislikes[i][0], dislikes[i][1] #!!!
            graph[x].append(y) 
            graph[y].append(x) 
        for i in range(1, n):
            if color[i] == -1:
                if dfs(graph, i, 0) == False:
                    return False 
        return True 
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,718评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,683评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,207评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,755评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,862评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,050评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,136评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,882评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,330评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,651评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,789评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,477评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,135评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,864评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,099评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,598评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,697评论 2 351

推荐阅读更多精彩内容