刷题(11) 图和并查集

有空再补充最小生成树和最小路径树

基本概念

图的分类

  • 有向/无向
  • 加权/不加权
  • 两两组合,一共有4种图

图的表示

  • 邻接表
    • 邻接链表数组, A【2】里面放的是 一个链表,是和节点2相连的节点
    • 适用于稀疏图,也就是顶点数大于边数的情况
    • 代码表示:
      • vector<vector> adjs
      • adjs【i】 是一个vector, 里面存着第i个节点所有边的另一个节点编号
  • 邻接矩阵
    • 二维矩阵,n个节点,nxn个矩阵。
    • 假如无向图, i和j相连,那么A【i】【j】, A【j】【i】 为true
  • 根据题目输入,转为邻接表方法
 一般题目给的输入是边的矩阵edges= [[1,0],[2,0],[3,1],[3,2]] 和 节点个数 num
 
  1) 如果是无向图
      a) 假如给了节点个数
          转为:vector<vector<int>> adjs(num);
          转换方法:
          for (auto &edge: edges)
          {
              int i = edge[0], j = edge[1];
              adjs[i].push_back(j);
              adjs[j].push_back(i);
          }
          adjs【i】 是一个vector, 里面存着第i个节点所有边的另一个节点编号
      b) 假如没给节点个数
          转为:unordered_map<int, vector<int>> adjs;
  2)有向图
      struct Edge
      {
          Edge(int val, int from, int to): val(val), from(from), to(to) {}
          int val; // 有权 要这个数据成员, 无权图不要
          int from, to;
      };
 
      a) 给了节点个数用   vector<vector<Edge>> adjs(num);
      b) 没给节点个数用   unordered_map<int, vector<Edge>> adjs;

图的遍历

  • 遍历跟节点s 连通的所有节点
  • 以下代码 图均用邻接表表示
  • visited的用途是因为。
    • 无向图,必须使用visited数组,因为肯定会重复遍历节点,进入死循环。
    • 有向图
      • 有向有环图,必须用。
      • 有向无环图,可以不用visited数组。

DFS

void travese(vector<vector<int>> &adjs, int s){
	vector<bool> visited(adjs.size(), false);
    visited[s] = true;
    dfs(adjs, visited, s);
}


void dfs(vector<vector<int>> &adjs, vector<bool> &visited, int cur){
    // do something
    for (int next: adjs[cur]){
        if (!visited[next]){
            visited[next] = true;
            dfs(adjs, visited, next);
        }
    }
}

  • 时间复杂度: O(V+E),其中 V 是图中的顶点数,E 是图中的边数。
  • 空间复杂度: O(V)。 栈中最多可以有 V 个元素。所以所需的空间是 O(V)。

BFS

  • 基本思路
    • 一圈一圈的扫荡。首先扫荡距离起点为1的点,然后扫荡距离起点为2的点,按照与起点的距离的顺序来遍历所有的顶点
void travese(vector<vector<int>> &adjs, int s){
	vector<bool> visited(adjs.size(), false);
	visited[s] = true;
    queue<int> q{{s}};
    while (q.size()){
        int cur = q.front();
        q.pop();
        // do something
        for (int i: adjs[cur]){
            if (!visited[i]){
                visited[i] = true;
                q.push(i);
            }
        }
    }
}
  • 时间复杂度: O(V+E),其中 V 是图中的顶点数,E 是图中的边数。
  • 空间复杂度: O(V)。 队列中最多可以有 V 个元素。所以所需的空间是 O(V)。

并查集

  • 应用场景:

    • 给出两个节点,判断它们是否连通,如果连通,不需要给出具体的路径
    • 如果需要给出具体的路径,可以用dfs,bfs
  • 算法流程:
    0. 让每个节点,构成单元素的集合

    1. 依次合并相连的两个节点
  • 并查集就是两个操作,union和find

    • union的时候,把小树挂到大树,防止增加树的高度, 同时union是c++的关键字,所以用union_
    • find的时候,加入路径压缩,如果当前节点p的父节点不是p自己的话,那把p的父节点设为原先p的爷爷节点
class UF{
    
public:
    
/*
    		可以把每个连通分量,想象成一颗树
    		id[i]表示的含义是以下两者之一
    			a) 第i个节点的父节点
    			b) 假如id【i】 = i 那就表示他是根节点,同时作为这个连通分量的编号

    		size【根节点】 表示的是这个连通分量有多少个节点
    		size【其他】  没有意义

    		count 表示当前有多少个连通分量
    		
*/
    
    UF(int k):size(k,1),count(k){
        for(int i=0;i<k;i++){
            id.push_back(i);
        }
    }
    
    int find(int p){
        while(p != id[p]){
            id[p]=id[id[p]]; //加入路径压缩,如果当前节点p的父节点不是p自己的话,那把p的父节点设为原先p的爷爷节点
            p=id[p];
        }
        
        return p; 
    }
    
    void union_(int p,int q){
       
        int i=find(p);
        int j=find(q);
        if(i!=j){
            // 小树挂在大树上
            if(size[i]>size[j]){
                id[j]=i;size[i] += size[j]; 
            } else {
                id[i]=j;size[j] += size[i];
            }
            --count;
        }
    }
    
    int number(){
        return count;
    }
    
    
private:
    vector<int> id;
    vector<int> size;
    int count;
 
    
};
  • 构造并查集的复杂度是O(N),而在使用了find时候路径压缩技巧的实现中,find和union都是平均O(1)的

  • 例题:

应用

求两点之间是否连通

给出一条路径DFS版
vector<int> isReachable(vector<vector<int>> &adjs, int s, int d){
    vector<bool> visited(adjs.size(), false);
    vector<int> path;
    dfs(adjs, visted, path, s, d);
    return path;
}


bool dfs(vector<vector<int>> &adjs, vector<bool> &visited, vector<int> &path, int s, int d){
    path.push_back(s);
    visited[s] = true;
    if (s == d){
        return true;
    }
    
    for (int next: adjs[s]){
        if (!visited[next]){
            if (dfs(adjs,visited, path, next, d)
                return true;
        }
    }
    path.pop_back();
    // visted[s] = false;  不需要重置, 因为只需要给出一条路径即可,
    return false;
    
}

给出一条路径BFS版
vector<int> isReachable(vector<vector<int>> &adjs, int s, int d){
    vector<bool> visited(adjs.size(), false);
    vector<int> edge_to(adjs.size(), -1); // edge_to[w] = k 表示 k到w的一条边, -1表示未设置
	visted[s] = true;
	queue<int> q{{s}};
	while (q.size()){
		int cur = q.front();
		q.pop();
		for (int i: adjs[cur]){
			if (!visited[i]){
				visited[i] = true;
				edge_to[i] = cur;
				if (i == d){
					return get_path(edge_to, s, d);
				}
				
				q.push(i);
			}
		}
	}
		

    return {};
}

vector<int> get_path(vector<int> &edge_to, int s, int d){
	vector<int> ret;
	for (int i= d; i != s; i = edge_to[i]){
		ret.push_back(i);
	}
	
	ret.push_back(s);
	reverse(ret.begin(), ret.end());
	return ret;
}


求两点之间所有路径


// 找出所有从节点 0 到节点 n-1 的路径
class Solution {
public:
    vector<vector<int>> allPathsSourceTarget(vector<vector<int>>& graph) {
        vector<vector<int>> ret;
        vector<int> path;
        
        // 假如是有向无环图,就不需要visited数组
        vector<bool> visited(graph.size(), false);
        visited[0] = true; 
        path.push_back(0);

        dfs(ret, path, graph, visited, 0);
        return ret;
        
    }

    void dfs(vector<vector<int>> &ret, vector<int> &path, vector<vector<int>> &graph, vector<bool> &visited, int cur){
        
        if (cur == graph.size() - 1){
            ret.push_back(path);
            return;
        }

        for (int next: graph[cur]){
            if (!visited[next]){
                visited[next] = true;
                path.push_back(next);
                dfs(ret, path, graph, visited, next);
                visited[next] = false; // 找出所有路径是需要重置visited数组的
                path.pop_back();
            }
        }
        
    }

};

求两点之间最短路径

无权图
  • 跟求两点之间是否连通BFS版代码一致
有权图

有无环

  • 环是指一条至少含有一条边且起点和终点相同的路径
无向图(这个一般不会考)
  • 不考虑有自环和平行边
    • 自环:一条连接一个顶点和自身的边
    • 平行边: 连接同一对顶点的两条边
class Cycle
{
    private Graph graph;
    private bool[] visited;
    private bool hasCycle;

    public Cycle(Graph g)
    {
        this.graph = g;
        visited = new bool[g.Verts];//已访问标记,大小为图的顶点数
        //考虑一个图有多个连通分量,需要对每个没访问到的点都分别进行DFS才能保证访问到所有顶点
        for (int i = 0; i < g.Verts; i++)
        {
            if (!visited[i])
                DFS(i, i);
        }

    }

    private void DFS(int v, int w)
    {
        //参数v是当前访问顶点,w是上一个访问顶点
        visited[v] = true;
        foreach (var item in graph.Adj(v))
        {
            if (!visited[item])
                DFS(item, v);
            else if (item != w)//如果一个顶点已经被访问过,且这个顶点不是上一个访问顶点,则有环
                hasCycle = true;
        }
    }

    public bool HasCycle()
    {
        return hasCycle;
    }
}

  • 若在深度优先搜索的过程中遇到回边(即指向已经访问过的顶点的边),则必定存在环

    从树的角度很容易理解上面的代码,树就是一个无环图,而树的前中后序遍历就是DFS,我们从根节点开始进行DFS,对于树中任意一个节点,只与他的父节点以及他的子节点连通,基于DFS的回溯方式,实际上不会访问到父节点外的重复节点,若遇到已访问的节点且不是该点的“父节点”,则树不成立,而是一个有环图。

有向图
BFS (重要)
  • 看拓扑排序的Kahn算法,能找到拓扑排序的有向图就是无环图,否则就是有环图
DFS (没时间不用看)
  • 具体办法:
    • 在DFS的基础上添加一个bool数组来保存在递归调用期间正在遍历路径的所有顶点,若遇到一个顶点已被访问过而且还在调用栈上,则视为有环。
    • 因为使用DFS遍历的时候, 系统维护的递归栈,保存的其实是当前正在遍历的有向路径,假如我们当前正在访问节点v的所有边,有一个边 v->w, w是之前访问过的节点,且在当前正在遍历的递归栈里,那就是有环。因为栈里表示的是一条w到v的有向路径, 而v->w正好补全了这个环。
  • 考虑一个图有多个连通分量,所以需要对每个没访问到的点都分别进行DFS才能保证访问到所有顶点。
  • 如果需要返回某个环的路径,那么只需要在遍历的时候,同时记录edge_to数组即可。
  • 例题:207. 课程表
class Solution {
public:
    bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {

        // 转为邻接表
        vector<vector<int>> adjs(numCourses, vector<int>());
        for (vector<int> &edge: prerequisites){
            adjs[edge[1]].push_back(edge[0]);
        }

        vector<bool> visited(numCourses, false); // 防止重复遍历,陷入死循环
        vector<bool> onPath(numCourses, false);  // 保存正在遍历路径上的所有节点.

        // 需要遍历每个节点。        
        for (int i= 0; i != numCourses; i++){
            if (!visited[i]){
                if (dfs(adjs, visited, onPath, i))
                    return false;
            }
        }

        return true;
    }

    bool dfs(vector<vector<int>> &adjs, vector<bool> &visited, vector<bool> &onPath, int cur){
        visited[cur] = true;
        onPath[cur] = true;
        for (int next: adjs[cur]){
            if (!visited[next]){
                if (dfs(adjs, visited, onPath, next))
                    return true;
            } else if (onPath[next]) {
                return true;
            }
        }

        onPath[cur] = false;
        return false;


    }

};

拓扑排序

  • 首先必须是有向无环图才能有拓扑排序.

  • 概念:

    • 拓扑排序:所有顶点排序,保证所有的有向边都是排在前面的顶点指向后面的顶点。

    • 入度:在有向图中,表示其他顶点直接指向某个顶点的边的数目

    • 出度:在有向图中,表示从某个顶点出发指向其他顶点的边的数目

Kahn算法 (BFS)
  • 算法流程

    1. 设置邻接表,入度数组(nums[i] 表示 第i个节点的入度),结果数组Ret
    2. 初始化队列,将入度为0的顶点全都入队(顺序不重要)
    3. 出队队首元素,放入ret数组,同时把这个元素的所有邻接点入度减-1,假如入度为0,就入队。
    4. 循环步骤2 直到队列为空
    5. 如果ret数组长度与节点数目一致,ret数组就是其中一个拓扑排序,否则就是有环。
  • 时间复杂度:

    O(V+E) V表示顶点数,E表示边数。

  • 空间复杂度:

    O(V+E)

  • 例题:

class Solution {
public:
    vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {


        vector<vector<int>> adjs(numCourses, vector<int>()); // 邻接表
        vector<int> nums(numCourses, 0); // 入度数组
        for(auto &edge: prerequisites){
            adjs[edge[1]].push_back(edge[0]);
            nums[edge[0]]++;
        }


        queue<int> q;
        vector<int> ret;
        for (int i = 0; i < numCourses; i++){
            if (nums[i] == 0){
                q.push(i);
            }
        }

        while(q.size()){
            int cur = q.front();
            q.pop();
            ret.push_back(cur);
            for (int next: adjs[cur]){
                nums[next]--;
                if (nums[next] == 0){
                    q.push(next);
                }
            }
        }

        if (ret.size() == numCourses)
            return ret;
        else
            return {};

    }
};

二分图判定

  • 概念

    • 如果能将一个图的节点集合分割成两个独立的子集 A 和 B ,并使图中的每一条边的两个节点一个来自 A 集合,一个来自 B 集合,就将这个图称为 二分图 。
  • 算法的流程如下:

    • 我们任选一个节点开始,将其染成红色,并从该节点开始对整个无向图进行遍历;
    • 在遍历的过程中,如果我们通过节点 u 遍历到了节点 v(即 u 和 v 在图中有一条边直接相连),那么会有两种情况:
      • 如果 v未被染色,那么我们将其染成与 u 不同的颜色,并对 v 直接相连的节点进行遍历;
      • 如果v被染色,并且颜色与 u相同,那么说明给定的无向图不是二分图。我们可以直接退出遍历并返回False 作为答案。
    • 当遍历结束时,说明给定的无向图是二分图,返回 True 作为答案。
  • 我们可以使用「深度优先搜索」或「广度优先搜索」对无向图进行遍历

  • 例题:

  • BFS代码

    class Solution {
    public:
        bool isBipartite(vector<vector<int>>& graph) {
            
            vector<int> colors(graph.size(), UNKNOW);
            for (int i = 0; i< graph.size(); i++){
                if (colors[i] == UNKNOW){
                    colors[i] = RED;
                    queue<int> q{{i}};
                    while (q.size()){
                        int cur = q.front();
                        q.pop();
                        int next_color = colors[cur] == RED?BLUE:RED;
                        for (int next: graph[cur]){
                            if (colors[next] == UNKNOW){
                                colors[next] = next_color;
                                q.push(next);
                            } else if (colors[next] != next_color)
                                return false;
                        }
                    }
                }
            }
    
            return true;
    
        }
    
    private:
        enum {UNKNOW, RED, BLUE};
    
    };
    

最小生成树

概念
  • 生成树 指的是「无向图」中,具有该图的 全部顶点边数最少 的连通子图。

  • 最小生成树指的是「加权无向图」中总权重最小的生成树。

  • 连通图,从图的任意一个顶点能到任意另一个顶点

  • 最小生成树针对的是加权连通无向图

  • prim算法

  • kruskal算法

最小路径树

  • 针对加权有向图
  • 解决方法:
    • Dijkstra算法 (限制权值非负)
    • Bellman-Ford算法 (适用于所有情况,权值可以负,可以有环,但别有负权重环,存在负权重环,最短路径树就不存在了)

参考资料:

  • https://www.drflower.top/posts/8c9bd54f/#%E6%9C%89%E5%90%91%E5%9B%BE%E6%98%AF%E5%90%A6%E5%AD%98%E5%9C%A8%E7%8E%AF

  • https://labuladong.gitee.io/algo/2/19/41/

  • https://leetcode-cn.com/leetbook/read/graph/r3cr3r/

  • https://www.geeksforgeeks.org/find-if-there-is-a-path-between-two-vertices-in-a-given-graph/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值