【C++】图论总结

图论在信息学竞赛中占了很大部分,很多实际问题可以用图论来解决。

定义

  • 什么叫图论?

  • 研究图的问题一门高深的学科。

  • 什么是图?

  • 就是由点和线组成的图形
    G=<V,E>
    G=graph V=vertex E=edge

图的描述:

来自百度地图,侵删

图的表示:

来自维基百科,侵删在这里插入图片描述

分类

  • 有向图和无向图
    有向图就是边有方向的
    无向图就是可以两边走的
  • 混合图: 既有有向又有无向边
  • 简单图:没有重边和自环的图
  • 完全图:任意两个点直接都有一条边
    比如n个点完全图有C(n,2)条边
  • 稀疏图和稠密图
    稀疏图: 边的数量相对点来说很少
    稠密图 : 边的数量接近于完全图
  • 连通图
    连通图: 在无向图如果任意两个之间都可以相互到达,就是连通图。
    n个点连通图最少需要n-1条边。
    强连通图: 在有向图里面,任意两点可以相互到达,就是强连通。
    n个点的强连通图最少需要n条。
    弱连通图:有向图中,任意两个点,至少有一个点可以到达另外一个点。

顶点的度

顶点的度: 和点相连的边的数量。
有向图的度可以分为入度和出度,A的入度1,A的出度2,A度3。
在这里插入图片描述
定理:1 任何一个图里面顶点的度之和一定是边的数量的2倍
2 有向图中所有顶点的入度之和是等于所有顶点的出度之和。
3 任意一个无向图一定有偶数个奇点.

例题1:

  • 一个无向图有16条边(每个点的度至少是2),其中4个度为3,3个度为4,求这个无向图最多有几个点 ?(2003年普及组问题求解)
  • 答案是11

例题2:

  • 一个无向图有4个结点,其中3个的度数为2,3,3,则第4个结点的度数不可能是___________
    A. 0 B. 1 C. 2 D. 4
  • 答案是B

例题3:

  • 假设我们用d=(a1,a2,….a5)表示无向无自环图G的5个顶点的度数,下面给出的哪组值是可能的?
    A.{3,4,4,3,1}
    B.{4,2,2,1,1}
    C.{3,3,3,2,2}
    D.{3,4,3,2,1}
  • 答案是B

另外,树一个特殊的图,n个点,n-1条边。

图里面边的存储方法

一 相邻矩阵

int a[10][10];
用a[i][j]>0 表示i到j有边
a[i][j]==k 表示i到j的边长

优点:写法简单,能在O(1)得出任意两个点是否有边和边长。
缺点:在稀疏图的时候空间浪费太大,找和i相邻的点需要O(n)的时间。

二 数组模拟邻接表(边表)

int a[10][10];
用a[i][0]表示和i相连的点有几个
a[i][j]表示和i相连的第j个点的编号。

读入:

while(m--) {
	int x,y;
	cin >> x >> y;
	a[x][++a[x][0]]=y;
	a[y][++a[y][0]]=x;
}

查找于x相连的点:

for(int i=1; i<=a[x][0]; i++) cout<<a[x][i];

优点: 查询与x相邻的点时间复杂是O(k) 。(k是相邻的点的数量)
缺点: 空间还是需要很大,需要在O(k)时间知道i和j是否有边。

三 利用stl标准模板库里的动态数组vector(前向星)

定义:

vector<int> a;           //a一维数组动态数组
vector<vector<int> > a;  //a二维数组
vector<int> a[100];      //定义了100个一维

注意:第二种定义方法中>和>间必须加空格,否则会编译错误

使用如果a数组拥有第i个元素,那么直接可以用a[i]表示第i个数,注意a数组从0开始

存放(把x放到a数组的最后):

a.push_back(x);   //把x放到a数组的最后

查找所有和x相邻的点:

for(int i=0; i<a[x].size(); i++) cout << a[x][i];

整合:

int n,m;
vector<int> edge[N];

void init() {
    cin >> n >> m;
    for(int i=0; i<m; i++) {
        int x,y;
        cin >> x >> y;         //边连接的两个顶点
        edge[x].push_back(y);  //添边x->y
        edge[y].push_back(x);  //添边y->x
    }
}

优点 :节省空间,找x相邻的需要O(k)的复杂度
缺点: 判断i和j是否有边需要O(k),比自己写的邻接表要慢一些。

四 前向星邻接表(链式前向星)

定义:

struct edge{
	int to,nt;     //to是边的终点,nt(next)是下一条边的序号
} e[边的数量];

int h[N],cnt;      // h[i]表示i的第一条在e里面序号,cnt是边的总数

建边:

inline void add(int a,int b){
	e[++cnt].to=b; 
	e[cnt].nt=h[a]; 
	h[a]=cnt;
} 

读入:

while(m--) {
	int x,y;
	scanf(%d%d”,&x,&y);
	add(x,y);
	add(y,x);
}

枚举与x的相邻的所有点:

for(int i=h[x]; i; i=e[i].nt) cout<<e[i].to;

整合:

struct edge{
	int to,nt; 
} e[边的数量];

int h[N],cnt;

void add(int a,int b) {
	e[++cnt].to=b;
	e[cnt].nt=h[a];
	h[a]=cnt;
}

int main() {
	while (m--) {
		int x,y;
		scanf(%d%d”,&x,&y);
		add(x,y);
		add(y,x);
	}
	for(int i=h[x]; i; i=e[i].nt) cout << e[i].to;
}

图的遍历问题

图的遍历问题是搜索图。
图的搜索分为深度优先搜索和宽度优先搜索两种方法。

深度优先搜索

对下图进行深度优先搜索,写出搜索结果。注意:从A出发。
在这里插入图片描述
从顶点A出发,进行深度优先搜索的结果为:A,B,C,D,E。

对于一个连通图,深度优先遍历的递归过程如下:

void dfs(int i) { //图用邻接矩阵存储
	//访问顶点i;
	visited[i]=1;
	for(int j=1; j<=n; j++)
		if(!visited[j] && a[i][j]) dfs(j)}

以上dfs(i)的时间复杂度为O(n^2)。
对于一个非连通图,调用一次dfs(i),即按深度优先顺序依次访问了顶点i所在的(强)连通分支,所以只要在主程序中加上:

for(int i=1; i<=n; i++)   //深度优先搜索每一个未被访问过的顶点
	if(!visited[i]) dfs(i); 

广度优先搜索(宽度优先搜索)

对下图从A出发进行宽度优先搜索,写出搜索结果。
在这里插入图片描述
从顶点A出发,进行宽度优先遍历的结果为: A,B,C,D,E 。

void bfs(int i) { //宽度优先遍历,图用邻接矩阵表示
	queue<int> q;
	i=q.pop();
	visited[i]=true;
	q.push(i);
	while(!q.empty()) {
		v=q.front();
		q.pop();
		for(int j=1; j<=n; j++) {
			if(!visited[j]) {
  				visited[j]=1;
				q.push(j);
			}
		}
	}
}

时间复杂度是O(n^2).
BFS与DFS的总结:

  • DFS:类似回溯,利用堆栈进行搜索
    BFS:类似树的层次遍历,利用队列进行搜索
  • DFS:尽可能地走“顶点表”
    BFS:尽可能地沿着顶点的“边”进行访问
  • DFS:容易记录访问过的路径
    BFS:不易记录访问过的路径,需要开辟另外的存储空间进行保存路径

图的最短路径算法

分类:

  • 多源最短路径算法:求任意两点之间的最短距离。
    Floyd算法
  • 单源最短路径算法:求一个点到其他所有点的最短路径
    Dijkstra算法,Spfa算法,Bellman-ford算法

Floyd算法

时间复杂度O(n^3)
本质上是一个动态规划
f[i][j]表示i到j最短路径长度
开始的时候,如果i到j有边,那么f[i][j]就是直接的边长,如果没边,f[i][j]就是无穷大。

for(int k=1 ;k<=n; k++) 
	for(int i=1; i<=n; i++) 
		for(int j=1; j<=n; j++)
			f[i][j]=min(f[i][j],f[i][k]+f[k][j]); 

为什么k循环要写在最外面?

这个状态数组本来是3维的。
f[i][j][0]表示i到j的最短路径中间经过了0个点。
f[i][j][0]=edge[i][j] edge[i][j]表示i到j直接的边长
如果i到j边不存在,f[i][j][0]=无穷大。
f[i][j][k]表示i到j的最短路径中间最多经过了1到k这些点
答案就是f[i][j][n]
f[i][j][k]=min(一定没有经过k,一定经过k)
=min(f[i][j][k-1],f[i][k][k-1] + f[k][j][k-1])
把一维舍掉:
f[i][j]=min(f[i][j],f[i][k]+f[k][j]);

Dijkstra算法

思想 :贪心的思想
步骤:
1 标记所有的点都没有求得最短路径,所有的d[i]=无穷大,除了起点的d值是0。
2 循环n次,每次从没有求得最短路径的点里面找出一个d值最小的点,把他标记,用这个点去更新其他没有求得最短路径的点。

void dijkstra() {
	memset(vis,0,sizeof(vis));
	for(int i=1;i<=n;i++) d[i]=inf;
	d[s]=0;
	for(int i=1;i<=n;i++) {
		int k=-1;
		for(int j=1;j<=n;j++)
			if(!vis[j] && (k==-1 || d[k]>d[j])) k=j;
		vis[k]=1;
		for(int j=1;j<=n;j++)
			if(!vis[j] && d[k]+edge[k][j]<d[j])
				d[j]=d[k]+edge[k][j];
	}
}

Spfa算法

设dist代表s到i点的当前最短距离,fa代表s到i的当前最短路径中i点之前的一个点的编号。开始时dist全部为+∞,只有dist[s]=0,fa全部为0。
维护一个队列,里面存放所有需要进行迭代的点。初始时队列中只有一个点S。用一个布尔数组记录每个点是否处在队列中。
每次迭代,取出队头的点v,依次枚举从v出发的边v->u,设边的长度为len,判断dist[v]+len是否小于dist[u],若小于则改进dist[u],将fa[u]记为v,并且由于s到u的最短距离变小了,有可能u可以改进其它的点,所以若u不在队列中,就将它放入队尾。这样一直迭代下去直到队列变空,也就是S到所有的最短距离都确定下来,结束算法。

int const oo=1e9;
vector<int> a[N],b[N];
queue<int> q;
int s,t;
int v[N],d[N];

int spfa() {
	for(int i=1; i<=n; i++) d[i]=oo;
	q.push(s);
	v[s]=1;
	d[s]=0;
	while(!q.empty()) {
		int x=q.front();
		q.pop();
		v[x]=0;
		for(int i=0; i<a[x].size(); i++) {
			int tp=a[x][i];
			if(d[tp]>d[x]+b[x][i]) {
				d[tp]=d[x]+b[x][i];
				if(!v[tp]) {
					q.push(tp);
					v[tp]=1;
				}
			}
		}
	}
	if(d[t]==oo) d[t]=-1;
	return d[t];
}

未完待续……

发布了57 篇原创文章 · 获赞 70 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/Ljnoit/article/details/104448848