图论算法入门基础---图的储存---链式前向星详讲

先给出离散数学上的概念:

有序对:

有两个元素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;
}

推荐一个记忆化搜的题:

最大食物链计数 - 洛谷

  • 7
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值