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 所有可能的路径
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 可能的二分法
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 课程表
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
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 外星文字典
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