关于图论算法

图论基础

图本质上就是个高级点的多叉树而已,适用于树的 DFS/BFS 遍历算法,全部适用于图。

1. 图的存储常常通过邻接表和邻接矩阵来实现

2. 我们再明确一个图论中特有的(degree)的概念:

        在无向图中,「度」就是每个节点相连的边的条数

        由于有向图的边有方向,所以有向图中每个节点「度」被细分为入度(indegree)和出度(outdegree)=> 入度:指向该节点边的数量


遍历算法框架

DFS

图和多叉树最大的区别是,图是可能包含环的,你从图的某一个节点开始遍历,有可能走了一圈又回到这个节点,而树不会出现这种情况,从某个节点出发必然走到叶子节点,绝不可能回到它自身。

所以,如果图包含环,遍历框架就要一个 visited 数组进行辅助

 这里你看到的onPath是用来解决路径问题的 => 标记正在走的路线

类比贪吃蛇游戏,visited 记录蛇经过过的格子,而 onPath 仅仅记录蛇身。在图的遍历过程中,onPath 用于判断是否成环,类比当贪吃蛇自己咬到自己(成环)的场景,这下你可以理解它们二者的区别了吧。

如果让你处理路径相关的问题, onPath 变量是肯定会被用到的,拓扑排序 中就有运用。

这个 onPath 数组的操作很像回溯算法中的「做选择」和「撤销选择」,区别在于位置:

回溯算法的「做选择」和「撤销选择」在 for 循环里面,

而对 onPath 数组的操作在 for 循环外面。

回溯算法和 DFS 算法的区别所在:回溯算法关注的不是节点,而是树枝。

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

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

vector visited;
vector onPath;
void DFS(Graph graph, int s){
    if(visited[s]) return;
    visited[s] = true;
    onPath[s] = true;
    for(auto neighbour:graph.neighbour(s)){
        DFS(graph,neighbour);
    }
    onPath[s] = false;
} 

BFS

所谓「出度」和「入度」是「有向图」中的概念,很直观:如果一个节点 xa 条边指向别的节点,同时被 b 条边所指,则称节点 x 的出度为 a,入度为 b

按道理, 图的遍历 都需要 visited 数组防止走回头路,

这里的 BFS 算法其实是通过 indegree 数组实现的 visited 数组的作用,只有入度为 0 的节点才能入队,从而保证不会出现死循环。

void BFS(int num, vector>& pre){
        queue q;
        for(int i=0;i

环检测算法

207. 课程表 - 力扣(LeetCode)

成环 => 结点之间有依赖关系

先修课程修完才能学习下面的课程 => 这就是依赖关系

如果发现这幅有向图中存在环,那就说明课程之间存在循环依赖,肯定没办法全部上完;反之,如果没有环,那么肯定能上完全部课程

DFS

class Solution {
public:
    bool hasCycle = false;
    vector onPath;
    vector visited;
    vector> graph;
    bool canFinish(int numCourses, vector>& prerequisites) {
        onPath.resize(numCourses);
        visited.resize(numCourses);
        buildGraph(numCourses, prerequisites);
        for(int i = 0; i < numCourses; i++) {
            DFS(i); // 防止有些孤立的结点遍历不到
        }
        return !hasCycle; // 没有循环依赖 就是可以完成学习
    }

    void buildGraph(int n, vector>& pre) {
        graph.resize(n);
        for(auto p: pre) {
            graph[p[1]].emplace_back(p[0]);
        }
    }

    void DFS(int node) {
        if(onPath[node]) hasCycle = true;
        if(visited[node] || hasCycle) return;
        visited[node] = true;
        onPath[node] = true;
        for(auto next: graph[node]) DFS(next);
        onPath[node] = false;
    }
};

BFS

我们使用一个队列来进行广度优先搜索。初始时,所有入度为 0 的节点都被放入队列中,它们就是可以作为拓扑排序最前面的节点,并且它们之间的相对顺序是无关紧要的

判断环的依据 => 存在环时,始终会有结点入度降不到0,就加不到队列里去

class Solution {
public:
    vector> graph;
    vector indegree;
    bool canFinish(int numCourses, vector>& prerequisites) {
        indegree.resize(numCourses, 0);
        buildGraph(numCourses, prerequisites);
        return BFS(numCourses);
    }
    
    void buildGraph(int n, vector>& pre) {
        graph.resize(n);
        for(auto p: pre) {
            graph[p[1]].emplace_back(p[0]);
            indegree[p[0]]++;
        }
    }

    bool BFS(int num) {
        queue q;
        for(int i = 0; i < num; i++) {
            if(!indegree[i]) q.push(i);
        }
        int count = 0;
        while(!q.empty()) {
            int cur = q.front();
            q.pop();
            count++;
            for(auto next: graph[cur]) {
                indegree[next]--;
                if(!indegree[next]) q.push(next);
            }
        }
        return count == num;
    }
};

拓扑排序

210. 课程表 II - 力扣(LeetCode)

什么叫做拓扑排序:

直观地说就是,让你把一幅图「拉平」,而且这个「拉平」的图里面,所有箭头方向都是一致的

 抽象出题目中需要的逻辑:

如果一幅有向图中存在环,是无法进行拓扑排序的,因为肯定做不到所有箭头方向一致;

反过来,如果一幅图是「有向无环图」,那么一定可以进行拓扑排序。

但是我们这道题和拓扑排序有什么关系呢?

其实也不难看出来,如果把课程抽象成节点,课程之间的依赖关系抽象成有向边,那么这幅图的拓扑排序结果就是上课顺序

怎么用算法实现拓扑排序:

对于DFS而言:后序遍历的结果就是拓扑排序

对于BFS而言:按照入度下降梯度相对排序

DFS

关于后序结果是否逆序 => 在于建图函数的写法问题

class Solution {
public:
    vector> graph;
    vector onPath;
    vector visited;
    bool hasCycle = false;
    vector ans;
    vector findOrder(int numCourses, vector>& prerequisites) {
        onPath.resize(numCourses);
        visited.resize(numCourses);
        buildGraph(numCourses, prerequisites);
        for(int i = 0; i < numCourses; i++) {
            DFS(i);
        }
        reverse(ans.begin(), ans.end());
        if(hasCycle) return {};
        else return ans;
    }

    void buildGraph(int n, vector>& pre) {
        graph.resize(n);
        for(auto p: pre) {
            graph[p[1]].emplace_back(p[0]);
        }
    }

    void DFS(int now) {
        if(onPath[now]) hasCycle = true;
        if(hasCycle || visited[now]) return;
        visited[now] = true;
        onPath[now] = true;
        for(auto next: graph[now]) DFS(next);
        ans.emplace_back(now);
        onPath[now] = false;
    }
};

BFS

vector indegree => 索引是结点,存放的值是该结点的入度数

class Solution {
public:
    vector> graph;
    vector indegree;
    vector ans;
    vector findOrder(int numCourses, vector>& prerequisites) {
        buildGraph(numCourses, prerequisites);
        if(BFS(numCourses)) return ans;
        else return {};
    }

    void buildGraph(int n, vector>& pre) {
        indegree.resize(n);
        graph.resize(n);
        for(auto p: pre) {
            graph[p[1]].emplace_back(p[0]);
            indegree[p[0]]++;
        }
    }

    bool BFS(int num) {
        queue q;
        for(int i = 0; i < num; i++) {
            if(!indegree[i]) q.push(i);
        }
        while(!q.empty()) {
            int cur = q.front();
            q.pop();
            ans.emplace_back(cur);
            for(auto next: graph[cur]){
                indegree[next]--;
                if(!indegree[next]) q.push(next);
            }
        }
        return ans.size() == num;
    }
};

你可能感兴趣的:(算法,图论,深度优先)