图论简介及相关概念
图是一个二元组G = ( V ( G ) , E ( G ) )
其中V ( G )是非空集,称为点集:表示顶点的集合
E(G)为V ( G )各结点之间边的集合,称为边集:表示边的集合
以下为主要的图
图的存储方式
邻接矩阵(二维数组)
用于稠密图
不带权的邻接矩阵中,1代表两顶点连通,0代表不连通,某顶点到它本身的边可以为0或1
带权的邻接矩阵中,不连通的顶点之间的边权值为正无穷,来表示两顶点无法到达
特别注意:两顶点之间的不同方向的权值可以不同(如G4)
代码实现:
bool adj[MAXN][MAXN];
scanf("%d %d", &n , &m);
for (int i = 1 ; i <= m ; i ++) {
int u , v;
scanf("%d %d", &u , &v);
adj[u][v] = 1;
adj[v][u] = 1;
}
邻接表(链表)
用于稀疏图
所以对于邻接表的存储,要采取头插法
代码:
以下为链表实现邻接表
# include <stdio.h>
# include <malloc.h>
struct node
{
int a; //顶点的编号
int w; // 权重
struct node* next; // 存储下一个节点
};
void add(struct node* p, int u, int v, int w) //头插法
{
struct node* x = (struct node*)malloc(sizeof(struct node));
x->a = v;
x->w = w;
x->next = p[u-1].next;
p[u-1].next = x;
}
int main()
{
int n = 4;
struct node q[n]; //节点数组
int u, v, w; //u表示起始点,v表示终点,w表示权重
int number = 5;
for (int i=1; i<=n; ++i)
{
q[i-1].a = i;
q[i-1].w = 0;
q[i-1].next = NULL;
}
for (int i=0; i<number; ++i)
{
scanf("%d %d %d", &u, &v, &w);
add(q, u, v, w);
}
for (int i=0; i<number; ++i)
{
struct node* p = q[i].next;
while (p!=NULL)
{
printf("(%d, %d, %d)", i+1, p->a, p->w);
p= p->next;
}
}
return 0;
}
用数组模拟邻接表
// edge[m]: m表示边的条数,edge[i] 表示索引为i的边所对应的终点
// weight[m]: m表示边的条数,weight[i] 表示索引为i的边的权重
// next[m]: m表示边的条数,next[i] 表示索引为i的边的兄弟边(起点一样的点)的索引(头插法)
// head[n]: n表示点的个数,head[i] 表示以i为起点所相连的第一条边的索引
// idx:表示边的序号,从0开始 (充当一个动态分配内存的作用,表示内存地址的编号,并没有实际意义)
对这个数据画出邻接表:
# include <stdio.h>
int idx = 0;
void add_edge(int u, int v, int w)//头插法
{
edge[idx] = v;
weight[idx] = w;
next[idx] = head[u];
head[u] = idx;
idx = idx + 1;
}
int main()
{
int* head;
int* edge;
int* Next;
int* weight;
//上述只是伪代码
//建图
for (int i=1; i<=n; ++i)
for (int j = head[i]; j != -1; j = next[i])
{
int v = i;
int u = edge[j];
int w = weight[j];
}
//查询
for (int i=1; i<n; ++i)
for (int er=head[i]; er>0; er=next[er])
printf("%d : %d ", i, edge[er]);
}
拓扑排序
每个节点的前置节点都在这个节点之前的排序顺序
要求:有向图,没有环
首先我们引入度的概念:
对于有向图每个结点都有入度和出度,入度就是指向该结点的边数,出度就是该结点指向其他结点的边数。
如第一个图:
A的入度为0,出度为2;
B的入度为1,出度为1;
C的入度为1,出度为1;
D的入度为2,出度为0;
总结一下拓扑排序就是只有从前指向后的边,没有从后指向前的边。
如果是一个有向无环图,那么一定有一个点的入度为0,如果找不到一个入度为0的点,这个图一定是带环的。
拓扑排序的思路:
1. 在图中找到所有入度为0的点
2. 把所有入度为0的点在图中删掉,重点是删掉影响!继续找到入度为0的点并删掉影响
3. 直到所有点都被删掉,依次删除的顺序就是正确的拓扑排序结果
如果无法把所有的点都删掉,说明有向图里有环
我们画图来解释一下:
首先我们的有向无环图是这样的:
我们发现A的入度为0,那么A就可以作为源点(不会有边在它前面),然后删除A和A上所连的边,如下图:
然后我们发现B和C的入度都是0,那么同样删除B,C和B,C上所连的边,如下图:
然后D的入度为0,我们同样操作,最后图被删除干净,证明可以拓扑排序。
模版
bool topsort()
{
int l = 0, r = -1;
// d[i] 存储点i的入度
for (int i = 1; i <= n; i ++ )
if (d[i] == 0)
{
r = r + 1;
q[r] = i;
}
while (l <= r)
{
int t = q[l ++ ];
for (int i = head[t]; i != -1; i = next[i])
{
int j = edge[i];
if (d[j]-1 == 0)
{
q[j] = j;
}
}
}
// 如果所有点都入队了,说明存在拓扑序列;否则不存在拓扑序列。
return r == n - 1;
}
floyd算法
floyd算法是求解多源最短路(多对多)的算法,即确定每个节点(起点)到其他节点(终点)的最短路,floyd算法适用于有向图,无向图,允许边的权重为负,但负边构成的回路(环)的权重不能为负(不能为负环)
本质:动态规划
dp[i][j] = min(dp[i][k] + dp[j][k], dp[i][j])
# include <stdio.h>
int path[6][6]= {0};
void floyd(int (*d)[6], int n)
{
for (int k=1; k<=n; ++k) //k一定要在最外面 这样才能得到子问题才是最优的
for (int i=1; i<=n; ++i)
for (int j=1; j<=n; ++j)
{
if (d[i][j] > d[i][k] + d[k][j])
{
d[i][j] = d[i][k] + d[k][j];
path[i][j] = k; //记录i和j的中间点k即可
}
}
}
void get_path(int u, int v, int (*path)[6]) //递归求出路径
{
if (path[u][v] = 0)
{
printf("%d -> %d", u, v);
printf("\n");
return;
}
int mid = path[u][v];
get_path(u, mid, path);
get_path(mid, v, path);
}
int main()
{
int d[6][6] = {0};
int n = 5;
for (int i=1; i<=n; ++i)
for (int j=1; j<=n; ++j)
scanf("%d", &d[i][j]);
floyd(d, n);
for (int i=1; i<=n; ++i)
{
for (int j=1; j<=n; ++j)
printf("%d ", d[i][j]);
printf("\n");
}
get_path(1, 3, path);
}
注意:因为floyd算法的本质是动态规划,由子问题的最优解从前向后推导,所以k一定为最外层
for (int k=1; k<=n; ++k) //k一定要在最外面 这样才能得到子问题才是最优的
for (int i=1; i<=n; ++i)
for (int j=1; j<=n; ++j)
{
if (d[i][j] > d[i][k] + d[k][j])
{
d[i][j] = d[i][k] + d[k][j];
}
}
以下为k为什么要放在最外层的解释:
Dijkstras算法
从一个顶点到其余各顶点的最短路的算法(单源最短路)
解决的是有权图种最短路径的问题,但不能解决带有负权边的图
迪杰斯特拉算法主要特点是从起始点开始,采用贪心算法的策略,每次遍历到始点距离最近且未访问过的顶点的邻接节点,直到扩展到终点为止。
过程:
1.选定一个点,这个点未被选过并且离顶点最近
2.以这个点为中间点,对它所有的邻近点去尝试松弛
通过最小堆选择距离最近的点
通过邻接表选择所有的邻近点
最后的ac代码
# include <stdio.h>
# include <string.h>
void get_path(int x, int* path) //寻找节点前面的那个节点并输出
{
if (path[x] == -1)
return;
get_path(path[x], path);
printf("%d ", x);
}
int main()
{
int n = 7;
int a[7][7] = { //邻接矩阵
{0, 5, 2, 999, 999, 999, 999},
{5, 0, 999, 1, 6, 999, 999},
{2, 999, 0, 6, 999, 8, 999},
{999, 1, 6, 0, 1, 2, 999},
{999, 6, 999, 1, 0, 999, 7},
{999, 999, 8, 2, 999, 0, 3},
{999, 999, 999, 999, 7, 3, 0},
};
int dis[n];
for (int i=0; i<n; ++i)
dis[i] = 999; //表示两点之间没有连接
int path[n]; //记录路径
memset(path, -1, sizeof(path));
dis[0] = 0;
int x[n]; //记录该点是否被使用过
memset(x, 0, sizeof(x));
for (int i=0; i<n-1; ++i)
{
int node = -1;
for (int j=0; j<n; ++j)
{
if (x[j] == 0 && (node == -1 || dis[j] < dis[node])) //选择距离最近的点
node = j;
}
for (int j=0; j<n; ++j)
{
if (dis[j] > dis[node] + a[node][j]) //松弛操作
{
dis[j] = dis[node] + a[node][j];
path[j] = node; //记录j节点前面的那个节点node
}
}
x[node] = 1;
}
get_path(6, path);
}