图论知识总结(算法模板+复杂度分析+例题总结)

***特别感谢px大佬提供的思路和帮助***

参考博客—1

图论知识总结:(仅仅包含以下几个部分)
1,求最短路的算法和思想:
(1)Floyd-Warshal
(2)Bellman——ford(求负环)
(3)队列优化的Bellman——ford,也就是SPFA(求负环)
(4Dijkstra(不优化&&优化)
2,求最小生成树的算法,kruskal算法--稀疏图,prim算法——稠密图:
3,求树的直径,两次dfs,两次bfs,dp:
4,扩扑排序:
5,差分约束:
6,求树的割点:
7,求树的割边:
8,例题:
***补充:
(1)稀疏图和稠密图
	有很少条边或弧(边的条数|E|远小于|V|²)的图称为稀疏图(sparse graph);
	反之边的条数|E|接近|V|²,称为稠密图(dense graph)。
	稀疏图一般有邻接表来存储。
(2

一,求最短路:

1,Froyd——Warshal算法
(1)算法模板:
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
dis[i][j]=min(dis[i][j], dis[i][k] + dis[k][j] );2)复杂度
时间复杂度:O(N^3)
空间复杂度:O(N^2)
(3)适合条件:
稠密图和顶点的关系密切
(4)
可以解决负权边;
可以用floyd判断图中哪些点之间有关系(下面附上一道题目链接);

题目链接
题目解析

2,Bellman——ford
//注意,无向图和有向图加边,u-v,只需要加一条b即可。
//处理有向边时,只需要处理u-v的情况即可;
//处理无向边时,只需要处理v-u的情况即可。

(1)算法模板:
注:有向图和无向图,两个顶点之间,只存储一条边的信息。
**有向图:
for(int i=1;i<=n;i++)
{
	for(int i=1;i<=n;i++) 
	book[i]=dis[i];
	
	int k=1;//用来判断负环,求最短路时,可以不用写
	for(int j=1;j<=m;j++)
	{
	if(dis[e[j].v] > dis[e[j].u + e[j].w)
	{
	dis[e[j].v] = dis[e[j].u + e[j].w;
	k=0;
	}
	}
	//如果图没有更新,可以提前退出。
	int f=0;
	for(int i=1;i<=n;i++)
	{
	if(dis[i]!=book[i]
	f=1;
	}
	if(f==0)
	break;
}

if(k==0) return -1;//表示有负环,这个解释会在后面的例题中说明。


**无向图:
for(int i=1;i<=n;i++)
{
	for(int i=1;i<=n;i++) 
	book[i]=dis[i];
	//下面处理无向的时候,对于无向边
	//可以考虑加一次边,然后按照下面的情况做;
	//也可以考虑加两次边,然后判断一次就行
	//但是在有的题中,按照加两条边的方法,不对。
	for(int j=1;j<=m;j++)
	{
	if(dis[e[j].v] > dis[e[j].u] + e[j].w)
	dis[e[j].v] = dis[e[j].u] + e[j].w;
	
	//和有向图的区别:(所以说,无向图两点之间也只需要存储一条边)
	//要注意:如果题目是有向边和无向边的混合,
	//要加一个判断,如果当前是有向边,那么下面的语句就不执行
	
	if(dis[e[j].u] > dis[e[j].v] + e[j].w)
	dis[e[j].u] = dis[e[j].v] + e[j].w;
	}
	//如果图没有更新,可以提前退出。
	int f=0;
	for(int i=1;i<=n;i++)
	{
	if(dis[i]!=book[i]
	f=1;
	}
	if(f==0)
	break;
}2)复杂度
空间:O(M)
时间:O(NM)3)适合条件:
稀疏图和边的关系密切
3.SPFA--邻接表存图
(1)算法模板:
//存图(节选重要代码)
struct node
{
	int v,w,next;//v,边的终点;w,边的价值;next,和这条边同起点的下一条边。
}edge[num_e];
int head[num_p];//记录以当前节点为起点的最新的一条边
int pre[num_p];//记录路径
int n,cnt;
void int_i()
{
	cnt=0;
	memset(head,0,sizeof(head));
	for(int i=1;i<=n;i++)
	pre[i]=i;
	return ;
}
void addedge(int a,int b,int c)
{
	edge[++cnt].v=b;
	edge[cnt].w=c;
	edge[cnt].next=head[a];
	head[a]=cnt;
	return ;
}
//递归输出路径
void print_path(int x)
{
	if(x!=pre[x]) print_path(pre[x]);
	printf("%d ",x);
	return ;
}
int spfa(int s,int t) //s点到t点的距离
{
	int dis[num_p];
	int book[num_p];//表示哪个点在队列中
	int neg[num_p];//判断是否存在负环
	int inf=0x3f3f3f3f;
	for(int i=1;i<=n;i++)
	{
	dis[i]=inf;
	book[i]=neg[i]=0;
	}
	dis[s]=0;
	
	queue<int>q;
	q.push(s);
	book[s]=1;

	while(!q.empty())
	{
		int x=q.front();q.pop();
		book[x]=0;
		for(int i=head[x];i;i=edge[i].next)
		{
			int y=edge[i].v;
			if(dis[y] > dis[x] + edge[i].w)
			{
				dis[y] = dis[x] + edge[i].w;
				pre[y]=x;
				if(book[y]==0)
				{
					q.push(y);
					book[y]=1;
					neg[y]++;
					if(neg[y] > n) return -1;//出现负圈
				}
			}
		}
	
	}
	printf("%d\n",dis[t]);
	return 1;
}
int main()
{
	int a,b,c;
	for(int i=1;i<=m;i++)
	{
	//输入时存图
	scanf("%d%d%d",&a,&b,&c);
	addedge(a,b,c);
	//如果是无向图:
	addedge(b,a,c);
	}
	spfa(1,n);
}2)复杂度:
时间:最坏是:O(NM)
空间:O(M)
(3)可以解决的问题:
稀疏图和边的关系密切,可以解决负权边
4,Dijkstra
(1)(算法模板——邻接矩阵存图)
int e[num_p][num_p];
void dijkstra(int s,int t)//s-->t
{
	int dis[num_p];
	int book[num_p];
	for(int i=1;i<=n;i++)
	{
	dis[i]=e[s][i];//初始化dis数组
	book[i]=0;
	}
	book[s]=1;
	dis[s]=0;

	//dijkstra算法核心代码
	for(int i=1;i<=n-1;i++)
	{
		int minn=inf,u;
		//找到距离s点没有被使用过的,目前距离s最短的点
		for(int j=1;j<=n;j++)
		{
			if(book[i]==0&&dis[j] < min )
			{
				min=dis[j];
				u=j;
			}
		}
		book[u]=1;
		for(int j=1;j<=n;j++)
		{
			if(e[u][j] < inf)
			{
				if(dis[j] > dis[u] + e[u][j])
				{
					dis[j]=dis[u] + e[u][j];
				}
			}
		}		
	}
	printf("%d\n",dis[j]);
	return ;
}
int main()
{
	for(int i=1;i<=n;i++)
	for(int j=1;j<=n;j++)
	{
	e[i][j]= i!=j ? inf : 0;
	}

	int a,b,c;
	for(int i=1;i<=m;i++)
	{
	scanf("%d%d%d",&a,&b,&c);
	e[a][b]=c;//无向图;
	}
}

(2)(算法模板--邻接表存图+优先队列优化)--基于Dijkstra算法的基本思想
//如果看了优化版的Dijkstra,有疑惑,可以先去看上面没有优化的Dijkstra
//二者的算法思想一致
struct node
{
	int v,w,next;
	node (){};//c++里的构造函数
	node(int a,int b)
	{
		v=a;w=b;
	}
	bool operator <(const node & s) const//使用了优先队列,对点排序,距离起点近的在前
	{
	return w > s.w;
	}
}edge[num_e];
int head[num_p];//记录以当前节点为起点的最新的一条边
int pre[num_p];//记录路径
int n,cnt;
void int_i()
{
	cnt=0;
	mesmet(head,0,sizeof(head));
	for(int i=1;i<=n;i++)
	pre[i]=i;
	return ;
}
void addedge(int a,int b,int c)
{
	edge[++cnt].v=b;
	edge[cnt].w=c;
	edge[cnt].next=head[a];
	head[a]=cnt;
	return ;
}
void dijkstra(int s,int t)
{
	int dis[num_p];
	int book[num_p];//标记哪个点已经被收缩过了
	for(int i=1;i<=n;i++)
	{
		dis[i]=inf;
		book[i]=0;
	}
	dis[s]=0;
	priority_queue<node>q;
	q.push(node(s,0));

	while(!q.empty())
	{
		node x=q.top();q.pop();
		if(book[x.v])
		continue;
		book[x.v]=1;
		
		for(int i=head[x.v];i;i=edge[i].next)
		{
			int y=edge[i].v;
			if(dis[y] > x.w + edge[i].w)
			{
				dis[y]=x.w + edge[i].w;
				q.push(node(y,dis[y]));
			}
		}
	}
	printf("%d\n",dis[t]);
}

(3)复杂度
未优化的:
时间:O(N^2)
空间:O(M^2)
优化后:
时间:O((M+N)LogN)
空间:O(M)

二,求最小生成树

1**kruskal算法**
(1)算法模板
struct node
{
	int u,v,w;
}edge[num_e];
//并查集
int pre[num_p];
void int_i(void)
{
	cnt=0;
	for(int i=1;i<=n;i++)
	pre[i]=i;
	return ;
}
int fa(int x)
{
	if(x!=pre[x]) return pre[x]=fa(pre[x]);
	return pre[x];
}
int merge(int x,int y)
{
	int tx=fa(pre[x]);
	int ty=fa(pre[y]);
	if(tx!=ty)
	{
		pre[tx]=ty;
		return 1;
	}
	return 0;
}
int cmp(node a,node b) {
	return a.w < b.w;
}
//kruskal算法核心代码
void kruskal(void)
{
	sort(edge+1,edge+cnt+1,cmp);
	int sum=0,c=0;
	for(int i=1;i<=cnt;i++)
	{
		if(merge(edge[i].u,edge[i].v))
		{
		sum+=edge[i].w;
		c++;
		}
		if(c==n-1)
		break;
	}
	printf("%d\n",sum);
	return ;
}2)复杂度分析:
时间:对边的排序,O(E*log2E),并查集的操作O(E),一共O(E*log2(E)+E)
所以可以看出来,如过边很多的话,kruskal算法就很麻烦,所以kruskal算法适合稀疏图,不适合稠密图。

(下面介绍另外一种求最小生成树的代码Prim,适合用于稠密图。)
2,prim算法
(1)算法模板:(来自啊哈c,未优化)
#include
using namespace std;
const int num_p=10010;
const int inf=0x3f3f3f3f;
int n,m;
int e[num_p][num_p],dis[num_p],book[num_p];
void int_i(void)
{
	//ÓÃbookÊý×éÀ´±ê¼ÇÄĸöµãÒѾ­ÔÚÊ÷ÖÐÁË¡£ 
	for(int i=1;i<=n;i++)
	book[i]=0;
	for(int i=1;i<=n;i++)
	for(int j=1;j<=n;j++)
	e[i][j]= i!=j ? inf : 0;
	return ;
}
int main()
{
	scanf("%d%d",&n,&m);
	int_i();
	int a,b,c;
	for(int i=1;i<=m;i++)
	{
		scanf("%d%d%d",&a,&b,&c);
		e[a][b]=c;
		e[b][a]=c;
	}
	
	for(int i=1;i<=n;i++)
	dis[i]=e[1][i];
	
	//primËã·¨ºËÐÄ£»
	int cnt=0,sum=0;
	book[1]=1; 
	++cnt;
	
	while(cnt<n)
	{
		int minn=inf,u;
		for(int i=1;i<=n;i++)
		{
			if(book[i]==0&&dis[i]<minn)
			{
				minn=dis[i];
				u=i;
			}
		}
		
		book[u]=1;
		cnt++;
		sum+=dis[u];
		
		//¸üÐÂδ¼ÓÈëÉú³ÉÊ÷µÄ½Úµãµ½Éú³ÉÊ÷µÄ¾àÀë¡£ 
		for(int i=1;i<=n;i++)
		{
			if(book[i]==0&&dis[i] > e[u][i])
			dis[i]=e[u][i];	
		} 
	}
	
	printf("%d\n",sum); 
	return 0;	
}2)复杂度:
时间:未优化:O(N^2);优化后的时间复杂度:O(M*log2(N))3)对比:(算法的具体选择看题目要求)
Kruskala        O(M*log2(M)+M)
Prim(优化)      O(M*log2(N))
Prim(未优化)    O(N^2)

三,求树的直径:

1,dp数组实现树的直径
//f1 记录从一个点出发的最长单向路径,f2记录从一个点出发第二长单向路径
//f1+f2,就是这个点最大的可以达到的最大长度,枚举所有的点的结果,求最大值即可

//邻接表存图
struct node
{
	int v,w,next; 
}edge[num_e];
int head[num_p];

int f1[num_p],f2[num_p];
int ans=0,cnt;
void int_i(void)
{
	ans=0;
	cnt=0;
	memset(head,0,sizeof(head));
	return ;
}
void addedge(int a,int b,int c)
{
	edge[++cnt].v=b;
	edge[cnt].w=c;
	edge[cnt].next=head[a];
	head[a]=cnt;
	return ;
}
void dp(int cur,int father)
{
	for(int i=head[cur];i;i=edge[i].next)
	{
		int y=edge[i].v;
		if(y==father) //想象以下,有一个点,向四周发散出几条边,有一条边是它刚刚来的边,就是father--》cur的边,这个边要去掉
		continue;
		dp(y,cur);//继续向下递归
		//满足条件的话更新最大值和次大值
		if(f1[y] + edge[i].w > f1[cur])
		{
			f2[cur]=f1[cur];
			f1[cur]=f1[y] + edge[i].w;
		}
		else if(f1[y] + edge[i].w >f2[cur])//只更新次大值
		{
			f2[cur]=f1[y] + edge[i].w;
		}
		ans=max(ans,f1[cur] + f2[cur]);
	}

	return ;
}

2,两次dfs实现树的直径(第一次自己写dfs版本的(还没跑过,我相信它是对的,**嘻嘻**),就写个比较完整的吧,pxgg太强了)
#inlude
using namespace std;
const int num=10000;
struct node
{
	int v,w,next;
}edge[num];
int head[num];
//book标记哪些点加入到了直径中
int book[num];
//temp和date用来保存哪些点在树的直径上。
int temp[num];
int date[num];
int cnt,ans,f,n,m;
void int_i(void)
{
	cnt=0;
	ans=0;
	memset(point,0,sizeof(point));
	memset(head,0,sizeof(head));
	return ;
}
void addedge(int a,int b,int c)
{
	edge[++cnt].v=b;
	edge[cnt].w=c;
	edge[cnt].next=head[a];
	head[a]=cnt;
	return ;
}
void dfs(int x,int sum,int id)
{
	book[x]=1;//标记哪些点被访问过了
	temp[id]=x;//记录路径
	if(sum>ans)
	{
		ans=sum;//更新最大值
		for(int i=1;i<=id;i++)
		date[i]=temp[i];//保存路径
		f=x;
	}
	
	//以这个点为中心,向四周扩散,递归这些点;
	for(int i=head[x];i;i=edge[i].next)
	{
		if(!book[edge[i].v])
		dfs(edge[i].v,sum+edge[i].w,id+1);
	}
	return ;
}
int main()
{
	int x,y,z;
	int_i();
	while(scanf("%d%d%d",&x,&y,&z)!=EOF)
	{
		add(x,y,z);
		add(y,x,z);
	}
	ans=0;
	memset(book,0,sizeof(book));
	dfs(1,0,0);
	memset(book,0,sizeof(book));
	dfs(f,0,0);
	
	printf("%d\n",ans);
	return 0;
}

3,bfs求树的直径(还没掌握,预计这周之前补充)

四,扩扑排序

1,(算法模板)(确定是否有KP有序,不按字典序输出序列)
#define update(a,n) for(int i=1;i<=n;i++) a[i]=0  //取代memset的使用,因为看了一些东西说,memset会增加复杂度!!!!真的
struct node
{	
	int v,next;
}edge[num_e];
int head[num_p];
int point[num_p];
int cnt,n;
void int_i(void)
{
	cnt=0;
	update(point,n);
	update(head,n);
	return ;
}
void add(int a,int b) // a--> b 注意这个顺序要符合题目排序的要求
{
	edge[++cnt].v=b;
	edge[cnt].next=head[a];
	head[a]=cnt;
	
	point[b]++;
	return ;
}
void KPsort(void)
{
	int ans[n];
	int c=0;
	int book[n];
	update(book,n);
	
	queue<int>q;
	for(int i=1;i<=n;i++)
	{
		if(point[i]==0)
		{
			q.push(i);
			ans[c]=i;
			c++:
		}
	}
	
	while(!q.empty())
	{
		int k=q.front();q.pop();
		for(int i=head[k];i;i=edge[i].next)
		{
			int y=edge[i].v;
			point[y]--;
			if(point[y]==0)
			{
				q.push(y);
				ans[c]=y;
				c++;
			}
		}
	}
	if(c!=n)
	printf("存在环\n");
	else 
	{
		for(int i=0;i<n;i++)
		printf("%d ",ans[i]);
	}
	return ;
}

五,差分约束
如果对差分约束还不了解,可以先看一下这个博客。
参考博客2

目前差分约束只接触到一道题,不敢说太多,(以后补充)
下面分享一下做的第一道也是唯一一道差分约束的题目。

题目链接

*****总结一下我看到的博客里面的内容:
差分约束的问题可以转化为求最大路径和最小路经的问题。
博客里证明得出,可以将不等式化成:
Xn - X1 <=sum(a1+----+ak);1)
或者
Xn - X1 >=sum(a1+---+ak);2)
这两个公式有什么区别呢?
sum(a1+---+ak)是我们可以求的路径
如果要求Xn - X1的**最大值**,就要用公式 (1),然后求出 1和n 之间的**最小路径**;(sum去最小,Xn-X1取最大,二者不就相等;)
如果要求Xn - X1的**最小值**,就要用公式 (2),然后求出 1和n 之间的**最大路径*****注意,特别的,如果题目给出的是a+b==c这样的关系,要先把这个公式转化为:
a+b>=c和a+b<=c;***
**题目大体意思:
*N头奶牛,分别有ML个关系,和MD个关系.
*例如:满足ML关系的奶牛a 和 b,之间最大的距离是c,且,a的位置在b的左面,即:b-a<=c;
*a b c.
*满足MD关系的奶牛a 和 b,之间最小的距离是c,且,a的位置在b的左面,即:b-a>=c;
*a b c.
*要求求出(1)这N头奶牛在满足这(ML+MD)种关系时第一头和最后一头奶牛的**最大距离**。(2)如果这个距离可以无限大,则输出-2*3)如果不能满足题目中的关系,输出-1
									&&&解题思路&&&
题目最后让求的是最短路径问题,那么可以将约束关系转化为求最小路径的问题,问题是怎么转化呢??
·将输入的关系转化为边的关系
·因为存在负边,用SPFA算法解决最短路。
·求出上面提出的(1)问题,就是求出最短路径,求出(2)问题,判断二者之间的距离是否为inf;
求出(3)问题,判断是否有负环,如果有负环,又因为Xn - X1 <=sum(``); sum>=0,有负环,求不出最段路径,sum<0
//基于vector的SPFA。
#include
#include
#include
#include
#include
using namespace std;
const int inf=0x3f3f3f3f;
const int num=1010;
int n,m1,m2;
struct edge
{
	int u,v,w;
	edge(int a,int b,int c)
	{
		u=a;v=b;w=c;
	}
};
vector<edge>e[num];
int spfa(int s)
{
	int dis[num];
	bool book[num];
	int neg[num];//是否存在负环
	 
	for(int i=1;i<=n;i++)
	{
		dis[i]=inf;
		neg[i]=0;
		book[i]=false;
	}	
	dis[1]=0;
	
	queue<int>q;
	
	q.push(s);
	book[s]=true;
	
	while(!q.empty())
	{
		int u=q.front();
		q.pop();
		book[u]=false;
		
		for(int i=0;i<e[u].size();i++)
		{
			edge x=e[u][i];
			if(dis[x.v] >= dis[u] + x.w)
			{
				dis[x.v]=dis[u]+x.w;
				if(book[x.v]==false)
				{
					book[x.v]=true;
					q.push(x.v);
					neg[x.v]++;
					if(neg[x.v] > n) return -1;
				}
			} 
		}
	}
	
	if(dis[n]==inf) dis[n]=-2;
	
	return dis[n]; 
}
int main()
{
	scanf("%d%d%d",&n,&m1,&m2);
	
	int a,b,c;
	//这些关系表示,b-a<=c;求Xn-X1的最大值,因此把公式转化为 <=  的样子。
	for(int i=1;i<=m1;i++)
	{
		//b-a<=c;b<=a+c;即是,以a为起点,b为终点,c为价值的一条边
		scanf("%d%d%d",&a,&b,&c);
		e[a].push_back(edge(a,b,c));
	}
	//把公式转化为 <=  的样子
	for(int i=m1+1;i<=m2+m1;i++)
	{
		//b-a>=c;a-b<=-c;a<=b+(-c);即是以b为起点,a为终点,(-c)为价值的一条边
		scanf("%d%d%d",&a,&b,&c);
		e[b].push_back(edge(b,a,-c));
	}
	
	printf("%d\n",spfa(1));

	return 0;
}

六,求树的割点

算法模板(思路来自啊哈c,邻接表优化)
(这里没有这个算法的解释,如果你对啊哈c上的求割点还不了解,建议你,耐心地多看几遍)

//邻接表如何存图,前面算法模板的板块里有。
//第一个数组low[]用来记录点a在不经过某一点b可以达到的最小的祖先的编号。
//num[]用来记录该点的编号;
//flag[]用来标记哪个点是割点。
int low[N],num[N],flag[N];
int index=0;//”时间点“ 第几次到达的。
void dfs(int cur,int father)//cur是当前点,father--》cur的关系
{
	int child=0;
	num[cur]=++index;
	low[cur]=index;
	for(int i=head[cur];i;i=edge[i].next)
	{
		int y=edge[i].v;
		if(num[y]==0)//还没有到达过该点
		{
			child++;
			dfs(y,cur);//接着递归下去
			low[cur]=min(low[cur],low[y]);
			if(cur!=1&&low[y] >= num[cur])
			{
				flag[cur]=1;
			}
			if(cur==1&&child==2)
			{
				flag[cur]=1;
			}
		}
		else if(cur!=father)
		{
			low[cur]=min(low[cur],num[y]);
		}
	}
	return ;
}

七,求树的割边

***算法模板(算法思路来自啊哈C+邻接表优化)
//下面内容和求割点的算法大部分相同,这里标记的是那条边是割边。
int low[N],num[N],flag[N];
int index=0;
void dfs(int cur,int father)
{
	num[cur]=index++;
	low[cur]=index;
	for(int i=head[cur];i;i=edge[i].next)
	{
		int x=edge[i].v;
		if(num[x]==0)
		{
			dfs(y,cur);
			low[cur]=min(low[cur],low[x]);
			//和求割点的不同之处
			if(low[y] > now[cur])
			{
				flag[i]=1;
			}
		}
		else if(x!=father)
		{
			low[cur]=min(low[cur],low[x]);
		}
	}
	return ;
}

八,例题
Silver Cow Party POJ - 3268

你可能感兴趣的:(暑假训练图论第二周,模板,图论)