图的基本概念
图一
图二,边上数字代表通过需要的时间
图三
假设图一是某一个位置的地图,其中1,2,3,4,为该地方的建筑物,建筑物与建筑物之间的线段为一条双向可通的路。
对于图一而言,每个建筑物被称为顶点,建筑物与建筑物之间的道路被称为边。而图这种数据结构就是这些顶点与关联这些顶点的集合。比如对于上图而言,其中顶点的集合就是{1,2,3,4},其边的集合就是{(1,2),(1,3),(1,4),(2,4)}。
另外,对于图而言,分为有向图与无向图 : 无向图简单而言就是每条道路都可以双向通行的图,如图一图二。 有向图就是像图三一样,道路都是单向通行,也有可能是一来一回的,这种图被称为有向图。
在无向图中,一条结点连边的条数被称为这个图的度数。在有向图中,一个结点向别的结点连边的条数被称为入度,别的结点向一个结点连边的条数被称为出度。
对于图二而言,我们标记了走每条道路需要消耗的时间。那么对于每条边的属性值,我们称之为边权。除了边以外,点也具有属性值,我们称点的属性值为点权。如果两个顶点之间不止一条边相连,我们这称之为重边。甚至有时候会出现一条边的起点和终点是一样的,这就会被称之为自环。在大多数时候,图的重边和自环都会被简化掉。而孤点是指不与任何其他其他结点相连的点,但是孤点参与节点对的构成,影响节点对的数量。
环是指一条只有第一个和最后一个顶点重复的非空路径
图的存储
1.邻接矩阵
我们现在若想将图二和图三存储进计算机,在没有重边的情况下,可以采用邻接矩阵的方法。
使用一个二维数组arr[i][j]表示,arr[i][j]表示从点i到点j的边权。图二的邻接矩阵如下图所示。 图三就不搬了,与图二类似,与之不同的是,无向图的邻接矩阵是对称的,但是有向图就不一定对称了。
代码实现,就是声明一个二维数组,然后读入邻接矩阵的数据就好了,比较简单,这里就不写出来了。
2.邻接表
邻接表的概念
虽然邻接矩阵确实可以将图二图三这样的结构存储进计算机,但是由于其开辟了一个
n
2
n^2
n2的二维数组,其空间复杂度为
O
(
n
2
)
O(n^2)
O(n2),后续遍历该数组,时间复杂度为
O
(
n
2
)
O(n^2)
O(n2)。毫无疑问,邻接矩阵的效率十分低下,所以大部分时候存储图结构,我们会采用邻接表。
邻接表的思想是,对于有向图而言,其中一条有向边 ( i , j ) (i,j) (i,j),不需要用 n 2 n^2 n2的数组来进行存下到其他点是否存在边,只需要存储一个点能够到达的顶点和相应的边长的集合即可。使用邻接表,其空间复杂度会优化到 O ( n ) O(n) O(n)。但是如果要查找到某个路径的边权,因为不知道具体存放位置,需要遍历以起点的所有边来找到,其时间复杂度为 O ( n ) O(n) O(n),不如邻接矩阵的 O ( 1 ) O(1) O(1)。
在C++中,我们可以采用vector来实现邻接表。采用STL的pair和vector,pair的first和second分别表示一条边的终点和边权。每次读入一条边 ( u , v , l ) (u,v,l) (u,v,l),就使用p[u].push_back({v,l});即可,来表示为点u增加一条终点为v,边权为l的边。
邻接表的基本代码:
#include <iostream>
#include <vector>
using namespace std;
const int MAX = 1000;
int m, n;//图中m顶点,n条边。
vector<pair<int, int>> P[MAX];
int v[MAX][MAX];
int main()
{
cin >> m >> n;
for (int i = 1; i <= m; i++)
{
int u, v, l; cin >> u >> v >> l;
P[u].push_back({ v,l });
//P[v].push_back({u,l})
//如果是无向图,则还需要将终点,起始点颠倒过来存储一下
}
//将邻接表转换为邻接矩阵
for (int i = 1; i <= m; i++)
for (int j = 0; j < P[i].size(); j++)
v[i][P[i][j].first] = P[i][j].second;
//对于邻接矩阵v[i][j],j就是终点,即为P[i][j].first,
//其储存的值为边权,也就是P[i][j].second
//output
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= m; j++)
cout << v[i][j] << " ";
cout << endl;
}
return 0;
}
图的遍历
遍历图有两种方法,DFS(深度优先搜索)与BFS(广度优先搜索)。深搜与广搜就不多少了,参考上一篇文章
链接: 搜索算法-深搜与广搜
P5318 【深基18.例3】查找文献 - 洛谷 | 计算机科学教育新生态。
我们通过这题,来分别对图的遍历的两种方式进行举例,掌握DFS和BFS后,遍历图轻而易举。
1.DFS遍历图
void dfs(int x)
{
vis[x] = true;
cout << x << " ";
for (int i=0; i<P[x].size(); i++)
if (!vis[P[x][i]])
dfs(P[x][i]);
}
2.BFS遍历图
queue<int> q;
void bfs(int x)
{
memset(vis, false, sizeof(vis));
vis[x] = true;
q.push(x);
while (!q.empty())
{
int v = q.front();
q.pop();
cout << v << " ";
for (int i=0; i<P[v].size(); i++)
if (!vis[P[v][i]])
{
vis[P[v][i]] = true;
q.push(P[v][i]);
}
}
}
该题完整题解:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e5+5;
vector<int> P[MAXN];
int n, m;
bool vis[MAXN];
queue<int> q;
void dfs(int x)
{
vis[x] = true;
cout << x << " ";
for (int i = 0; i < P[x].size(); i++)
if (!vis[P[x][i]])
dfs(P[x][i]);
}
void bfs(int x)
{
memset(vis, false, sizeof(vis));
vis[x] = true;
q.push(x);
while (!q.empty())
{
int v = q.front();
q.pop();
cout << v << " ";
for (int i = 0; i < P[v].size(); i++)
if (!vis[P[v][i]])
{
vis[P[v][i]] = true;
q.push(P[v][i]);
}
}
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= m; i++)
{
int u, v;
cin >> u >> v;
P[u].push_back(v);
}
for (int i = 1; i <= n; i++)
sort(P[i].begin(), P[i].end());
dfs(1);
cout << endl;
bfs(1);
return 0;
}
DAG与拓扑排序
基本概念
DAG:对于一个图而言,如果这个图是没有环的,但是边是有方向的,那么就称之为有向无环图,即DAG。
拓扑排序:拓扑排序就是在DAG的基础上对点进行排序,使得在搜到点x时所有能到达点x的点y已经被搜过了。其具体实现流程如下: 1.将所有入度为0的点加入处理队列 2.将处于队头的点x取出,遍历x所能到达的所有点y。 3. 对于每一个y,删除从点x到点y的边。 4.如果点y的入度减为0了,说明说明所有能到y的点都被计算过了,再将点y加入处理队列。 5.重复2,直到队列为空。
求一个DAG的最长路
P1113 杂务 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 挺有价值的题目,该题要求人掌握如何求解一个DAG中的最长路。该题要使用简单DP+深搜,我感觉挺难的。另外这题,也可以使用拓扑排序来做。这里,我们需要创建一个value数组,用于存储做第x件事情时需要花费的时间。
记忆化搜索做法
#include<bits/stdc++.h>
using namespace std;
const int MAX = 1e4 + 10;
vector<int> V[MAX];
int value[MAX];
int vis[MAX];
int dfs(int x) {
if (vis[x]) return vis[x];
for (int i = 0; i < V[x].size(); i++)
vis[x] = max(vis[x], dfs(V[x][i]));
vis[x] += value[x];
return vis[x];
}
int main()
{
int n,x,y,res=0; cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> x >> value[i];
while (cin >> y) {
if (!y)break;
else
V[x].push_back(y);
}
}
for (int i = 1; i <= n; i++)
res = max(res, dfs(i));
cout << res;
return 0;
}
拓扑排序做法
#include<bits/stdc++.h>
using namespace std;
const int MAX = 1e5 + 10;
vector<int> V[MAX];
queue<int> Q;
int ind[MAX], value[MAX],ans[MAX];
int main()
{
int n, y,res = 0; cin >> n;
for (int i = 1; i <= n; i++)
{
int x; cin >> x >>value[i];
while (cin >> y) {
if (!y)break;
V[y].push_back(x);
ind[x]++;
}
}
for (int i = 1; i <= n; i++) {
if (ind[i] == 0)
Q.push(i),ans[i]=value[i];
}
while (!Q.empty()) {
int x = Q.front(); Q.pop();
for (int i = 0; i < V[x].size(); i++) {
int y = V[x][i];
ind[y]--;
if (ind[y] == 0)
Q.push(y);
ans[y] = max(ans[y], ans[x] + value[y]);
}
}
for (int i = 1; i <= n; i++)
{
res = max(res, ans[i]);
}
cout << res;
return 0;
}
拓扑排序例题
P4017 最大食物链计数 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
深入浅出的拓扑排序经典例题,我们刚才已经了解过,拓扑排序,可以使得搜到点x时所有能到达点x的y都已经被搜索过,这题说求出最大的食物链,这里最大的食物链的定义就是说,一条入度为0的结点到一条出度为0的点的链,那么对于这题,我们就可以采用拓扑排序计数每一个结点从任意入度为0的点到此点的食物链计数,最后再统计所有出度为0的点所对应的计数的值就好了。
#include<bits/stdc++.h>
using namespace std;
const int MAX = 5e5 + 10;
const int MOD = 80112002;
vector<int> V[MAX];
queue<int> Q;
int ind[MAX], outd[MAX], f[MAX];
int main()
{
int n, m, res = 0; cin >> n >> m;
for (int i = 0; i < m; i++)
{
int x, y; cin >> x >> y;
outd[x]++; ind[y]++;
V[x].push_back(y);
}
for (int i = 1; i <= n; i++) {
if (ind[i] == 0)
Q.push(i),f[i]=1;
}
while (!Q.empty()) {
int x = Q.front(); Q.pop();
for (int i = 0; i < V[x].size(); i++) {
int y = V[x][i];
f[y] = (f[x] + f[y]) % MOD;
ind[y]--;
if (ind[y] == 0)
Q.push(y);
}
}
for (int i = 1; i <= n; i++)
{
if (outd[i] == 0)
res = (res + f[i]) % MOD;
}
cout << res;
return 0;
}