【算法每日一练]-图论 篇3(最短路) 营救 ,POJ3268 ,传送门 ,负环判断 ,灾后重建

文章介绍了Dijkstra算法求解点到点的最大边权最小直接路径,以及如何扩展到求来回最短路径、处理权值变化、负环判断和灾后重建等问题。通过实例展示了如何运用这些算法解决实际问题。

目录

今天知识点  

求到点的最大边权最小直接dijkstra,人家本来就是按topo序

求来回最短路径,正向反向建边跑

当权值变小时候,可以在原floyd的图上单独跑权值为0的边

判断负环,判断最短路长度即可

询问第t天的最短路,就每天更新一条边

            

题目:营救

思路: 

题目:POJ3268:

思路

题目:传送门

思路:

题目:负环判断

思路:

题目:灾后重建

思路:


        

        

        

题目:营救

        

思路: 

(一道dijkstral的变种题)

                

dis[v]表示从s到v点的最大拥挤度,那么每个通向v的点u有:
dis[v]=min(max(dis[u],uv_w))

                
采用dijkstra思想(dp思想)来做就是:
【选点】:原则上是最小的dis点。因为队列中其余的点dis本来就更大(不考虑旧白点),权值取max后不可能超过该点的dis。故此点已经确定不会再被更新。
【更新未确定点】:松弛操作  dis[v]>max(dis[u],w)时  dis[v]=max(dis[u],w)

        

         

#include <bits/stdc++.h>//1396  求从s到t的最大拥挤度的最小值
using namespace std;
typedef pair<int ,int> pii;
const int maxn=1e4+5,maxm=3e4+5;
int n,m,s,t,w,tot,head[maxn];
int dis[maxn];
bool vis[maxn];
struct node {int to,w,next;}e[maxm];

void add(int u,int v,int w){e[++tot]=(node){v,w,head[u]};head[u]=tot;}

void dijkstra(int s){
	priority_queue<pii,vector<pii>,greater<pii> >q;
	memset(vis,0,sizeof(vis));
	memset(dis,0x3f,sizeof(dis));
	dis[s]=0;
	q.push(make_pair(dis[s],s));
	while(!q.empty()){
		int u=q.top().second;
		q.pop();
		if(vis[u]) continue;
		vis[u]=1;
		for(int i=head[u];i;i=e[i].next){
			int v=e[i].to,w=e[i].w;
			if(vis[v])continue;//如果u比v大,那么v在确定的时候就一定会借助u点,所以不成立,这句话加不加都一样
			if(dis[v]>max(dis[u],w)){
				dis[v]=max(dis[u],w);
				q.push(make_pair(dis[v],v));
			}
		}
//		cout<<"u: "<<u<<'\n';
//		for(int i=1;i<=n;i++)cout<<i<<' '<<dis[i]<<'\n';
	}
}
int main(){
	tot=0;
	memset(head,0,sizeof(head));
	scanf("%d%d%d%d",&n,&m,&s,&t);
	int u,v,w;
	for(int i=1;i<=m;i++){
		scanf("%d %d %d",&u,&v,&w);
		add(u,v,w);
		add(v,u,w);
	}
	dijkstra(s);
	cout<<dis[t];
}

        

题目:POJ3268:

每头牛都可以从任何一个农场N去参加X点的聚会,一个有M个单行道,对应花费Ti时间,参加完聚会后每头牛都要回到原来的地方。它们来回都直走耗时最短的路径,求所有牛中参加聚会来回的最长时间。

输入:
4 8 2
1 2 4
1 3 2
1 4 7
2 1 1
2 3 5
3 1 2
3 4 4
4 2 3

        

思路

        

等价与求一个点到每个点的来回的最短距离。那么先从x跑一次最短路相当于回去的路径,因为去的路径也是最短路径且只到x点,那么我们反向建边
就相当于从x开始再跑一次最短路,然后两次的加在一起求最大值即可。

        

还不理解的话可以想象一下:原图中从a到b的某一路径,就是反向建图中的b到a同一路径,那么当a到b的这一路径恰好是最短路径时候,那么反向图中的b到a的这一路径也一定是同一路径,且是最短路径。

        

#include <bits/stdc++.h>
using namespace std;
const int maxn=10005,maxm=100005,INF=0x3f3f3f3f;
int n,m,x,tot,tt;
int head[maxn],hh[maxn],dis[maxn],dd[maxn];
bool vis[maxn];
struct node{int to,next,w;}e[maxm],ee[maxm];

void add1(int u,int v,int w){e[++tot]=(node){v,head[u],w};head[u]=tot;}
void add2(int u,int v,int w){ee[++tt]=(node){v,hh[u],w};hh[u]=tt;}

void spfa(int t){
	queue<int> q;
	memset(vis,0,sizeof(vis));
	memset(dis,0x3f,sizeof(dis));
	dis[t]=0;	vis[t]=1; //注意vis等于1表示队列中已经存在此点
	q.push(t);
	while(!q.empty()){
		int cur=q.front();	q.pop();
		vis[cur]=0;//扩展后此点出队
		for(int i=head[cur];i;i=e[i].next){//i是边号  遍历点cur连向的周围边i的点v
			int v=e[i].to,w=e[i].w;
			if(dis[v]>dis[cur]+w){//判断是否需要更新,更新过的且不在队伍的点才入队,方便找更优解
				dis[v]=dis[cur]+w;
				if(!vis[v])q.push(v),vis[v]=1;
			}
		}
	}
}
void spfa2(int t){
	queue<int> q2;
	memset(vis,0,sizeof(vis));
	memset(dd,0x3f,sizeof(dd));
	dd[t]=0;	vis[t]=1; //注意vis等于1表示队列中已经存在此点
	q2.push(t);
	while(!q2.empty()){
		int cur=q2.front();	q2.pop();
		vis[cur]=0;//扩展后此点出队
		for(int i=hh[cur];i;i=ee[i].next){//i是边号  遍历点cur连向的周围边i的点v
			int v=ee[i].to,w=ee[i].w;
			if(dd[v]>dd[cur]+w){//判断是否需要更新,更新过的且不在队伍的点才入队,方便找更优解
				dd[v]=dd[cur]+w;
				if(!vis[v])q2.push(v),vis[v]=1;
			}
		}
	}
}

int main(){
	cin>>n>>m>>x;
	int u,v,w;
	for(int i=1;i<=m;i++){
		cin>>u>>v>>w;
		add1(u,v,w);
		add2(v,u,w);
	}
	spfa(x);
	spfa2(x);
	long long ans=0;
	for(int i=1;i<=n;i++)
		ans=max(ans,1ll*dis[i]+dd[i]);
		cout<<ans<<'\n';
}

        

        

        

题目:传送门

        

思路:

        

先跑一边floyd,然后依次加入每个传送门,O(n^5)不行。

所以不能跑n^2次floyd,应该单独把两个有影响的点摘出来处理dis,就能降为O(n^4)。

为什么?为什么可以单独拿出来?

你先这样想一下刚开始在经过i点中转时候,绝大多数点k1对应的f[k1][k2]都是不会的,但是j点的一定会变化(因为f1[k1][i]+f1[i][j]必然变小),那么在进行j点中转的时候但凡之前经过j点从而得出最优解的点的f都会变化,这个时候才把w_ij的权值影响到了整个图。
        

#include <bits/stdc++.h>
using namespace std;
const int N=105;
int n,m,f1[N][N],f2[N][N];
int main(){
	cin>>n>>m;int u,v,w,ans=1e9;
	memset(f1,0x3f,sizeof(f1));
	for(int i=1;i<=m;i++){
		scanf("%d%d%d",&u,&v,&w);
		f1[u][v]=w;f1[v][u]=w;
	}
	for(int k=1;k<=n;k++)
	    for(int i=1;i<=n;i++)
	    for(int j=1;j<=n;j++){
	    	f1[i][j]=min(f1[i][k]+f1[k][j],f1[i][j]);
	    	f2[i][j]=f1[i][j];//f2保存最开始跑图结果
		}
	for(int i=1;i<=n;i++)
		for(int j=1;j<i;j++){//枚举每个传送门
			f1[i][j]=f1[j][i]=0;
			for(int k1=1;k1<=n;k1++)
			for(int k2=1;k2<=n;k2++)
				f1[k1][k2]=min(f1[k1][k2],f1[k1][i]+f1[i][k2]);//由i点进行中转
			for(int k1=1;k1<=n;k1++)
			for(int k2=1;k2<=n;k2++)
				f1[k1][k2]=min(f1[k1][k2],f1[k1][j]+f1[j][k2]);//由i,j点进行中转
			int tmp=0;
			for(int k1=1;k1<=n;k1++)//这里因为我们没有初始化对角线,所以不要加对角线元素
			for(int k2=1;k2<k1;k2++)//把根据无向图的对称型即可
			tmp+=f1[k1][k2];
			ans=min(ans,tmp);
			memcpy(f1,f2,sizeof(f2));
		}    
	cout<<ans;
	return 0;
}

思考一下:如果是把某一条路给拆掉,这个题还能这样做吗?

这道题在原图的跑出的floyd结果基础上对两个点间权值修改一下,这就相当于在原图上直接把两点权值给修改了这个过程是等价的。我直接这么说吧:这个修改后的权值只要比原图的两点间权值小,那么直接拿出这两个修改后的点去跑O(n^2)floyd一定都是没有问题的。但是如果修改后的权值更大,那就老老实实O(n^3)的跑floyd吧,因为这个时候就不能使用原来跑出来的结果了!!!

        

        

题目:负环判断

        

思路:

        

只需要记录最短路长度即可,这个长度不是带权的长度,是经过的点个数 如果长度大于n就有问题,也就是出现了负环,如果不停止就会走向无穷小

        

#include <bits/stdc++.h> 
using namespace std;
const int N=2005,M=3005;
int n,m,tot;
queue<int>q;
int head[N],vis[N],dis[N],cnt[N];//dis存放到每个点的最短距离,cnt存放对应的长度
struct node{int to;int w;int next;}e[M*2];
void add(int u,int v,int w){e[++tot]=(node){v,w,head[u]};head[u]=tot;}
int spfa(){//判断负环的spfa
	memset(dis,0x3f,sizeof(dis));
	memset(cnt,0,sizeof(cnt));
	memset(vis,0,sizeof(vis));
	while(!q.empty()) q.pop();//要把队列清空(c++不支持队列清空函数)
	dis[1]=0;vis[1]=1;q.push(1);
	while(!q.empty()){
		int cur=q.front();q.pop();
		vis[cur]=0;
		for(int i=head[cur];i;i=e[i].next){
			int v=e[i].to,w=e[i].w;
			if(dis[cur]+w<dis[v]){
				dis[v]=dis[cur]+w;
				cnt[v]=cnt[cur]+1//记录最短路长度;
				if(cnt[v]>n)return 1;//如果长度大于n一定有问题,也就是出现了负环,如果不停止就会走向无穷小
				if(!vis[v])q.push(v),vis[v]=1;	
			}
		} 
	}	
	return 0;
}
int main(){
	int t,u,v,w;
	cin>>t;
	while(t--){
		tot=0;memset(head,0,sizeof(head));
		cin>>n>>m;
		for(int i=1;i<=m;i++){
			scanf("%d%d%d",&u,&v,&w);
			add(u,v,w);
			if(w>=0)add(v,u,w);
		}
		if(spfa())cout<<"YES"<<'\n';
		else cout<<"NO"<<'\n';
	}

}

        

        

题目:灾后重建

         

思路:

        

本题的本质就是逐渐加入每个点,或者说是点的修建。

floyd的最外层k其实是在放入前k个点后的对dis的影响

eg:k为1是仅放入1对dis的影响,k为2是仅放入1和2(也就是加入了边12)后对dis的影响,依次类推
那么此题,我们只需要放一个对应的点,就输出一次,这就是前k个点的影响
        

#include <bits/stdc++.h>
using namespace std;
const int N=205;
int n,m,INF;
int a[N],f[N][N];
inline void updata(int k){//依次放入k,产生各边从而更新整个dis
	for(int i=0;i<n;i++)
	for(int j=0;j<n;j++){
		if(f[i][j]>f[i][k]+f[k][j])
		f[i][j]=f[i][k]+f[k][j];
	}
}
int main(){
	cin>>n>>m;
	for(int i=0;i<n;i++)  cin>>a[i];//每个村庄的建好时间
	memset(f,0x3f,sizeof(f));INF=f[1][1];//初始化f
	for(int i=0;i<n;i++)  f[i][i]=0;//初始化对角线
	int u,v,w;
	for(int i=1;i<=m;i++){
		scanf("%d%d%d",&u,&v,&w);
		f[u][v]=f[v][u]=w;
	}
	int q,t,now=0;cin>>q;
	while(q--){
		cin>>u>>v>>t;//询问
		while(a[now]<=t&&now<n){//把t时间之前的村庄都考虑进去
			updata(now);now++;
		}
		if(a[u]>t||a[v]>t)cout<<-1<<'\n';//村庄还没修好
		else{
			if(f[u][v]==INF)cout<<-1<<'\n';//根本就无法到达
			else cout<<f[u][v]<<'\n';
		}
	}
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值