图-有向图-无向图-二分图-拓扑排序

1 简介

        由节点和边构成的,其实就是一种高级点的多叉树。

2 图的表示(或者叫实现)

        图常用邻接表和邻接矩阵进行描述。

2.1 邻接表

        邻接表:每个节点x的邻居都存到一个列表里,然后把x和这个列表关联起来,这样就可以通过一个节点x找到它的所有相邻节点。

        邻接表相对于邻接矩阵占用的空间较少;但是无法快速判断两个节点是否相邻;

2.2 邻接矩阵

        邻接矩阵:是一个二维布尔数组,暂且称为matrix,如果节点x和y是相连的,那么就把matrix[x][y]设为true。如果想找节点x的邻居,找到matrix[x][..]为true的就是相邻的节点。

        邻接矩阵占用的空间较多;但是可以快速判断两个节点是否相邻;

3 图的类型

        图的类型包含:有向图、无向图、有向加权图等。

3.1 图的方向性

        图的方向性可以通过索引进行描述:

        比如在用邻接表实现有向图时,节点0的邻居是[4,3,1],则说明从0节点出发可以到达4、3和1节点;至于4、3和1节点能够到达哪些节点,需要查看它们的邻居。

        如果用的是邻接矩阵实现有向图,matrix[0][1]=true表示节点0可以到达节点1;如果matrix[1][0]=false,则说明节点1无法到达节点0。

3.2 图的权重

        图的权重则需要具体的值进行存储:

        如果用邻接表实现加权图,除了在列表中保存邻居节点外,还需要存储到达这些邻居节点的权重。

        如果用邻接矩阵实现加权图,matrix[x][y]的值就可以表示节点x到达节点y的权重;如果节点x无法到达节点y,则将matrix[x][y]设置为0即可。

4 图的遍历

        如果告知图中没有环,则可以不使用 visited;如果不使用path也可以去掉path。

vector<int> visited;
vector<int> path;
void traverse(vector<vector<int>>& graph, int index)
{     
	if (visited[index])  
		return;
 
	visited[index] = true;
	path.push_back(index);
	for (int i : graph[index])
		traverse(graph, i);
	
	path.pop_back();
}

5 力扣题目

5.1 所有可能的路径

797. 所有可能的路径

class Solution {
public:
    vector<vector<int>> allPathsSourceTarget(vector<vector<int>>& graph) {
        vector<int> path;
        vector<vector<int>> res;
        traverse(graph, 0, path, res);
        return res;
    }

    void traverse(vector<vector<int>>& graph, int index, vector<int>& path, vector<vector<int>>& res)
    {     
        path.push_back(index);   
        if (index == graph.size()-1)
        {
            res.emplace_back(path);
            path.pop_back();
            return;
        }        
        
        for (int i : graph[index])
            traverse(graph, i, path, res);
        
        path.pop_back();
    }
};

5.2 二分图

5.2.1 二分图的介绍

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

        换句更好理解的话就是:只能使用两种颜色对图中的节点进行着色,要求任意一条边的两个端点的颜色都不相同,如果能做到这个图就是二分图,反之则不是;

        二分图的应用场景:

        (1)可以被用到一些高级的图算法中;

        比如最大流算法,这个后面进行补充。

        (2)作为一种存储数据的结构;

        比如存储演员与电影的关系,如果用map进行存储则需要两个映射表;但如果用图来存储的话,将电影和演员作为该图中的两种不同类型的节点,并将其进行相连,这就形成了一个二分图。因为每个电影节点的相邻节点就是参与该电影的演员;而每个演员的相邻节点就是该演员所参演过的电影。

5.2.2 二分图的判断

        解题思路:

        遍历一遍图,一边遍历一遍染色,看是否可以用两种颜色给所有节点染色,使得相邻节点的颜色都不相同。

        这里有值得注意的地方:

        (1)图不一定是联通的,可能存在多个子图,故需要将每个节点作为遍历的起点来遍历一遍;

        (2)只有所有子图都是二分图的时候,整个图才是二分图;

5.3 力扣中"二分图"的题目

5.3.1 二分图的判断

785. 判断二分图 剑指 Offer II 106. 二分图

class Solution {
public:
    bool isBipartite(vector<vector<int>>& graph) {
        int n = graph.size();
        vector<bool> visited(n, false);
        //false 和 true 代表两种不同颜色
        vector<bool> color(n, false);
        bool isBipartiteFlag = true;
        //这里将节点0设置为颜色 true目的是说明在访问节点0之前已经初始化了该节点的颜色
        //其实节点0使用默认设置的false也是可以的
        color[0] = true;

        // 因为图不一定是联通的,可能存在多个子图
        // 所以要把每个节点都作为起点进行一次遍历
        // 如果发现任何一个子图不是二分图,整幅图都不算二分图        
        for (int i=0;i<n;++i)
        {
            if (!visited[i])
            {
                // dfs(graph, i, visited, color, isBipartiteFlag);
                bfs(graph, i, visited, color, isBipartiteFlag);
                if (!isBipartiteFlag)
                    break;                
            }
        }        

        return isBipartiteFlag;
    }

    void dfs(vector<vector<int>>& graph, int index, vector<bool>& visited, vector<bool>& color, bool& flag)
    {
        if (!flag)
            return;

        visited[index] = true;
        for (int i : graph[index])
        {
            if (!visited[i])
            {
                //在真正访问节点i之前,根据当前节点index的颜色来设置节点i的颜色
                color[i] = !color[index];
                dfs(graph, i, visited, color, flag);
                if (!flag)
                    break;
            }
            else
            {
                if (color[i] == color[index])
                {
                    flag = false;
                    break;
                }
            }
        }
    }

    void bfs(vector<vector<int>>& graph, int index, vector<bool>& visited, vector<bool>& color, bool& flag)
    {
        queue<int> q;
        q.push(index);
        visited[index] = true;
        int tmpIndex = 0;

        while (!q.empty() && flag)
        {
            tmpIndex = q.front();
            q.pop();

            for (int i : graph[tmpIndex])
            {
                if (!visited[i])
                {
                    //在真正访问节点i之前,根据当前节点tmpIndex的颜色来设置节点i的颜色
                    color[i] = !color[tmpIndex];
                    visited[i] = true;
                    q.push(i);
                }                
                else
                {
                    if (color[i] == color[tmpIndex])
                    {
                        flag = false;
                        break;
                    }
                }                
            }          
        }
    }    
};

5.3.2 可能的二分法

886. 可能的二分法

class Solution {
public:
    //参考题目:https://leetcode-cn.com/problems/vEAB3K/ 二分图
    //根据 dislikes 构建出 graph,然后判断 graph 是否是二分图即可
    bool possibleBipartition(int n, vector<vector<int>>& dislikes) {
        vector<vector<int>> graph;
        buildGraph(n+1, dislikes, graph);

        vector<bool> visited(n+1, false), color(n+1, false);
        bool flag = true;
        for (int i=1;i<=n;++i)
        {
            if (!visited[i])
            {
                dfs(graph, i, visited, color, flag);
                if (!flag)
                    break;
            }
        }

        return flag;
    }

    void buildGraph(int n, vector<vector<int>>& dislikes, vector<vector<int>>& graph)
    {
        graph.resize(n+1);
        for (auto& v : dislikes)
        {
            // 「无向图」相当于「双向图」
            graph[v[0]].push_back(v[1]);
            graph[v[1]].push_back(v[0]);
        }
    }

    void dfs(vector<vector<int>>& graph, int index, vector<bool>& visited, vector<bool>& color, bool& flag)
    {
        if (!flag)
            return;

        visited[index] = true;
        for (int i : graph[index])
        {
            if (!visited[i])
            {
                //在真正访问节点i之前,根据当前节点index的颜色来设置节点i的颜色
                color[i] = !color[index];
                dfs(graph, i, visited, color, flag);
                if (!flag)
                    break;                
            }
            else
            {
                if (color[i] == color[index])
                {
                    flag = false;
                    break;
                }
            }
        }
    }    
};

5.3.3

5.4 拓扑排序

        拓扑排序针对的是有向无环图,它是有向无环图的一种线性排序,该排序满足条件是:使得所有的有向边都是从左指向右(即:有向边的起点要在有向边终点的前面,也就是左边)。

        拓扑排序的用途:有向图中,有向边的起点被有向边的终点所依赖,在实际中,必须要先完成左侧的任务,才能完成右侧的任务。从左到右可以理解为按照这个时间线去完成任务,刚好满足任务间的依赖关系,保证被依赖的任务先执行。

        在利用DFS进行遍历有向无环图的时候,会一直走到最深处,所以第一个被回溯的点一定是处在最右侧的。如果一个列表中存储的顺序是:第一个被回溯的元素,第二个被回溯的元素,...,第n个被回溯的元素。需要对这个列表进行反转后,第一个被回溯的元素才能变成最右侧。

5.4.1 课程表

207. 课程表

class Solution {
public:
    bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
        vector<vector<int>> graph;
        buildGraph(numCourses, prerequisites, graph);
        vector<bool> visited(numCourses, false);
        vector<bool> onPath(numCourses, false);
        bool hasCycle = false;

        // 因为图不一定是联通的,可能存在多个子图
        // 所以要把每个节点都作为起点进行一次遍历
        for (int i=0;i<numCourses;++i)
        {
            if (!visited[i])
            {
                dfs(graph, i, visited, onPath, hasCycle);
                if (hasCycle)
                    return false;
            }
        }

        return true;
    }

    void buildGraph(int n, vector<vector<int>>& prerequisites, vector<vector<int>>& graph)
    {
        graph.resize(n);
        for (auto& v : prerequisites)
        {
            // 有向图
            // prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程  bi, 所以方向应该是 bi->ai
            graph[v[1]].push_back(v[0]);
        }
    }

    void dfs(vector<vector<int>>& graph, int index, vector<bool>& visited, vector<bool>& onPath, bool& hasCycle)
    {
        if (onPath[index])
        {
            hasCycle = true;
            return;
        }

        if (visited[index])
            return;

        visited[index] = true;        
        // 开始遍历节点 index
        onPath[index] = true;
        for (int i : graph[index])
        {
            dfs(graph, i, visited, onPath, hasCycle); 
        }
        // 节点 index 遍历完成
        onPath[index] = false;
    }  
};

5.4.2 课程表II

210. 课程表 II

class Solution {
public:
    vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
        vector<vector<int>> graph;
        buildGraph(numCourses, prerequisites, graph);
        vector<bool> visited(numCourses, false);
        vector<bool> onPath(numCourses, false);
        vector<int> topology;
        bool hasCycle = false;

        // 因为图不一定是联通的,可能存在多个子图
        // 所以要把每个节点都作为起点进行一次遍历
        for (int i=0;i<numCourses;++i)
        {
            if (!visited[i])
            {
                dfs(graph, i, visited, onPath, hasCycle, topology);
                if (hasCycle)
                    return vector<int>{};
            }
        }

        //逆序输出即可
        return vector<int>(topology.rbegin(), topology.rend());
    }

    void buildGraph(int n, vector<vector<int>>& prerequisites, vector<vector<int>>& graph)
    {
        graph.resize(n);
        for (auto& v : prerequisites)
        {
            // 有向图
            // prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则必须先学习课程  bi, 所以方向应该是 bi->ai
            graph[v[1]].push_back(v[0]);
        }
    }

    void dfs(vector<vector<int>>& graph, int index, vector<bool>& visited, vector<bool>& onPath, bool& hasCycle, vector<int>& topology)
    {
        if (onPath[index])
        {
            hasCycle = true;
            return;
        }

        if (visited[index])
            return;

        visited[index] = true;
        // 开始遍历节点 index
        onPath[index] = true;
        for (int i : graph[index])
        {
            dfs(graph, i, visited, onPath, hasCycle, topology); 
        }

        topology.push_back(index);
        // 节点 index 遍历完成
        onPath[index] = false;
    }         
};

5.4.3 外星文字典

剑指 Offer II 114. 外星文字典

class Solution {
public:
	//转换成图,如果图中存在环则不存在合法的顺序;否则图的拓扑排序就是合法的顺序
	string alienOrder(vector<string>& words) {
		vector<unordered_set<int>> graph(26);        
		bool isValid = buildGraph(words, graph);

        if (!isValid)
            return "";

        unordered_set<char> s;
        for (auto& str:words)
            for (auto& c:str)
                s.insert(c);

		vector<bool> visited(26, false), onPath(26, false);
		bool hasCycle = false;
		vector<int> topology;

		// 因为图不一定是联通的,可能存在多个子图
		// 所以要把每个节点都作为起点进行一次遍历
		for (int i = 0; i < 26; ++i)
		{
			if (!visited[i] && !graph[i].empty())
			{
				dfs(graph, i, visited, onPath, hasCycle, topology);
				if (hasCycle)
					return "";
			}
		}

		string res;
		auto it = topology.rbegin();
		while (it != topology.rend())
		{
            char c = *it + 'a';
			res.append(1, c);
            s.erase(c);
			++it;
		}

        res.append(string(s.begin(), s.end()));        
		return res;
	}

	bool buildGraph(vector<string>& words, vector<unordered_set<int>>& graph)
	{
		for (int i = 0; i < words.size() - 1; ++i)
		{
			for (int j = 0; j < words[i].length(); ++j)
			{
                //测试用例中竟然出现了 ["abc","ab"] 这种排序,所以加了下面这个代码
				if (j >= words[i + 1].length())
				{
					return false;
				}                
				if (words[i][j] != words[i + 1][j])
				{
					graph[words[i][j] - 'a'].insert(words[i + 1][j] - 'a');
					break;
				}
			}
		}

        return true;
	}

	void dfs(vector<unordered_set<int>>& graph, int index, vector<bool>& visited, vector<bool>& onPath, bool& hasCycle, vector<int>& topology)
	{
		if (onPath[index])
		{
			hasCycle = true;
			return;
		}

		if (visited[index])
			return;

		visited[index] = true;
		onPath[index] = true;
		for (auto& i : graph[index])
			dfs(graph, i, visited, onPath, hasCycle, topology);

		topology.push_back(index);		
		onPath[index] = false;
	}
};

5.4.4

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值