图论的理解和应用

图论简介及相关概念

图是一个二元组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);

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值