【学习笔记】图论总体学习笔记

学习笔记索引

记录图论的学习。

省流:被吊打了

更改日志

2024/01/08:开坑,先不写。

2024/01/09:更新非严格次短路、判负环、差分约束系统、最短路计数、传递闭包。

学习背景

图论早就学了,只是把知识记一遍而已,有些太简单的就不记了。

最短路技巧

最短路大家应该都会,我就来写几个技巧。

1.非严格次短路

非严格次短路就是如果有多条最短路,次短路就是最短路。

先看例题吧,边讲例题边讲做法。

例题

P2865 [USACO06NOV] Roadblocks G

又是一道蓝题!

贝茜把家搬到了一个小农场,但她常常回到 FJ 的农场去拜访她的朋友。贝茜很喜欢路边的风景,不想那么快地结束她的旅途,于是她每次回农场,都会选择第二短的路径,而不象我们所习惯的那样,选择最短路。

贝茜所在的乡村有 R(1R105) 条双向道路,每条路都联结了所有的 N(1N5000) 个农场中的某两个。贝茜居住在农场 1,她的朋友们居住在农场 N(即贝茜每次旅行的目的地)。

贝茜选择的第二短的路径中,可以包含任何一条在最短路中出现的道路,并且,一条路可以重复走多次。当然咯,第二短路的长度必须严格大于最短路(可能有多条)的长度,但它的长度必须不大于所有除最短路外的路径的长度。


解法

这里可以看出不就是个非严格次短路,就是如果有多条最短路,次短路就是最短路。

那么我们就可以想,能不能再求最短路时求出次短路呢?答案是肯定的。

我用的是 dijkstra 堆优化( dijkstra 好闪,拜谢 dijkstra !)。

先设 u 为当前的点,v 为与 u 有边连接的点, w 代表从点 u 到点 v 的边的权值, dis 数组代表最短路,ret 数组代表次短路。

更新时有以下三种情况:

  • 如果 disu+w<disv,说明从按 u 的最短路再走到点 v 比原先的最短路更优,则更新最短路,就是 disv=disu+w

  • 如果 disu+wdisvdisu+w<retv,说明从按 u 的最短路再走到点 v 比原先的次短路更优,则更新次短路,就是 retv=disu+w

  • 如果 retu+w<retv,说明从按 u 的次短路再走到点 v 还比原先的次短路更优,则再更新次短路,就是 retv=retu+w

若上述条件有一个成立,则将点 v 加入堆中,还要将点 v 对应的最短路 disv 加入堆中,那怎么知道是否有成立呢?可以用一个标记变量 flag,每次更新就也把它的值给更新了,最后判断就行了。

设最后一个农场为点 N ,则最终答案为 retN

CODE(就是非严格次短路模板啦):

#include<bits/stdc++.h>
#define fi first
#define se second
using namespace std;
int r,n,u,v,w;
vector<pair<int,int> >g[100010];//还要存上权值
int dis[10010],ret[10010];
struct node{
	int fi,se;//节点和最短路
	bool operator<(const node &b) const{//重载运算符,大根堆->小根堆
		return se>b.se;
	}
};
priority_queue<node>q;//堆
pair<int,int> m_p(int x,int y){
	return make_pair(x,y);
}void dij(){
	memset(dis,127,sizeof dis);//初始化
	memset(ret,127,sizeof ret);
	dis[1]=0;
	q.push((node){1,0});//先入堆
	while(!q.empty()){
		int u=q.top().fi,dic=q.top().se
		q.pop();
		for(int i=0;i<g[u].size();i++){
			int v=g[u][i].fi,w=g[u][i].se;
			int f=0;
			if(dic+w<dis[v]){//转移
				dis[v]=dic+w,f=1;
			}if(dic+w>dis[v]&&dic+w<ret[v]){
				ret[v]=dic+w,f=1;
			}if(ret[u]+w<ret[v]){
				ret[v]=ret[u]+w,f=1;
			}if(f==1){//有成立的条件,入队
				q.push((node){v,dis[v]});
			}
		}
	}
}int main(){
	scanf("%d%d",&r,&n);
	for(int i=1;i<=n;i++){
		scanf("%d%d%d",&u,&v,&w);
		g[u].push_back(m_p(v,w));
		g[v].push_back(m_p(u,w));
	}dij();
	printf("%d",ret[r]);
	return 0;
}

2.图的判负环

巨大的知识点。

这里我们知道,dijkstra 就是无法解决边权为负数的问题,但是其他的三个 FloydBellman-FordSPFA死了!)可以,还可以判负环,接下来我将用一道例题来讲一下这三种算法的判负环。

首先,先看一下如何判负环。

我们可以发现,若 Floyddisi,i 小于 0 的情况存在,则一定会有实体负环的存在。

再来看看 Bellman-Ford,如果它没有负环的话,进行了 n1 次松弛操作后,最短路长度不会再改变了,若第 n 次松弛操作仍能执行成功,则一定存在负环。

最后再是 SPFA,这里如果某顶点的入队次数超过 n1 次(n 为图中顶点数),则存在负环。但是这样判断可能遇到特殊的卡 SPFA图,使得入队次数接近 n2 (此时的时间复杂度为 O(n2) ),由此造成超时。但是我们可以换一种思考方式,若最短路径的边数超过 n (普通的最多只有 n1 条),则一定存在负环,这里直接开个 cnt 数组统计就好了。

例题

P3385 【模板】负环

题目描述

给定一个 n 个点的有向图,请求出图中是否存在从顶点 1 出发能到达的负环。

负环的定义是:一条边权之和为负数的回路。

输入格式

本题单测试点有多组测试数据

输入的第一行是一个整数 T,表示测试数据的组数。对于每组数据的格式如下:

第一行有两个整数,分别表示图的点数 n 和接下来给出边信息的条数 m

接下来 m 行,每行三个整数 u,v,w

  • w0,则表示存在一条从 uv 边权为 w 的边,还存在一条从 vu 边权为 w 的边。
  • w<0,则只表示存在一条从 uv 边权为 w 的边。

输出格式

对于每组数据,输出一行一个字符串,若所求负环存在,则输出 YES,否则输出 NO

提示

数据规模与约定

对于全部的测试点,保证:

  • 1n2×1031m3×103
  • 1u,vn104w104
  • 1T10

提示

请注意,m 不是图的边数。


解法(代码部分,前面已经讲过方法)

Floyd 做法(不优)

这里的 n2×103,根据 Floyd O(n3) 的时间复杂度,会超时,仅可拿到部分分。

这样可以拿到 60

CODE:

#include<bits/stdc++.h>
using namespace std;
int t,n,m,u,v,w,dis[2010][2010];
void floyd(){
	for(int k=1;k<=n;k++){
		for(int i=1;i<=n;i++){
			for(int j=1;j<=n;j++){
				if(dis[i][k]==0x3f3f3f3f||dis[k][j]==0x3f3f3f3f){
					continue;
				}dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);转移
				if(dis[1][i]!=0x3f3f3f3f&&dis[i][i]<0){//判断负环
					printf("YES\n");
					return;
				}
			}
		}
	}printf("NO\n");
}int main(){
	scanf("%d",&t);
	while(t--){
		memset(dis,0x3f3f3f3f,sizeof dis);
		scanf("%d%d",&n,&m);
		for(int i=1;i<=m;i++){
			scanf("%d%d%d",&u,&v,&w);
			if(w>=0){
				dis[u][v]=min(w,dis[u][v]);//建图
				dis[v][u]=min(w,dis[v][u]);
			}else{
				dis[u][v]=min(w,dis[u][v]);
			}
		}floyd();
	}return 0;
}

Bellman-Ford 做法

注意初始化,前面已经讲过如何判负环,满分。

CODE:

#include<bits/stdc++.h>
using namespace std;
int t,n,m,u,v,w,dis[2010],cnt[2010],vis[2010];
struct node{
	int v,w;
};
vector<node>g[2010];
void Bellman_Ford(int a){
	memset(dis,0x3f,sizeof dis);
	dis[a]=0;
	for(int i=1;i<n;i++){
		for(int j=1;j<=n;j++){
			if(dis[j]!=0x3f3f3f3f){
				for(int k=0;k<g[j].size();k++){
					int v=g[j][k].v,w=g[j][k].w;
					if(dis[j]+w<dis[v]){//尝试松弛
						dis[v]=dis[j]+w;
					}
				}
			}
		}
	}for(int j=1;j<=n;j++){
		for(int k=0;k<g[j].size();k++){
			int v=g[j][k].v,w=g[j][k].w;
			if(dis[j]==0x3f3f3f3f||dis[v]==0x3f3f3f3f){//判断
				continue;
			}if(dis[j]+w<dis[v]){
				printf("YES\n");
				return;
			}
		}
	}printf("NO\n");
}int main(){
	scanf("%d",&t);
	while(t--){
		scanf("%d%d",&n,&m);
		for(int i=1;i<=2000;i++){
			g[i].clear();
		}for(int i=1;i<=m;i++){
			scanf("%d%d%d",&u,&v,&w);
			if(w>=0){
				g[u].push_back((node){v,w});//建图
				g[v].push_back((node){u,w});
			}else{
				g[u].push_back((node){v,w});
			}
		}Bellman_Ford(1);
	}return 0;
}

SPFA 做法

前面同样已经讲过如何判负环,满分。

CODE:

#include<bits/stdc++.h>
using namespace std;
int t,n,m,u,v,w,dis[2010],cnt[2010],vis[2010];
struct node{
	int v,w;
};
vector<node>g[2010];
void spfa(int a){
	memset(dis,0x3f,sizeof dis);//初始化
	memset(cnt,0,sizeof cnt);
	memset(vis,0,sizeof vis);
	queue<int>q;
	dis[a]=0,vis[a]=1,q.push(a);
	while(!q.empty()){
		int u=q.front();
		vis[u]=0;
		q.pop();
		for(int i=0;i<g[u].size();i++){
			int v=g[u][i].v,w=g[u][i].w;
			if(dis[u]+w<dis[v]){//操作
				dis[v]=dis[u]+w;
				cnt[v]=cnt[u]+1;//统计边的条数
				if(cnt[v]>=n){//如果边的数量超过了n
					printf("YES\n");
					return;
				}if(vis[v]==0){//入队
					vis[v]=1;
					q.push(v);
				}
			}
		}
	}printf("NO\n");
}int main(){
	scanf("%d",&t);
	while(t--){
		scanf("%d%d",&n,&m);
		for(int i=1;i<=2000;i++){
			g[i].clear();//初始化
		}for(int i=1;i<=m;i++){
			scanf("%d%d%d",&u,&v,&w);
			if(w>=0){//建图
				g[u].push_back((node){v,w});
				g[v].push_back((node){u,w});
			}else{
				g[u].push_back((node){v,w});
			}
		}spfa(1);
	}return 0;
}

3.差分约束系统

差分约束系统是一种特殊的 n 元一次不等式组(图论强大),这里包含了 n 个变量 x1,x2,......,xnm 个约束条件,每个约束条件形如 xixjCk(1i,j,kn),其中 Ck 被定义为一个常数,求满足约束的解。

这个 n 元一次不等式组,只有两种情况:

  • 无解。

  • 可以找到其中一组解 {x1,x2,......,xn},再将其进行常数变换,进而得到无数组解(怎么开始讲数学了)。

总所周知,我们的最短路若不存在负环,则 dis 数组满足 disidisj+w(i,j),这与我们的约束条件变换后的 xixj+Ck 类似。

于是,我们就可以将约束条件中的变量 xi,xj 抽象为图的顶点,将 Ck 抽象为点 xi 到点 xj 边的权值,于是就可以将差分约束系统转换为一个图模型,求单源最短路。

抽象了图模型后,我们可以建立一个顶点 x0 ,将这个顶点与所有点顶点建边,权值为 0,即 xix0=0(1in)

这样就可以从 x0 开始求单源最短路,即为源。

x0 开始到其他顶点的最短路 disi 就是解,若不存在最短路则无解。

例题

P1993 小 K 的农场

题目描述

小 K 在 MC 里面建立很多很多的农场,总共 n 个,以至于他自己都忘记了每个农场中种植作物的具体数量了,他只记得一些含糊的信息(共 m 个),以下列三种形式描述:

  • 农场 a 比农场 b 至少多种植了 c 个单位的作物;
  • 农场 a 比农场 b 至多多种植了 c 个单位的作物;
  • 农场 a 与农场 b 种植的作物数一样多。

但是,由于小 K 的记忆有些偏差,所以他想要知道存不存在一种情况,使得农场的种植作物数量与他记忆中的所有信息吻合。

输入格式

第一行包括两个整数 nm,分别表示农场数目和小 K 记忆中的信息数目n

接下来 m 行:

  • 如果每行的第一个数是 1,接下来有三个整数 a,b,c,表示农场 a 比农场 b 至少多种植了 c 个单位的作物;
  • 如果每行的第一个数是 2,接下来有三个整数 a,b,c,表示农场 a 比农场 b 至多多种植了 c 个单位的作物;
  • 如果每行的第一个数是 3,接下来有两个整数 a,b,表示农场 a 种植的的数量和 b 一样多。

输出格式

如果存在某种情况与小 K 的记忆吻合,输出 Yes,否则输出 No

提示

对于 100% 的数据,保证 1n,m,a,b,c5×103


这里 n 个农场可以抽象为 n 个变量,m 条信息可以抽象为 m 个约束条件,那么题目就是问这个不等式是否存在解。

这里的不等式分为 3 种情况:

  • vivjc1,可以转化为 vjvic1

  • vivjc2

  • vi=vj

根据不同情况建边,后面还要多增加一个节点(前面讲了,不再赘述)。

CODE(我怎么越来越喜欢用SPFA了,不是死了吗):

#include<bits/stdc++.h>
using namespace std;
struct node{
	int v,w;
};
int n,m,u,v,w,op,dis[20010],cnt[20010],vis[20010];
vector<node>g[20010];
queue<int>q;
void spfa(int a){
	memset(dis,127,sizeof dis);
	queue<int>q;
	dis[a]=0,vis[a]=1,q.push(a);
	while(!q.empty()){
		int u=q.front();
		vis[u]=0;
		q.pop();
		for(int i=0;i<g[u].size();i++){
			int v=g[u][i].v,w=g[u][i].w;
			if(dis[u]+w<dis[v]){//更新
				dis[v]=dis[u]+w;
				cnt[v]++;
				if(cnt[v]>=n+1){//判环,若有环则不存在
					printf("No");
					return;
				}if(vis[v]==0){//入队
					vis[v]=1;
					q.push(v);
				}
			}
		}
	}printf("Yes");
}int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++){
		scanf("%d%d%d",&op,&u,&v);
		if(op==1){//根据情况建边
			scanf("%d",&w);
			g[u].push_back((node){v,-w});
		}else if(op==2){
			scanf("%d",&w);
			g[v].push_back((node){u,w});
		}else{
			g[u].push_back((node){v,0});
			g[v].push_back((node){u,0});
		}
	}for(int i=1;i<=n;i++){//增加节点,讲解如上
		g[0].push_back((node){i,0});	
	}spfa(0);//以增加的节点为源点跑最短路(
    return 0;
}

4.最短路计数

最短路计数也是图论中很常见的,实际运用到题目中总会有一些不同。

我们设 cntu 表示从源点到点 u 的最短路径条数,它的更新有两种情况:

  • disv>disu+w(u,v),说明从源点 1 到点 v 的最短路径就是从源点 0 到点 u 的最短路径加上边 u,v,所以 cntv 等于 cntu

  • disv=disu+w(u,v),说明原来从从源点 1 到点 v 的最短路径有 cntv 条,现在又可以加上经过点 u 的这些路径,所以 cntv=cntv+cntu

源点 1 的最短路径是 cnts=1,若有重边和自环,需要分开考虑。

例题

P1144 最短路计数

又是一道绿题

题目描述

给出一个 N 个顶点 M 条边的无向无权图,顶点编号为 1N。问从顶点 1 开始,到其他每个点的最短路有几条。

输入格式

第一行包含 2 个正整数 N,M,为图的顶点数与边数。

接下来 M 行,每行 2 个正整数 x,y,表示有一条连接顶点 x 和顶点 y 的边,请注意可能有自环与重边。

输出格式

N 行,每行一个非负整数,第 i 行输出从顶点 1 到顶点 i 有多少条不同的最短路,由于答案有可能会很大,你只需要输出 ansmod100003 后的结果即可。如果无法到达顶点 i 则输出 0

提示

15 的最短路有 4 条,分别为 2124521345(由于 45 的边有 2 条)。

对于 20% 的数据,1N100
对于 60% 的数据,1N103
对于 100% 的数据,1N1061M2×106

模板,我使用的是 SPFA ,在松弛时更新 cnt 就好了。

CODE:

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10,mod=1e5+3;
vector<int>g[N];
queue<int>q;
int cnt[N],dis[N],vis[N];
int main(){
    int n,m;
	scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++){
        int x,y;
		scanf("%d%d",&x,&y);
    	g[x].push_back(y),g[y].push_back(x);
    }int s=1;
	memset(dis,0x3f,sizeof dis);//初始化
	dis[s]=0,vis[s]=1,cnt[s]=1;
	q.push(s);
    while(!q.empty()){
        int u=q.front();
		q.pop();
		vis[u]=0;
        for(int i=0;i<g[u].size();i++){
            int v=g[u][i];
            if(dis[u]+1<dis[v]){//情况1
            	dis[v]=dis[u]+1,cnt[v]=cnt[u];//松弛时更新
            	if(vis[v]==0){
           		q.push(v);
            		vis[v]=1;
				}
			}else if(dis[u]+1==dis[v]){//情况2
				cnt[v]=(cnt[v]+cnt[u])%mod;//注意取模!
			}
        }
    }for(int i=1;i<=n;i++){
    	printf("%d\n",cnt[i]);
    }return 0;
}

5.传递闭包

传递闭包是给出若干的元素以及二元关系,要求这些二元关系存在传递性,通过传递性和二元关系来推出尽量多的元素之间的关系的问题。

注意,这里用到的关系必须是二元关系,若为多元关系,必须要拆解为二元关系。

可以将元素抽象为图的顶点(又tm是抽象),将二元关系抽象为边,这样就可以用 Floyd 求解获得任意两点是否连通的问题(就是问两个元素有没有关系)。

可以将 Floyd 中的转移方程改为 disi,j=disi,j|(disi,k&disk,j),表示点 i 和点 j 之间有联通关系:

  • 要么是直接点 i 和点 j 之间有联通关系。

  • 要么是点 i 和点 k 之间有联通关系,且点 k 和点 j 之间有联通关系,可以根据传递性得到点 i 和点 j 之间有联通关系。

例题

P2419 [USACO08JAN] Cow Contest S

题目描述

FJ的 N1N100)头奶牛们最近参加了场程序设计竞赛。在赛场上,奶牛们按 1,2,,N 依次编号。每头奶牛的编程能力不尽相同,并且没有哪两头奶牛的水平不相上下,也就是说,奶牛们的编程能力有明确的排名。 整个比赛被分成了若干轮,每一轮是两头指定编号的奶牛的对决。如果编号为 A 的奶牛的编程能力强于编号为 B 的奶牛 (1A,BNAB),那么她们的对决中,编号为 A 的奶牛总是能胜出。 FJ 想知道奶牛们编程能力的具体排名,于是他找来了奶牛们所有 M1M4,500)轮比赛的结果,希望你能根据这些信息,推断出尽可能多的奶牛的编程能力排名。比赛结果保证不会自相矛盾。

输入格式

第一行两个用空格隔开的整数 N,M

2M+1 行,每行为两个用空格隔开的整数 A,B ,描述了参加某一轮比赛的奶牛的编号,以及结果(每行的第一个数的奶牛为胜者)。

输出格式

输出一行一个整数,表示排名可以确定的奶牛的数目。

提示

样例解释:

编号为 2 的奶牛输给了编号为 1,3,4 的奶牛,也就是说她的水平比这 3 头奶牛都差。而编号为 5 的奶牛又输在了她的手下,也就是说,她的水平比编号为 5 的奶牛强一些。于是,编号为 2 的奶牛的排名必然为第 4,编号为 5 的奶牛的水平必然最差。其他 3 头奶牛的排名仍无法确定。


这里如果 A 强于 BB 强于 C,则肯定 A 强于 C,这里就有了传递性。

其实就是如果第 i 只奶牛与其他 n1 只奶牛有关系,那么v排名就确定了。

那么我们就使 disu,v=1 代表奶牛 u 强于奶牛 v

那么我们就三重跑一遍前面的状态转移方程 disi,j=disi,j|(disi,k&disk,j),然后统计即可,但是统计是要避开自身的比较。

dis 的初始值根据实际情况分析。

CODE:

#include<bits/stdc++.h>
using namespace std;
int n,m,ret,u,v,cnt[110];
int dis[110][110];
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++){
		scanf("%d%d",&u,&v);
		dis[u][v]=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]=dis[i][j]|(dis[i][k]&dis[k][j]);//状态转移方程
			}
		}
	}for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			if(dis[i][j]==1){
				cnt[i]++,cnt[j]++;//可以确定这俩货的关系
			}
		}
	}for(int i=1;i<=n;i++){
		if(cnt[i]==n-1){//能确定排名
			ret++;
		}
	}printf("%d",ret);
	return 0;
}

由此,最短路的技巧讲完辣!

最小生成树

最小生成树就是从原图中删去任意条边,使得它的边权最小。

kruskal 算法

kruskal 算法是对边操作的。

kruskal 算法要先对边的权值排序,然后枚举所有的边,利用并查集判断会不会形成环,如果不会形成环,将这条边的两端合并一下,然后将记录这条边加入集合,如果统计了 n1 条边,因为已经排过序,因此就是答案,就输出答案并结束程序。

例题

P3366 【模板】最小生成树

题目描述

如题,给出一个无向图,求出最小生成树,如果该图不连通,则输出 orz

输入格式

第一行包含两个整数 N,M,表示该图共有 N 个结点和 M 条无向边。

接下来 M 行每行包含三个整数 Xi,Yi,Zi,表示有一条长度为 Zi 的无向边连接结点 Xi,Yi

输出格式

如果该图连通,则输出一个整数表示最小生成树的各边的长度之和。如果该图不连通则输出 orz

提示

数据规模:

对于 20% 的数据,N5M20

对于 40% 的数据,N50M2500

对于 70% 的数据,N500M104

对于 100% 的数据:1N50001M2×1051Zi104


这道题就是最小生成树模板,直接给出代码。

CODE:

#include<bits/stdc++.h>
using namespace std;
int n,m,x,cnt,ret,tmp,f[1001000];
struct node{
	int u,v,w;
	friend bool operator < (node a,node b){
		return a.w<b.w;
	}
}a[1001000];
int getfa(int x){
	if(x==f[x]){
		return x;
	}return f[x]=getfa(f[x]);
}void merge(int x,int y){
	f[getfa(x)]=getfa(y);
}int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++){
		f[i]=i;
	}for(int i=1;i<=m;i++){
		scanf("%d%d%d",&a[i].u,&a[i].v,&a[i].w);
	}sort(a+1,a+m+1);//排序
	for(int i=1;i<=m;i++){
		if(getfa(a[i].u)!=getfa(a[i].v)){//符合条件
			merge(a[i].u,a[i].v);//合并
			tmp++;
			ret+=a[i].w;
			if(tmp==n-1){//得到答案
				printf("%d",ret);
				return 0;
			}
		}
	}printf("orz");
	return 0;
}

咕咕咕

posted @   scy_qwq  阅读(13)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
点击右上角即可分享
微信分享提示