【整理】基础图论模板题及知识点汇讲

有兴趣的朋友可以去我的洛谷博客康康哦qwq

本篇文章洛谷博客传送门
我的博客总版传送门

特别特别感谢:

lmpp大佬牺牲自己宝贵时间,为我没有脾气的耐心讲解。

lmpp大佬的博客:墙裂建议进去康一康!

还有gmq、gbf同学,感谢你们的鼓励与支持!!

did教给我知识真是太强了!!

没有他们就没有这篇博客,我也会比现在蒻上 i n f inf inf倍。


提示:

本博客公开, 但“例题部分”仅记录的是本人认为较有意义添加的题或者是本人的知识盲区。 我会以知识点为经,算法为纬,在知识点开头加入模板题并附有代码,有的时候会写一点小提示,即 tips: \mathfrak\color{CornflowerBlue}\colorbox{Lavender}{tips:} tips:

在我自己掌握特别不好的题目/知识点旁会加以 ! \color{Red}\colorbox{Yellow}{!} 的标记。

所有题号以BSOJ为准。

有什么错误纰漏的直接QQ+洛谷私信+讨论区留言,我真的超级需要您的反馈的qwq。

希望能您能从这份清单中找到您的一些知识漏洞并把他们补起来!


更新信息

2020.3.29 1.0版本,开始新的篇章。知识点记录至拓扑排序模板题。

2020.3.30 && 2020.3.31 2.0版本,爆肝完所有知识点。


拓扑排序:

1462 拓扑排序

tips1: \mathfrak\color{CornflowerBlue}\colorbox{Lavender}{tips1:} tips1:

拓扑排序的基本知识:

  • 在图论中,拓扑排序是一个有向无环图(DAG)的所有顶点的线性序列,该序列必须满足下面两个条件:

    1. 每个顶点出现且只出现一次。
    2. 若存在一条从顶点A到顶点B的路径,那么在序列中顶点A出现在顶点B的前面。
  • 有向无环图(DAG)才有拓扑排序,非DAG图没有拓扑排序一说。 [ 1 ] ^{[1]} [1]

  • 拓扑排序的操作方法:

    1. 从DAG图中选择一个 没有前驱(即入度为零) 的顶点并输出。
    2. 从图中删除该顶点和所有以它为起点的有向边。
    3. 重复1和2直到当前的DAG图为空或当前图不存在无前驱的顶点为止。后一种情况说明有向图中必然存在环。
  • 通常,一个DAG图可以有一个或多个拓扑序列。

[ 1 ] ^{[1]} [1]:拓扑排序之所以只能针对于DAG图,就是因为它有每次取出入度为0的顶点的操作,如果有环,则环中的顶点不存在入度为0的点,无法进行拓扑排序。

tips2: \mathfrak\color{CornflowerBlue}\colorbox{Lavender}{tips2:} tips2:

关于拓扑排序的应用

拓扑排序一般很少有单独针对该知识点的题,但是在关键路径和平常的其他例题的辅助操作中却发挥着重要作用。所以掌握好拓扑排序是很重要的事情。

tips3: \mathfrak\color{CornflowerBlue}\colorbox{Lavender}{tips3:} tips3:

本代码按字典序输出的部分:

int j=1; //从第一个点开始查找
while(j<=n&&bein[j])
	j++; //统计入度为零的节点
//由于-1的bool值也视作真,所以可以标记为-1
if(j>n) return 0; //如果统计的节点超出了范围n,说明这个图有环
sum[++top]=j; //拓扑序列答案数组统计新答案
//本代码由邻接矩阵实现,按字典序输出,复杂度O(n^2)

#include 
#include 

#define maxn 205
using namespace std;

int bein[maxn]; //bein[i]表示节点i的入度
int a[maxn][maxn]; //邻接矩阵存图
int sum[maxn],top; //拓扑序列答案数组
int n,m;

int TS()
{
	for(int i=1;i<=n;i++)
	{
		int j=1; //从第一个点开始查找
		while(j<=n&&bein[j])
			j++; //统计入度为零的节点
        //由于-1的bool值也视作真,所以可以标记为-1
		if(j>n) return 0; //如果统计的节点超出了范围n,说明这个图有环
		sum[++top]=j; //拓扑序列答案数组统计新答案
		bein[j]=-1; //标记此点已经遍历
		for(int k=1;k<=n;k++)
			if(a[j][k]) //如果j和k之间有边相连
				bein[k]--; //和j相关联的节点删除与j相连的边,即入度--
	}
	return 1; //如果遍历过程中没有返回过假值,则有解,返回真值
}

int main()
{
	cin>>n>>m;
	for(int i=1;i<=m;i++)
	{
		int x,y;
		cin>>x>>y;
		a[x][y]=1; //邻接矩阵储存单向边
		bein[y]++; //输入是从x到y的连线,所以是y的入度增加
	}
	if(TS()) //如果有解
		for(int i=1;i<=n;i++)
			cout<

最小生成树

  • 最小生成树:在一张带权的无向连通图中,各边权和为最小的一颗生成树即为最小生成树。

    简单讲:找出连接所有点最低成本路线

  • 最小边原则 [ 2 ] ^{[2]} [2]:图中权值最小的边(如果唯一的话)一定在MST上。

  • 唯一性 [ 3 ] ^{[3]} [3]:一颗最小生成树上,如果各边的权都不相同,则最小生成树是唯一的。

[ 2 ] ^{[2]} [2] [ 3 ] ^{[3]} [3]:这些补充的知识主要用于Prim算法的使用。

Kruskal算法

tips1: \mathfrak\color{CornflowerBlue}\colorbox{Lavender}{tips1:} tips1:

Krudkal算法的基本知识:

  • Kruskal算法是一种贪心算法,它是将边按权值排序,每次从剩下的边集中选择权值最小且两个端点不在同一集合的边加入生成树中,反复操作,直到加入了(n-1)条边。 [ 4 ] ^{[4]} [4]

  • Kruskal的操作方法:

    1. 按图中的边按权值从小到大快排。
    2. 按照权值从小到大依次选边,若当前选取的边加入后使生成树形成环,则舍弃当前边;否则标记当前边并计数。
    3. 重复2的操作,直到生成树中包含(n-1)条边为止;否则当遍历完所有的边后,都不能选取(n-1)条边,表示最小生成树不存在。
  • 算法的关键在于如何判定新加入的边会不会使图产生环,在这里使用并查集。如果新加入的边两个端点在并查集的同一个集合中,说明存在环,需要舍弃这条边;否则保留当前边,并合并涉及的两个集合。

[ 4 ] ^{[4]} [4]:简单地说,就是将从小到大的边依次选入ans树中,若操作的两个两条边不属于同一集合(即不构成环),则加入ans树并累加值,反之则不作操作。

tips2: \mathfrak\color{CornflowerBlue}\colorbox{Lavender}{tips2:} tips2:

并查集的基本操作:

  1. 找根节点:
int GetFather(int x)
{
	if(prt[x]==x) return x;
    return prt[x]=GetFather(prt[x]);
}
  1. 合并操作(一般可以直接写在主函数里):
void Add(int x,int y)
{
	int f1=GetFather(x);
    int f2=GetFather(y);
    if(f1!=f2) prt[f1]=f2;
}

1449 最小生成树

#include 
#include 
#include 

#define maxn 40005
using namespace std;

int n,m;
int prt[maxn]; //prt[i]表示节点i的根节点
int ans=0; //最小生成树的总权值和
bool flag; //标记是否有解

struct Edge
{
	int x,y,v;
}a[maxn]; 
//x[i]代表a[i]这条边的起点
//y[i]代表a[i]这条边的终点
//v[i]代表a[i]这条边的权值

bool cmp(Edge x,Edge y) //结构体数组排序函数
{
	return x.v>n>>m;
	for(int i=1;i<=m;i++)
		cin>>a[i].x>>a[i].y>>a[i].v;
	for(int i=1;i<=n;i++)
		prt[i]=i; //并查集初始化
	sort(a+1,a+m+1,cmp); //按边权从小到大排好,以便贪心操作
	K();
	if(!flag) //如果有最小生成树,则输出最小权值
		cout<

Prim算法

tips1: \mathfrak\color{CornflowerBlue}\colorbox{Lavender}{tips1:} tips1:

Prim算法的操作方法:

  • 将1号节点置入集合S中。

  • 找到所有连接S中的节点和非S中的节点的边中权值最小的那一条,并标记这条边,同时将连接的非S中的节点加入S集合。

  • 重复2步骤,知道左右节点在S中。

(sorry,这里盗个did的图~)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fR17UXKH-1585646967612)(https://s1.ax1x.com/2020/03/30/GusFwd.png)]

简单来说,就是S集合中的元素从一个根节点1开始,以一个集合整体出动的方式来扩展其他的点,以获取最小生成树。

1449 最小生成树

#include 
#include 
#include 

#define maxn 1005
using namespace std;

int a[maxn][maxn]; //元素数组
int d[maxn]; //d[i]表示不是生成树中点i到当前生成树中点的最小值
bool vis[maxn]; //vis[i]标记顶点i是否加入最小生成树中
int ans[maxn]; //答案数组

int n,m;
int sum;

int P(int x)
{
	memset(vis,0,sizeof(vis));
	for(int i=1;i<=n;i++)
		d[i]=0x3f3f3f; //初始化
	d[x]=0; //第一个节点(即根节点)距离根节点(即自己)的距离为0
	int k;
	for(int i=1;i<=n;i++)
	{
		int minn=0x3f3f3f; //统计最小边权
		for(int j=1;j<=n;j++)
			if(!vis[j]&&d[j]>n>>m;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			a[i][j]=0x3f3f3f; //初始化
	for(int i=1;i<=m;i++)
	{
		int x,y,v;
		cin>>x>>y>>v;
		a[x][y]=v;
		a[y][x]=v; //邻接数组建双边
	}
	P(1); //从1开始遍历
	cout<

最短路径问题

  • 最短路径:在一个有权图中连接给定两个顶点的权值和最小的路径。

  • 最短路树和最小生成树可以不一样

  • 一般有两类最短路问题:

    1. SSSP(单源最短路):求给定起点S到其他所有点的最短路,常见算法有Dijkstra算法、SPFA算法等。
    2. APSP(多源最短路):求任意两对顶点之间的最短路,常见算法有Floyed算法。

Dijkstra算法

tips1: \mathfrak\color{CornflowerBlue}\colorbox{Lavender}{tips1:} tips1:

三角形性质

设源点S到点x、y的最短路径长度为d[x]、d[y]。x与y之间的距离是g[x][y],则有下面的“三角形定理”:

d [ x ] + g [ x ] [ y ] > = d [ y ] d[x]+g[x][y]>=d[y] d[x]+g[x][y]>=d[y]

tips2: \mathfrak\color{CornflowerBlue}\colorbox{Lavender}{tips2:} tips2:

松弛

若在处理过程中,有两点x、y出现不符合“三角形定理”,则可“松弛一下”:

i f ( d [ x ] + g [ x ] [ y ] < d [ y ] ) d [ y ] = d [ x ] + g [ x ] [ y ] ; if(d[x]+g[x][y]if(d[x]+g[x][y]<d[y])d[y]=d[x]+g[x][y];

tips3: \mathfrak\color{CornflowerBlue}\colorbox{Lavender}{tips3:} tips3:

Dijkstra算法的操作方法

  • 初始化d[v0]=0,源点到其他点的距离值d[i]=inf。

  • 经过n次如下步骤操作,最后得到v0到n个顶点的最短距离:

    1. 选择一个未标记的点k并且d[k]的值是当前最小的。
    2. 标记点k,即vis[k]=1.
    3. 以k为中间点,修改源点v0到其他未标记点j的距离值d[j]。

tips4: \mathfrak\color{CornflowerBlue}\colorbox{Lavender}{tips4:} tips4:

个人小议Dijkstra算法与Prim算法的不同

  • 目的上,Prim试求最小生成树,Dijkstra试求最短路

  • 距离上,Prim是让整个集合看距离,而这个集合是一堆边
    Dijkstra是从原点开始更新距离,然后点连点更新距离;

  • 连点上,Prim是一个集合(边)连所有点
    Dijkstra是一个点只能连直接与自己相关联的点,在开始连接新的点的时候,从最小的点开始继续访问

  • Dijkstra之所以选当前最小的点开始继续连点是因为最小的点可以有期望让到其他的点的边权尽量小

  • Prim算法数组里存的是点到集合的距离
    Dijkstra算法数组里存的是所有点到总的原点1的距离

1428 最短路径问题

#include 
#include 
#include 

#define maxn 1005
using namespace std;

double a[maxn]; //a[i]表示不是生成树中点i到源点的最小值
double g[maxn][maxn]; //邻接数组存边
int prt[maxn]; //记录自己的值是从哪个节点来的,即为相对的父亲节点
bool vis[maxn]; //标记是否访问过
int s,t; //起点和终点
int n,m;

struct Node
{
	int x,y;
}q[maxn]; //储存坐标

double len(Node a,Node b) //通过x、y坐标计算边的长度
{
	return sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y));
}

void D()
{
	for(int i=1;i<=n;i++)
		a[i]=999999.9; //初始化
	int k;
	a[s]=0; //第一个节点(即根节点)距离根节点(即自己)的距离为0
	prt[s]=0; //第一个节点的父亲定义为0
	for(int i=1;i<=n;i++)
	{
		double minn=999999.9; //找最小边权
		for(int j=1;j<=n;j++)
			if(!vis[j]) //如果未访问过
				if(a[j]0&&!vis[j]) //更新与该节点有连接的点的边权值
				if(a[k]+g[k][j]>n;
	for(int i=1;i<=n;i++)
	{
		int x,y;
		cin>>x>>y;
		q[i].x=x;
		q[i].y=y; //记录坐标
	}
	cin>>m;
	for(int i=1;i<=m;i++)
	{
		int x,y;
		cin>>x>>y;
		double v=len(q[x],q[y]); //计算边权
		g[x][y]=v;
		g[y][x]=v; //邻接矩阵建双边
	}
	cin>>s>>t;
	D();
	printf("%.2f",a[t]); //直接输出节点t距离源点s的距离
	return 0;
}

Floyed算法

tips1: \mathfrak\color{CornflowerBlue}\colorbox{Lavender}{tips1:} tips1:

Floyed算法的基本知识

  • Floyed算法是DP思想。

  • Floyed算法可以求出每队点之间的最短距离,它对于图的要求是,可以是无向图和有向图,边权可正可负,唯一的要求是不能有负环。

tips2: \mathfrak\color{CornflowerBlue}\colorbox{Lavender}{tips2:} tips2:

Floyed算法的基本思想

  • 初始化f[i][j]=w[i][j],从小到大枚举k,对每对节点(u,v),检查它们的最短路值。

  • f[i][j]表示该状态下(即路径中间只允许经过节点i~k的情况下,k递增,定义在循环内),i到j的最短路距离
    其状态转移方程为:

    1. 最短路经过点k f [ i ] [ j ] = f [ i ] [ k ] + f [ k ] [ j ] ; f[i][j]=f[i][k]+f[k][j]; f[i][j]=f[i][k]+f[k][j];
    2. 最短路不经过点k f [ i ] [ j ] = f [ i ] [ j ] f[i][j]=f[i][j] f[i][j]=f[i][j](这里等号右边的f[i][j]实为上一阶段的f[i][j])

1584 银行设置

tips3: \mathfrak\color{CornflowerBlue}\colorbox{Lavender}{tips3:} tips3:

初始化

f [ i ] [ j ] = 0 , i = = j ; f[i][j]=0,i==j; f[i][j]=0,i==j;

f [ i ] [ j ] = i n f , i ! = j ; f[i][j]=inf,i!=j; f[i][j]=inf,i!=j;

#include 
#include 

#define maxn 1005
#define inf 0x3f3f3f
using namespace std;

int f[maxn][maxn]; //操作数组,f[i][j]表示该状态下(即路径中间只允许经过节点i~k的情况下,k递增,定义在循环内),i到j的最短路距离
int n,m;
int a[maxn][maxn]; //

void Floyed()
{
	for(int k=1;k<=n;k++) //枚举中间点
		for(int i=1;i<=n;i++) //枚举起点
			for(int j=1;j<=n;j++) //枚举终点
				if(f[i][k]!=inf&&f[k][j]!=inf)
					f[i][j]=min(f[i][j],f[i][k]+f[k][j]);
}

int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			if(i==j) f[i][j]=0;
			else f[i][j]=inf; //初始化,自己到自己的最短路设置为0,
	for(int i=1;i<=m;i++)
	{
		int x,y,v;
		cin>>x>>y>>v;
		f[x][y]=f[y][x]=v; //邻接矩阵建边
	}
	Floyed();
	for(int k=1;k<=n;k++)
		for(int i=1;i

SPFA算法

tips1: \mathfrak\color{CornflowerBlue}\colorbox{Lavender}{tips1:} tips1:

SPFA算法的基本知识:(摘录自did的PPT,归纳的很好就直接搬运了qwq)

  • 设立一个先进先出的队列用来保存待优化的节点,优化时每次取出队首节点u,并且**用u点当前的最短路径估计值对u点所指向的节点v进行松弛操作,如果v点的最短路径估计值有所调整,且v点不在当前的队列中,就将v点放入队尾。**这样不断从队列中取出节点来进行松弛操作,直至队列空为止。这个算法保证只要最短路径存在,SPFA算法必定能求出最小值。

  • SPFA算法同样可以判断负环,如果某个点弹出队列的次数超过n-1次,则存在负环。对于存在负环的图,无法计算单源最短路径。

tips2: \mathfrak\color{CornflowerBlue}\colorbox{Lavender}{tips2:} tips2:

SPFA算法的基本操作:(摘录自did的PPT,归纳的很好就直接搬运了qwq)

  • S为源点,vst[]记录点是否在队列中,距离值为dist[]
  1. 初始化,源点的距离dist[S]=0,其它点的距离设为INF,新建一个队列,将源点S入队,标记源点S已经在队列中。

  2. 从队首取出一个点i,标记i已经出队,接着对和i点有边相连的j点进行松弛操作,如果松弛成功,则对i入队的次数进行检查,如果大于等于n,说明出现负环,算法结束;否则改进dist[j]的值,再检查j是否在队列中,如果不在,就将j点加入队尾。

  3. 重复执行步骤2,直到队列为空。

tips3: \mathfrak\color{CornflowerBlue}\colorbox{Lavender}{tips3:} tips3:

常见疑惑

  1. Q:SPFA算法的复杂度?
    A:玄学,平均复杂度为O(2E),其大时可以极大,小时可以极小。

  2. Q:在算法中,如果后出队的点可以使前面的点更忧,而那个点的值改变了,它的子节点的值也会改变。这个时候需要入队吗?
    A:需要,而且不用担心vis[i]=1的问题。当我们出队以后vis[i]会归零的。

1580 最短路(Spfa)2885

#include 
#include 
#include 
#include 

#define maxn 500010
using namespace std;

queue  q;

int vis[maxn]; //vis[i[]表示点i是否在队中
int d[maxn]; //d[i[]表示点i距离源点的最短路

struct Edge
{
	int to,next,v;
}a[maxn<<1]; //前向星结构体数组


int head[maxn],cnt;
//int used[maxn]; //这句代码可用于判断有无解时使用,本题保证有解,所以不需要了
int n,m;

void AddEdge(int x,int y,int v) //前向星建边操作
{
	a[++cnt].next=head[x];
	a[cnt].to=y;
	head[x]=cnt;
	a[cnt].v=v;
}

int Spfa(int x)
{
	for(int i=1;i<=n;i++)
		d[i]=0x3f3f3f3f;
//	memset(vis,0,sizeof(vis));
	q.push(x);
	vis[x]=1;
	d[x]=0;
	while(!q.empty())
	{
		int u=q.front();
		q.pop();
		vis[u]=0; //因为点u已出队,释放点u
		for(int i=head[u];i;i=a[i].next)
		{
			int v=a[i].to;
			if(d[v]>d[u]+a[i].v)
			{
//				used[v]++;
//				if(used[v]>n) return 0; //这句代码可用于判断有无解时使用,本题保证有解,所以不需要了
				d[v]=d[u]+a[i].v; //更新边权
				if(!vis[v]) //如果点v可以遍历(可以添加)
				{
					vis[v]=1; //标记已在队中
					q.push(v); //则入队
				}
			}
		}
	}
//	return 1; //这句代码可用于判断有无解时使用,本题保证有解,所以不需要了
}

int main()
{
	cin>>n>>m;
	for(int i=1;i<=m;i++)
	{
		int x,y,v;
		cin>>x>>y>>v;
		AddEdge(x,y,v);
		AddEdge(y,x,v); //前向星建双边
	}
//	if(Spfa(1))
//		cout<

! \color{Red}\colorbox{Yellow}{!}

有向图的连通性

关于有向图的相关概念

  • 强连通图:有向图中,如果对每一对Vi,Vj(Vi,Vj属于V,Vi不等于Vj)。从Vi到Vj和从Vj到 Vi都存在路径,则称G是强连通图。
    简单来说,即:如果一张有向图中任意两点有路径可以互相到达,则称这张图是强连通图

  • **强连通分量:**有向图的极大强连通子图叫强连通分量。

  • **最关键通用部分:**强连通分量一定是图的深搜树的一个子树。

Tarjan算法

tips1: \mathfrak\color{CornflowerBlue}\colorbox{Lavender}{tips1:} tips1:

Tarjan算法的基本知识:(摘录自did的PPT,归纳的很好就直接搬运了qwq)

  • Tarjan算法是基于对图深度优先搜索的算法,每个强连通分量为搜索树中的一棵子树。搜索时,把当前搜索树中未处理的节点加入一个堆栈,回溯时可以判断栈顶到栈中的节点是否为一个强连通分量。

  • 定义DFN(u)为节点u搜索的次序编号(时间戳),Low (u)为u或u的子树能够追溯到的最早的栈中节点的次序号。

  • 当DFN(u)=Low(u)时,以u为根的搜索子树上所有节点是一个强连通分量。

tips2: \mathfrak\color{CornflowerBlue}\colorbox{Lavender}{tips2:} tips2:

Tarjan算法的基本操作

  • 找一个没有被访问过的节点u;否则,算法结束;(图不连通)

  • 初始化dfn[u]和low[u]
    对于u所有的邻接顶点v:

    1. 如果没有访问过,则转到步骤(2),同时维护low[u];
    2. 如果访问过,但没有删除,维护low[u];
  • 如果low[u]==dfn[u],那么输出相应的强连通分量。

1572 消息的传递2269

#include 
#include 
#include 

#define maxn 1005
#define inf 0x7fffffff/2
using namespace std;

stack  q;
int in[maxn][maxn]; //入度
int DFN[maxn],LOW[maxn]; 
int from[maxn]; //找所属 
bool flag[maxn]; //标记数组
int bein[maxn]; //强连通分量入度 

int cnt;
int t; //时间戳 
int y;
int n;

void Tarjan(int x)
{
	DFN[x]=LOW[x]=++t; //给x按照访问顺序的先后标号为t,给LOW[x]赋初始值
	q.push(x); //x点进栈
	flag[x]=1; //这个用来判断横叉边
	for(int i=1;i<=n;i++)
		if(in[x][i]) //边xi没有被标记过
			if(!DFN[i]) //i未被标记过
			{
				Tarjan(i); //xi是父子边,递归访问
				LOW[x]=min(LOW[i],LOW[x]);
			}
			else if(flag[i])
				LOW[x]=min(DFN[i],LOW[x]); //xi是返祖边
	if(LOW[x]==DFN[x]) //统计块数
	{
		cnt++;
		while(y!=x)
		{
			y=q.top();
			q.pop();
			from[y]=cnt; //定义当前快的编号为cnt
			flag[y]=0; //释放标记
		}
	}
}

int main()
{
	cin>>n;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			cin>>in[i][j]; //邻接矩阵存储边关系
	for(int i=1;i<=n;i++)
		if(!DFN[i]) Tarjan(i); //如果没有遍历过,则遍历一遍
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			if(in[i][j]&&from[i]!=from[j])
				bein[from[j]]++; //统计每个块的入度
	int sum=0; //统计需要传播给多少个人
	for(int i=1;i<=cnt;i++)
		if(!bein[i])
			sum++; //如果有块入度为零,代表必须单独传播信息,所以sum++
	cout<

! \color{Red}\colorbox{Yellow}{!}

无向图的连通性

关于无向图的相关概念

  • 割点

    1. 定义:
      在双连通图上, 任何一对顶点之间至少存在有两条路径,在删去某个顶点及与该顶点相关联的边时, 也不破坏图的连通性。如果一个图不是双连通的,那么,将其删除后图将不再连通的那些顶点称为割点。
      简言之:G是连通图,u∈V(G),G–u不再连通,则称u是G的割点。

    2. 求割点的算法:
      我们通过DFS把无向图定向成有向图,定义每个顶点两个参数:
      ① dfn[u]表示顶点u访问的先后顺序。
      ② lowlink[u]表示沿u出发的有向轨能够到达的点v中,dfn[v]值的最小值 (经过返祖边后则停止)

    3. 三个定理
      【定理1】:DFS中,e=ab是返祖边,那么要么a是b的祖先,要么a是b的后代子孙。
      【定理2】:DFS中,e=uv是父子边,且dfn[u]>1,lowlink[v]≥dfn[u],则u是割点。
      【定理3】:DFS的根r是割点的充要条件是:至少有2条以r为尾(从r出发)的父子边。

    4. 求割点的算法:

    void DFS(int u)
    { 
    	sign++;
    	dfn[u]=sign; //给u按照访问顺序的先后标号为sign
    	lowlink[u]=sign; //给lowlink[u]赋初始值
    	for(int v=1;v<=n;v++) //寻找一个u的相邻节点v
    		if(MAP[u][v]&&prt[u]!=v) // u→v有边相连且不是回边
    		{
          	if(dfn[v]==0) //v未被访问
    			{
              	prt[v]=u; //则u是v的父亲
    				DFS(v); //uv是父子边,递归访问v
    				lowlink[u]=min(lowlink[u],lowlink[v]);//所有儿子中最小值
    				if(lowlink[v]>=dfn[u])
    				{
                  	if(dfn[u]==1)
                      {
                      	son++;
                          if(son>=2) cout<
  • 割边

    1. 定义:
      G是连通图,e∈E(G),G-e不再连通,则称e是G的割边,亦称做桥。

    2.求割边的算法
    与割点类似的,我们定义low和dfn。父子边e=u→v ,当且仅当low[v]>dfn[u]的时候,e是割边。

    3.求割边的参考代码:

    void DFS(int u)
    {
       sign++;
       dfn[u]=sign; //给u按照访问顺序的先后标号为sign
       lowlink[u]=sign; //给lowlink[u]赋初始值
       for(寻找一个u的相邻节点v)
          if(边uv没有被标记过)
          {
             标记边uv;
             给边定向u→v;
             if(v未被标记过)
             {
                DFS(v); //uv是父子边,递归访问
                lowlink[u]=min(lowlink[u],lowlink[v]);
                if(lowlink[v]>dfn[u])uv是割边
             }
             else lowlink[u]=min(lowlink[u],dfn[v]);//uv是返祖边
          }
    }
    
    1. 割点与割边
      两个割点之间的边不是割边,割边的两端点不是割点。

  • 1.定义:
    没有割点的图叫2-连通图,亦称做块。把每个块收缩成一个点,就得到一棵树,它的边就是桥。

    2.求块的算法:
    在求割点的算法中,当结点u的所有邻边都被访问过之后,如果存lowlink[u]=dfn[u],我们把u下方的整块和u导出作为图中的一个块。
    这里需要用一个来表示哪些元素是u代表的块。

关键路径

关于关键路径的相关概念

定义:在一个给定的有向无环图中,求从开始顶点到结束顶点的最长路径(路径上的
权值和)叫关键路径。

算法步骤:

  1. 读入数据,建立有向图
  2. 对DAG进行拓扑排序,得到拓扑序列
  3. 以拓扑序列为阶段,用DP求关键路径

1570 工厂的烦恼

#include 
#include 

#define maxn 1005
#define inf 0x7fffffff/2
using namespace std;

int bein[maxn];
int a[maxn];
int f[maxn]; //f[a[i]]表示到达顶点a[i]的最长路径
int in[maxn][maxn];
int n,m;

void Topsort() //拓扑排序,不解释,详细内容本博客开头有介绍
{
	int j;
	for(int i=1;i<=n;i++)
	{
		j=1;
		while((j<=n)&&(bein[j]!=0)) j++;
		bein[j]=inf;
		a[i]=j;
		for(int k=1;k<=n;k++)
			if(in[j][k]) bein[k]--;
	}
}

int main()
{
	cin>>n>>m;
	for(int i=1;i<=m;i++)
	{
		int x,y,v;
		cin>>x>>y>>v;
		in[x][y]=v;
		bein[y]++;
	} //配合拓扑排序及符合题意的输入,不解释
	Topsort();
	for(int i=2;i<=n;i++) //以拓扑序列为阶段
		for(int j=1;j<=i-1;j++)
			f[a[i]]=max(in[a[j]][a[i]]+f[a[j]],f[a[i]]); //DP求关键路径	
	cout<

! \color{Red}\colorbox{Yellow}{!}

差分约束系统

像这样一类问题:给定一组不等式x[i]-x[j]<=ck,需要求出满足所有不等式的一组解(x[1],x[2],…,x[n])。

这类问题实际上是线性规划的一类简单问题。通常可以用系数矩阵表示为Ax<=C(或Ax>=C),其中系数矩阵A的每一行里有一个1和一个-1,其余元素都为0。若A 为mn的矩阵,则x为n1的矩阵,C为m*1的矩阵,对应有m个不等式,n个未知数,即该系统为一个有n个未知数、m个约束条件的系统。这就是差分约束系统。

如果一组解(x[1],x[2],….,x[n])满足给定的不等式组,那么(x[1]+a,x[2]+a,….,x[n]+a)也能够满足,**所以这类问题的解不唯一。**实际问题中通常对输出的解有一些特别的要求。

tips: \mathfrak\color{CornflowerBlue}\colorbox{Lavender}{tips:} tips:

自己的一些零碎总结

  • 差分约束系统将题目中的约束条件不等式(即d[x]+w(x,t)>=d[y]或d[x]+w(x,t)<=d[y]之类)转换为图论的单源最短路问题。

  • 当不等式是 d[x]+w(x,t)>=d[y] 时,求最短路;当题目不等式是 d[x]+w(x,t)<=d[y] 时,求最长路。

  • 有负权回路的有向图不存在最短路径,即无解。

  • 注意建边时是j到i建边,其原因是不等式。

1595 工程规划1252

#include 
#include 
#include 
#include 

#define maxn 5005
using namespace std;

struct sj {
	int to,next,w;
} a[maxn<<1]; //前向星结构体数组

int kk,inf,n,m,cnt[maxn],flag;
int head[maxn],size;
int v[maxn],dis[maxn];
int fag;

void AddEdge(int x,int y,int z) {
	a[++size].to=y;
	a[size].next=head[x];
	head[x]=size;
	a[size].w=z;
} //前向星建边操作

void SPFA(int s) { //普通SPFA跑一遍,不解释,详细介绍前面有
	queueq;
	q.push(s);
	dis[s]=0;
	while(!q.empty()) {
		int x=q.front();
		q.pop();
		cnt[x]++;
		if(cnt[x]>n) {
			cout<<"NO SOLUTION"<dis[x]+a[i].w) {
				dis[tt]=dis[x]+a[i].w;
				if(!v[tt])
					q.push(tt),v[tt]=1;
			}
		}
		v[x]=0;
	}
}

int main() {
	scanf("%d%d",&n,&m);
	for(int i=1; i<=m; i++) {
		int x,y,z;
		scanf("%d%d%d",&x,&y,&z);
		AddEdge(y,x,z); //一定注意,是y向x连边
	}
	memset(dis,127,sizeof(dis));
	for(int i=1; i<=n; i++) { //构建虚拟超级源点,所有边都连上
		AddEdge(0,i,0);
	}
	SPFA(0);
	if(!fag) {
		for(int i=1; i<=n; i++)
			kk=min(kk,dis[i]); //寻找最小值
		for(int i=1; i<=n; i++)
			printf("%d\n",dis[i]-kk);
	}
	return 0;
}

后记 by 2020.3.31

由于时间仓促,后面的代码等内容可能没有前面详尽。当然,本蒟蒻在写作过程中肯定有不对之处,还希望大佬们广泛地提出问题,以便我和大家更好的知识掌握。

感谢您的认真观看!

你可能感兴趣的:(推荐)