先给出离散数学上的概念:
有序对:
有两个元素x,y(允许x=y)按照一定的顺序排序成的二元组称作一个有序对或称序偶记作<x,y>,其中x是它的第一个元素,y是它的第二个元素;其性质:
1,当x!=y时,<x,y>!=<y,x>;
2, <x,y>=<u,v>的充分必要条件:x=u,y=v;这些性质是二元集所不具备的 比如{x,y}={y,x}无论x,y是否相等都成立;
无序积概念:
设A,B为任意的两个集合,称{{a,b} | a∈A&&b∈B }为A与B的无序积记作A与B的无序积,记作:A&B;方便起见A&B中的无序对{a,b}记作(a,b),且允许a=b;无论a,b是否相等都有(a,b)=(b,a);因而有A&B=B&A;
V: vertex 顶点;
E: edge 边线;
笛卡尔积概念:
设A,B为集合,用A中的元素作为第一元素,B中元素作为第二元素构成的有序对,所有有序对构成的集合称作A和B的笛卡尔积,并记作AxB;符号表示为AxB={ <x,y> | x∈A&&y∈B };
例如A={a,b},B={0,1,2};
AxB={<a,0>,<a,1>,<a,2>,<b,0>,<b,1>,<b,2>};
BxA={<0,a>,<0,b>,<1,a>,<1,b>,<2,a>,<2,b>};
图的定义:
无向图:无向图G是一个有序的二元组<V,E>;其中:
(1)V是一个非空有穷集,称作顶点集,其元素称作顶点或结点。
(2) E是无序积V&V的有穷多重子集,称作边集,其元素称作无线边,简称边;
有向图:有向图D是一个有序的二元组<V,E>;其中;
(1)V定义同上;
(2)E是笛卡尔积VxV的有穷多重子集,称作编辑,其元素称作有有向边,简称边;
图片源于网络
该图为一个有向图;去掉箭头,将集合E改为无序对即为无向图;
接下来介绍三种图的储存方式:
1:邻接矩阵:优点:容易理解,实现简答,适用于稠密图(边多的图);缺点不适合与稀疏图会造成巨大的空间浪费,是最暴力的方法了;
2:邻接表:优点与缺点都不突出,是算法竞赛中比较常用的做法;缺点是不易判定重边
3:链式前向星:缺点也是判重麻烦,还有就是难以理解,代码实现麻烦。要是理解后把代码模版背下来呢? 优点的话,据前人经验,几乎所有图论题都能用它做。这也是本文重点讲的。
邻接矩阵
对于矩阵而言,其表示意义为,第i行第j列的值为顶点i到顶点j的权值;记作m(i,j)
因此开一个二维数组就可以了;
#include<iostream>
const int N = 1007;
int m[N][N];
/*
数据源于离散数学教材;
输入 n=4;
map:
0,2,1,0
0,0,1,0
0,0,0,1
0,0,1,1
*/
int main() {
int n;
std::cin >> n;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
std::cin >> m[i][j];
}//读入图
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (m[i][j]) {//如果两点可达
std::cout << "The distance from " << i << " to " << j << " is " << m[i][j] << " units\n";
}
}
}
return 0;
}
邻接表
邻接表的话用的是一个链表的思想,我们可以通过一个不定长的向量来表示它
k==v[i][j]表示的是 顶点i的第j条边到达顶点k;(这里的第几条边j只是为了方便遍历而表示的,i和k才是顶点);
v1,v2,v3,v4,v5
v1 : 0,0,0,0,1;
v2: 1,0,1,0,0;
v3: 0,0,0,0,1
v4: 1,0,1,0,0;
v5: 0,1,0,1,0;
这是一个可达矩阵
现在表示成向量形式:
vec1: 5 顶点1的第一条边到达5
vec2: 1,3 顶点2的第一条边到达1,第二条边到达3
vec3: 5, 顶点3的第一条边到达5
vec4: 1,3 顶点4的第一条边到达1,第二条边到达3
vec5: 2,4 顶点5的第一条边到达2,第二条边到达4
该表示方法不能表示边的权值;如果想要表示权值的话一般定义一个结构体:u为顶点,w为权值;
不考虑边权值:
#include<iostream>
#include<vector>
using std::vector;
const int N = 1007;
vector<int> vec[N];
/*
测试数据:
n,m n个点m条边
v u 点v到点u;
输入:
5 8
1 5
4 1
4 3
2 3
2 1
3 5
5 2
5 4
*/
int main() {
int n, m;
std::cin >> n >> m;
for (int i = 0; i < m; i++) {
int v, u;
std::cin >> v >> u;
vec[v].push_back(u);
//从边的角度读入
}
for (int i = 1; i <= n; i++) {
for (int j = 0; j < vec[i].size(); j++) {
std::cout << i << " to " << vec[i][j] << " ";
}
std::cout << '\n';
}
return 0;
}
考虑边权值:
#include<iostream>
#include<vector>
using std::vector;
const int N = 1007;
struct edge {
int u, w;
};
vector<edge> vec[N];
/*
数据:
n,m n个点m条边
v u 点v到点u;
输入:
5 8
1 5 2
4 1 2
4 3 3
2 3 1
2 1 4
3 5 2
5 2 5
5 4 7
*/
int main() {
int n, m;
std::cin >> n >> m;
for (int i = 0; i < m; i++) {
int v, u, w;
std::cin >> v >> u >> w;
vec[v].push_back({u,w});
//从边的角度读入
}
for (int i = 1; i <= n; i++) {
for (int j = 0; j < vec[i].size(); j++) {
std::cout<<" The distance of "<< i << " to " << vec[i][j].u << " is "<<vec[i][j].w<<"\t";
}
std::cout << '\n';
}
return 0;
}
接下来介绍较难理解的链式前向星;其实还有一种叫前向星的,不过我们可以跳过,直接学习
链式前向星;
链式前向星
接下来以书上的有向图(如果是无向图的话,可以化为两个点通过两条有向箭头连接的有向图)的数据为例:
前面的方式都是通过点来找点,而这里通过点找边,边找边的方式遍历;
我们可以设置几个数组来表示:
下列表示方法为网络上习惯的数组表示法:
to[cnt] 表示第cnt条边指向的终点,因此to[cnt]是一个顶点 cnt是第几条边;
w[cnt] 表示第cnt条边的权值;
ne[cnt] 表示的是第cnt条边指向的边 因此ne[cnt]是一条边;
h[u] 表示的是编号为u的点引出的最新的一条边 h[u]是一条边;
我们可以通过ne[cnt]来储存h[u],h[u]=一条新边,来更新;
那么遍历的思路就是:从第x的顶点出发 找到它的一条边 即为h[x],通过h[x]找出上一条边
即为 ne[h[x]]; 每次找到一条边那么就可以找到他的终点 即为 to[cnt],也可以找出它的权值w[cnt];
这也就是一开始说的,点找边,边找边 边找边。。。。。
再来看下数组含义,h[u] :head 头部 头引出边
to[cnt] : 没啥好说的,边指出点
w[cnt] : worth 边指出权值;
ne[cnt]: next 给出边 找到下一个边;
这里面有一个非常强的映射关系,如果理解通了,学习到的就不只是这么一个存图了;
下面给出具体实例加以理解:
这里特别注意 ne[1]=-1这个初始化,因为通过第一条边找不到下一条边了,因此初始化为-1;
那么:流程如下:为了偷懒 实例把权值都设为ww
cnt==1;
v1到v2 这条边 的终点v2 保留在to[1]中 to[1]=2 权值保留在w[1]中 w[1]=ww;
ne[1]=h[1];对此我们一开始可以对h全部赋值为-1;
h[1]=1; h[1]中的1 指的是点 v1 而非cnt
那么这条边录完了,cnt++;
cnt==2:
v1到v3 的终点为v2 to[2]=3 w[2]=ww;
ne[2]=h[1];
h[1]=2; cnt++;
cnt==3:
v1到v4 to[3]=4 w[3]=ww;
ne[3]=h[1];
h[1]=3; cnt++;
cnt==4;
v1到v5 to[4]=4 w[4]=ww;
ne[4]=h[1];
h[1]=4; cnt++;
cnt==5;
注意:这里换起点了哦;
v2到v3 to[5]=3 w[5]=ww;
ne[5]=h[2]; (h[2]==-1)
h[2]=5;cnt++;
cnt==6
v2到v4 to[6]=3 w[6]=ww;
ne[6]=h[2];
h[2]=6;cnt++;
cnt==7
v2到v5 to[7]=3 w[7]=ww;
ne[7]=h[2];
h[2]=cnt;cnt++;
于是就把这张图储存下来了
给出几种代码,一种就是按上面的几个数组实现的,另一种就是用head+结构体封装了一下;以普通枚举实现
最后再给出遍历方式为bfs||dfs的代码;
纯数组:
#include<iostream>
#include<algorithm>
/*
测试数据:
5 7
1 2 1
1 3 2
1 4 3
1 5 2
2 3 1
2 4 2
2 5 4
*/
const int N = 1e6;
int h[N], to[N], ne[N], w[N];
int cnt;
void add(int u, int v, int ww) {
to[cnt] = v;
w[cnt] = ww;
ne[cnt] = h[u];
h[u] = cnt;
cnt++;
}
int main() {
int n, m;
std::cin >> n >> m;
//输入顶点数和边数
for (int i = 1; i <= n; i++) {
h[i] = -1;//将所有顶点的最新编号为-1;
}
for (int i = 1; i <= m; i++) {
int u, v, ww;
std::cin >> u >> v >>ww;
add(u, v, ww);//加入这条边;
}
//枚举所有的点:
for (int i = 1; i <= n; i++) {
//根据head 枚举所有边
for (int j = h[i]; j != -1; j=ne[j]) {
std::cout << "The distance of " << i << " to " << to[j] << " is " << w[j]<<'\n';
}
}
return 0;
}
结构体封装:h+edge;
#include<iostream>
const int N = 1e6;
struct E {
int to , w, ne;
};
E edge[N];
int h[N], cnt;
void add(int u, int v, int ww) {
edge[cnt].to = v;
edge[cnt].w = ww;
edge[cnt].ne = h[u];
h[u] = cnt;
cnt++;
}
int main() {
int n, m;
std::cin >> n >> m;
for (int i = 1; i <= n; i++)h[i] = -1;
for (int i = 1; i <= m; i++) {
int u, v, w;
std::cin >> u >> v >> w;
add(u, v, w);
}
for (int i = 1; i <= n; i++) {
for (int j = h[i]; j != -1; j = edge[j].ne) {
std::cout << "The distance of " << i << " to " << edge[j].to<< " is " << edge[j].w << '\n';
}
}
return 0;
}
用bfs||dfs进行遍历:
#include<iostream>
#include<cstring>
#include<queue>
using std::queue;
const int N = 1e6;
struct E {
int to, w, ne;
};
E edge[N];
int h[N], cnt;
bool vis[N];
void add(int u, int v, int ww) {
edge[cnt].to = v;
edge[cnt].w = ww;
edge[cnt].ne = h[u];
h[u] = cnt;
cnt++;
}
void bfs(int n) {
queue<int>p;
//寻找所有起点;
for (int i = 1; i <= n; i++) {
if (vis[i] == true)continue;
p.push(i);
while (!p.empty()) {
int t = p.front();
p.pop();
if (vis[t] == true)continue;
vis[t] = true;
for (int j = h[t]; j != -1; j = edge[j].ne) {
int v = edge[j].to;
p.push(v);
std::cout << "The distance of " << t << " to " << edge[j].to << " is " << edge[j].w << "\n";
}
}
}
}
// dfs:
void dfs(int u) {
if (h[u]==-1||vis[u]==true) return;
vis[u] = true;
for(int i=h[u];i!=-1;i=edge[i].ne){
dfs(edge[i].to);
std::cout << "The distance of" << u << " to " << edge[i].to << " is " << edge[i].w << '\n';
}
}
void reseach_dfs(int n) {
//一样的枚举所有起点,因为有了vis的剪枝,所以不必担心效率太低;
for (int i = 1; i <= n; i++) {
if (vis[i] == true)continue;
dfs(i);
}
}
int main() {
int n, m;
std::cin >> n >> m;
for (int i = 1; i <= n; i++)h[i] = -1;
for (int i = 1; i <= m; i++) {
int u, v, w;
std::cin >> u >> v >> w;
add(u, v, w);
}
std::cout << "bfs:\n";
bfs(n);
std::cout << "dfs:\n";
memset(vis, false, sizeof vis);
reseach_dfs(n);
return 0;
}
推荐一个记忆化搜的题: