week9-图论进阶(最短路径)

文章介绍了如何使用DFS和BFS遍历洛谷博客中的文章引用关系,并提供了具体的C++代码实现。同时,文章讨论了Floyd算法和Dijkstra算法在求解最短路径问题上的应用,包括处理重边和优化时间复杂度的方法。此外,还涉及了并查集在集合合并与查询操作中的应用。
摘要由CSDN通过智能技术生成

1.【深基18.例3】查找文献

题目描述

小K 喜欢翻看洛谷博客获取知识。每篇文章可能会有若干个(也有可能没有)参考文献的链接指向别的博客文章。小K 求知欲旺盛,如果他看了某篇文章,那么他一定会去看这篇文章的参考文献(如果他之前已经看过这篇参考文献的话就不用再看它了)。

假设洛谷博客里面一共有 n(n<=10^5) 篇文章(编号为 1 到 n)以及 m(m<=10^6) 条参考文献引用关系。目前小 K 已经打开了编号为 1 的一篇文章,请帮助小 K 设计一种方法,使小 K 可以不重复、不遗漏的看完所有他能看到的文章。

这边是已经整理好的参考文献关系图,其中,文献 X → Y 表示文章 X 有参考文献 Y。不保证编号为 1 的文章没有被其他文章引用。

请对这个图分别进行 DFS 和 BFS,并输出遍历结果。如果有很多篇文章可以参阅,请先看编号较小的那篇(因此你可能需要先排序)。

输入格式

共 m+1 行,第 1 行为 2 个数,n 和 m,分别表示一共有 n(n<=10^5) 篇文章(编号为 1 到 n)以及m(m<=10^6) 条参考文献引用关系。

接下来 m 行,每行有两个整数 X,Y 表示文章 X 有参考文献 Y。

输出格式

共 2 行。
第一行为 DFS 遍历结果,第二行为 BFS 遍历结果。

样例 #1

样例输入 #1

8 9
1 2
1 3
1 4
2 5
2 6
3 7
4 7
4 8
7 8

样例输出 #1

1 2 5 6 3 7 8 4 
1 2 3 4 5 6 7 8

这道题本质就是考bfs与dfs,只是以图作为一个载体。所以,我们只需要知道bfs与dfs的基本步骤就可以简单地通过了。~~(好像也没那么简单)~~要注意的就是怎么存图了。这里的数据达到了10^5,所以显然不可以用邻接矩阵来存,可以考虑用vector或者链式前向星存图。这里我采用vector。

完整注释代码如下:

#include<bits/stdc++.h>
using namespace std;
#define maxn1 100005
#define maxn2 1000005
struct que{
	int head;
	int tail;
	int nums[maxn1];
}q;   //手写队列,最为致命qwq
vector<int>g[maxn1];  //vector存图
int n,m;
bool book[maxn1];  //标记数组
void dfs(int now){  //深搜
	int next;
		for(int i=0;i<g[now].size();i++){
			next=g[now][i];
			if(!book[next]){
				cout<<next<<" ";
				book[next]=1;
				dfs(next);   //不用回溯
			}
		}
}
int main(){
	cin>>n>>m;
	int u,v,now,next;
	for(int i=1;i<=m;i++){
		cin>>u>>v;
		g[u].push_back(v);  //vector存图
	}
	for(int i=1;i<=n;i++){
		sort(g[i].begin(),g[i].end());  //记得要排序
	}
	book[1]=1;  //初始化起点为标记过的
	cout<<"1 ";  //直接输出起点
	dfs(1);
	memset(book,0,sizeof(book));  //在dfs后还要bfs,所以再次全部清零
	cout<<endl<<"1 ";  //直接输出起点
	book[1]=1;
	q.nums[q.tail++]=1;  //将起点入队
	while(q.head<q.tail){  
		int now=q.nums[q.head];
		for(int i=0;i<g[now].size();i++){
			next=g[now][i];   //遍历所有的边
			if(!book[next]){
				cout<<next<<" ";
				book[next]=1;
				q.nums[q.tail++]=next;  //入队操作
			}
		}
		q.head++;  //记住队头一定要++,否则会死循环
	}
	return 0;
}


2.【模板】floyd

题目背景

模板题,无背景

题目描述

给出n个点,m条边的无向图,求每个点到其他点的距离之和%998244354的值

输入格式

第一行两个数n,m含义如上
从第二行开始,共m行,每行三个数x,y,l,代表从x到y点的长度为l

输出格式

n行,每行一个数,第i行代表点i到其他点的距离之和

样例 #1

样例输入 #1

2 1
1 2 4

样例输出 #1

4
4

样例 #2

样例输入 #2

4 5
1 2 1
1 3 2
2 3 2
3 4 3
2 4 4

样例输出 #2

8
7
7
12

提示

模板题,保证图联通
n<=500
m<=10000
1<=x,y<=n
l<=1e9

考floyd算法的一道模板题。floyd算法具有动态规划的思想,这里我用简单易懂的语言解释一下这个算法。设f(i,j)为从i点直接到达j点的距离,那么转态转移方程为:
f ( i , j ) = m i n ( f ( i , j ) , f ( i , k ) + f ( k , j ) ) 1 ≤ k ≤ n f(i,j)=min(f(i,j),f(i,k)+f(k,j)) \quad 1\leq k\leq n f(i,j)=min(f(i,j),f(i,k)+f(k,j))1kn
其实我们可以把它看成从i点到j点通过k点中转是否会让距离缩短,如果会就更新f(i,j),否则就保留。

但是这个算法的时间复杂度有一点点大,是O(n^3),虽然代码简单,但是一般不用。但是因为这道题的名字都叫floyd了,那么肯定是要用的。

但是,这道题的难点(坑点)并不在这里,最难(坑)的地方是,这m条边里是有重边的,我们要取其中较短的那一条边。(这一开始谁会想到啊,害我一直看我的floyd算法代码哪里错了,直接卡了一个多小时)

完整代码如下:

#include<bits/stdc++.h>
using namespace std;
#define maxn1 5005
#define maxn2 99999999999  //无穷大
#define mod 998244354  
long long mat[maxn1][maxn1],ans[maxn1];
int n,m;
int main(){
	cin>>n>>m;
	long long u,v,w,now;	
	for(int i=1;i<=n;i++){
		for(int a=1;a<=n;a++){
			mat[i][a]=maxn2;  //初始化矩阵
		}
	}
	for(int i=1;i<=n;i++){
		mat[i][i]=0;  //自身初始化为0
	}
	for(int i=1;i<=m;i++){
		cin>>u>>v>>w;
		mat[u][v]=min(w,mat[u][v]);
		mat[v][u]=min(w,mat[v][u]);  //有重边,要取较短的那一条
	}
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			for(int h=1;h<=n;h++){
				mat[j][h]=min(mat[j][h],mat[j][i]+mat[i][h]);  //只有4行的floyd算法
			}
		}
	}
	for(int i=1;i<=n;i++){
		for(int a=1;a<=n;a++){
			ans[i]=(ans[i]+mat[i][a])%mod;  //求和时记得求余
		}
	}
	for(int i=1;i<=n;i++){
		if(i==n){
			cout<<ans[i];  
			break;
		}
		cout<<ans[i]<<endl;  //输出结果
	}
	return 0;
}

3.【模板】单源最短路径(标准版)

题目背景

2018 年 7 月 19 日,某位同学在 NOI Day 1 T1 归程 一题里非常熟练地使用了一个广为人知的算法求最短路。

然后呢?

100 --> 60;

Ag --> Cu;

最终,他因此没能与理想的大学达成契约。

小 F 衷心祝愿大家不再重蹈覆辙。

题目描述

给定一个 n 个点,m 条有向边的带非负权图,请你计算从 s 出发,到每个点的距离。

数据保证你能从 s 出发到任意点。

输入格式

第一行为三个正整数 n, m, s。
第二行起 m 行,每行三个非负整数 u_i, v_i, w_i,表示从 u_i 到 v_i 有一条权值为 w_i 的有向边。

输出格式

输出一行 n 个空格分隔的非负整数,表示 s 到每个点的距离。

样例 #1

样例输入 #1

4 6 1
1 2 2
2 3 2
2 4 1
1 3 5
3 4 3
1 4 4

样例输出 #1

0 2 4 3

提示

样例解释请参考 数据随机的模板题

1 <= n <= 10^5;

1 <= m <= 2* 10^5;

s = 1;

1 <=u_i, v_i<= n;

0 <= w_i <= 10 ^ 9,

0 <=sum (w_i) <= 10 ^ 9。

本题数据可能会持续更新,但不会重测,望周知。

2018.09.04 数据更新 from @zzq

一道考dijkstra算法的题目,这里我就不过多介绍这个算法了,我引用洛谷题解里的这位大佬的题解,他已经讲得十分详细了。~~(主要是因为懒)这里的数据较大,dijkstra算法原本是具有O(n^2)的时间复杂度的,所以,我们需要用堆来优化这个算法。c语言需要手写堆,但是,在c++中,我们有高贵的优先队列priority_queue(c++狂喜)~~所以,我们就可以将算法的时间复杂度降为O((n+m)log2n)。

那么dijkstra的步骤是什么呢?

  • 1.初始化dis[start] = 0,dis[start]=0,其余节点的dis值为无穷大.
  • 2.找一个dis值最小的蓝点x,把节点x变成白点.
  • 3.遍历x的所有出边(x,y,z),若dis[y] > dis[x] + z,则令dis[y]=dis[x]+z
  • 4.重复2,3两步,直到所有点都成为白点…

(其实就是那篇题解里的步骤)堆优化优化的是找到dis值最小的点的步骤。我们只需要让没有被标记的点进入优先队列(c++狂喜),那么优先队列自动就会为我们找到最小的那个点**(要开最小堆)**,所以,我们就可以很容易写出答案

完整注释代码如下:

#include<bits/stdc++.h>
using namespace std;
#define maxn1 10000000
#define maxx 1e9+5  //无穷大
int head[maxn1],tot,n,m,s;
bool book[maxn1];  //标记点是否进入队列
struct edge{
	int v;
	int w;
	int next;
}e[maxn1];  //链式前向星
struct node{
	int dis;
	int pos;
	bool operator <(const node &s2)const {
		return s2.dis<dis;  //让优先队列以距离为排序标准
	}
};
int diss[maxn1];  
void add(int u,int v,int w){  //添加的函数
	e[++tot].v=v,e[tot].w=w,e[tot].next=head[u],head[u]=tot;
}
priority_queue<node>q;  //c++狂喜
void d(){  //diljkstra算法实现
	node temp;
	int v,w;
	diss[s]=0;
	q.push({0,s});  //初始时让起点入队
	while(!q.empty()){  //在队列不为空时循环
		temp=q.top();  //找到最小点
		q.pop();  //移除最小点
		int x=temp.dis,y=temp.pos;
		if(book[y]){
			continue;  //如果已经进集合了,就直接下一个
		}
		book[y]=1;  //如果没有进集合,就标记它
		for(int i=head[y];i;i=e[i].next){
			v=e[i].v,w=e[i].w;  //遍历所有边
			if(diss[v]>diss[y]+w){
				diss[v]=diss[y]+w;  //边松弛
				if(!book[v]){
					q.push((node){diss[v],v});  //没有进集合就入队
				}
			}
		}
	}
}
int main(){
	int u,v,w;
	cin>>n>>m>>s;
	for(int i=1;i<=m;i++){
		cin>>u>>v>>w;
		add(u,v,w);  //添加边
	}
	for(int i=1;i<=n;i++){
		diss[i]=maxx; //初始化都为无穷大
	}
	d();
	for(int i=1;i<=n;i++){
		cout<<diss[i]<<" "; //输出距离
	}
	return 0;
}

在图论题单里随便挑的一道题:(看上去比较简单的题,其他题都看不懂qwq)

4.【模板】并查集

题目描述

如题,现在有一个并查集,你需要完成合并和查询操作。

输入格式

第一行包含两个整数 N,M ,表示共有 N 个元素和 M 个操作。

接下来 M 行,每行包含三个整数 Z_i,X_i,Y_i 。

当 Z_i=1 时,将 X_i 与 Y_i 所在的集合合并。

Z i = 2 Z_i=2 Zi=2 时,输出 X_i 与 Y_i 是否在同一集合内,是的输出
Y ;否则输出 N

输出格式

对于每一个 Z_i=2 的操作,都有一行输出,每行包含一个大写字母,为 Y 或者 N

样例 #1

样例输入 #1

4 7
2 1 2
1 1 2
2 1 2
1 3 4
2 1 4
1 2 3
2 1 4

样例输出 #1

N
Y
N
Y

提示

对于 30% 的数据,N < 10,M <20。

对于 70% 的数据,N < 100,M < 10^3。

对于 100% 的数据,1< N < 10^4,1< M < 2* 10^5,1 < X_i, Y_i < N,Z_i { 1, 2 }。

对于并查表,我引用洛谷题解里的这位大佬的题解简单解释一下。我们可以先设f[i]=j表示 i 的老大是 j 。那么我们在查找一个人的老大时可以调用一个find函数,如下:

int find(int x){

​	if(f[x]==x) return x;

​	else{

​		find(f[x]);

​	}

}

显然,我们在查找该人的老大时,我们会遍历该人头上所以的上级,那么我们同时将这些人的老大同时改为该人的老大呢?

那么我们的代码就会变成这样:

int find(int x){

	if(f[x]==x) return x;
	
	
	else {
	
		return f[x]=find(f[x]);
	  
	}  //路径压缩

}

所以我们在判断a和b是否在一个集合里时,就可以写成if(find(a)==find(b))。非常简单

完整注释代码如下:

#include<bits/stdc++.h>
using namespace std;
int f[10005];   //f[i]=j表示i的老大是j
int find(int x){
	if(f[x]==x) return x;
	else{
		return f[x]=find(f[x]);		
	}
	  //路径压缩
}
int main(){
	int n,m,a,b,c;
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		f[i]=i;
	}
	for(int i=0;i<m;i++){
		cin>>a>>b>>c;
		if(a==1){  //b的老大变为c的老大
			f[find(b)]=find(c);
		}
		else{  //判断b和c的老大是否相同
			if(find(b)==find(c)){
				cout<<"Y"<<endl;
			}
			else{
				cout<<"N"<<endl;
			}
		}
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值