图论复习总结

图论复习总结

前端时间为了打比赛所以急急忙忙地把提高课的图论囫囵吞枣的学完了…

然后打完比赛回来在刷一些洛谷上的图论题,发现很多难的图论题还是不会,最多把和提高课里面做过的类似的题给A掉,所以还只是刚入门图论。

想着先把入门图论掌握了,我还没有理解透图论的精髓,只能写写一些做过的题。

一、单源最短路

单源最短路是指,给定一个带权图并给定一个起点,求这个点到其他所有点的最短路径。

一般解题思路:

  1. 根据题意建图
  2. 根据题目中的限制选择最短路算法
  3. 实现最短路算法
  4. 输出答案

一般来说,最短路的问题难点在于建图,思维转化,并不是在算法的模板上面。

常用的最短路算法:

  1. 朴素版dijkstra算法 (时间复杂度O(n2))
  2. 堆优化版dijkstra算法 (时间复杂度O(mlogn))
  3. spfa算法 (时间复杂度O(m),最坏O(nm))

当然其实还有Bellman-ford算法,但spfa本质是Bellman-ford算法的队列优化,所以实际应用中Bellman-ford并不常见。

一般情况下只会用到这三种算法,dijkstra算法的思想是基于贪心,只能用于非负权图,而spfa算法我不知道基于什么,反正大多数情况都能用,但是会被一些数据卡掉。

我一般用的堆优化版本的dijkstra比较多,因为负权图碰到的比较少,然后堆优化版本的dijkstra效率很稳定,不会被卡。

题目目录:

热浪

题目链接:热浪

返回目录:点这里

最短路算法模板题,而且是非负权图,直接套用堆优化版的dijkstra算法即可。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>

using namespace std;
const int N=1e5+10,M=2*N;
int h[N],e[M],ne[M],w[M],idx;
int d[N];
int n,m,s,t;
typedef pair<int,int>PII;
int st[N];
void dijkstra()
{
	priority_queue<PII,vector<PII>,greater<PII>>q;
	memset(d,0x3f,sizeof d);
	d[s]=0;
	q.push({0,s});
	while(q.size())
	{
		PII t=q.top();
		q.pop();
		if(st[t.second])continue;
		st[t.second];
		for(int i=h[t.second];~i;i=ne[i])
		{
			int j=e[i];
			if(d[j]>t.first+w[i])
			{
				d[j]=t.first+w[i];
				q.push({d[j],j});
			}
		}
	}
}
void add(int a,int b,int c)
{
	e[idx]=b;
	ne[idx]=h[a];
	w[idx]=c;
	h[a]=idx++;
}
int main()
{
	scanf("%d%d%d%d",&n,&m,&s,&t);
	memset(h,-1,sizeof h);
	for(int i=1;i<=m;i++)
	{
		int a,b,c;
		scanf("%d%d%d",&a,&b,&c);
		add(a,b,c),add(b,a,c);
	}
	dijkstra();
	printf("%d\n",d[t]);
	return 0;
}

信使

题目链接:信使

返回目录:点这里

指挥部给哨所发信,问你最少需要多少天才能使得所有的哨所收到指令。

抽象出来的题意就是求起点为1,然后到每个点的最短路中,最长的一条。

然后套一下最短路算法的模板就好了,我这里用的依然是堆优化版的dijkstra算法。

注意题目需要特判,所以我们以最长的最短路是否为正无穷来判断是否有到不了的点。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>

using namespace std;
const int N=1e5+10,M=2*N;
int h[N],e[M],ne[M],w[M],idx;
int d[N];
int st[N];
int n,m;
typedef pair<int,int>PII;
void add(int a,int b,int c)
{
	e[idx]=b;
	ne[idx]=h[a];
	w[idx]=c;
	h[a]=idx++;
}
void dijkstra()
{
	priority_queue<PII,vector<PII>,greater<PII>>q;
	memset(d,0x3f,sizeof d);
	d[1]=0;
	q.push({0,1});
	while(q.size())
	{
		PII t=q.top();
		q.pop();
		if(st[t.second])continue;
		st[t.second]=1;
		
		for(int i=h[t.second];~i;i=ne[i])
		{
			int j=e[i];
			if(d[j]>t.first+w[i])
			{
				d[j]=t.first+w[i];
				q.push({d[j],j});
			}
		}
	}
}
int main()
{
	scanf("%d%d",&n,&m);
	memset(h,-1,sizeof h);
	for(int i=1;i<=m;i++)
	{
		int a,b,c;
		scanf("%d%d%d",&a,&b,&c);
		add(a,b,c),add(b,a,c);
	}
	dijkstra();
	int ans=-1e9;
	for(int i=1;i<=n;i++)ans=max(ans,d[i]);
	printf("%d\n",ans==0x3f3f3f3f?-1:ans);
	return 0;
}

香甜的黄油

题目链接:香甜的黄油

返回目录:点这里

题意是有很多奶牛要到同一个地方去吃糖,求出所有奶牛加起来的路径之和的最小值。

还好本题数据范围比较小,否则应该是道难题。

由于数据范围比较小,所以我们直接枚举我们的集合点就好了,对于每个集合点都跑一遍最短路,然后求出来距离之和,最后取一遍min就好了。

这里用的是堆优化dijkstra算法,效率比spfa慢一些,但是稳点。

#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<queue>

using namespace std;
const int N=3e3+10,M=2*N;
int h[N],e[M],ne[M],w[M],idx;
int d[N],st[N];
int location[N];
int p,n,m;
typedef pair<int,int>PII;
void add(int a,int b,int c)
{
	e[idx]=b;
	ne[idx]=h[a];
	w[idx]=c;
	h[a]=idx++;
}
void dijkstra(int u)
{
	memset(d,0x3f,sizeof d);
	memset(st,0,sizeof st);	
	priority_queue<PII,vector<PII>,greater<PII>>q;	
	d[u]=0;	
	q.push({0,u});
	while(q.size())
	{
		PII t=q.top();	
		q.pop();
		if(st[t.second])continue;
		st[t.second]=1;
		for(int i=h[t.second];~i;i=ne[i])
		{
			int j=e[i];
			if(d[j]>t.first+w[i])
			{
				d[j]=t.first+w[i];
				q.push({d[j],j});
			}
		}
	}
}
int main()
{
	scanf("%d%d%d",&p,&n,&m);
	memset(h,-1,sizeof h);
	for(int i=1;i<=p;i++)scanf("%d",&location[i]);
	for(int i=1;i<=m;i++)
	{
	    int a,b,c;
	    scanf("%d%d%d",&a,&b,&c);	    
	    add(a,b,c),add(b,a,c);
	}	
	long long ans=1e18;	
	for(int i=1;i<=n;i++)
	{
		long long temp=0;		
		dijkstra(i);		
		for(int j=1;j<=p;j++)temp+=d[location[j]];	
		ans=min(temp,ans);
	}
	printf("%lld\n",ans);	
	return 0;
}

最小花费

题目链接:香甜的黄油

返回目录:点这里

题意是两个人要转账100块钱,转账需要手续费,它们可以直接转账或者间接转账,转账需要手续费,问你完成目的最少需要多少钱。

依然是最短路问题,只不过说在更新的时候 +w[i]变成了 /w[i] 。

题目给的是需要的手续费的比率,比如 1 代表 1%,2 代表 2%。

我们在读入的时候把这个数改成 (100-c)/100.0(转换一下,1%变成99%),那么如果要给你转 x 元,那么我就需要 x/c 元。

比如样例,最优解是先从1号先转到2号,那么这一步需要的钱就是 100/(99%)=101.0101010101,那么再从2号转账到3号,这一步需要的钱就是,101.0101010101/(98%)=103.07153164。

那么答案就是它了。

所以转化一下只需要在执行加法时执行除法就好了。

用的依然是堆优化的dijkstra算法。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>

using namespace std;
const int N=1e5+10,M=2*N;
int h[N],e[M],ne[M],idx;
double w[M];
int st[N];
double d[N];
int n,m,A,B;
typedef pair<double,int>PII;
void add(int a,int b,double c)
{
	e[idx]=b;
	ne[idx]=h[a];
	w[idx]=c;
	h[a]=idx++;
}
void dijkstra()
{
	for(int i=1;i<=n;i++)d[i]=1e9;
	d[A]=100.0;	
	priority_queue<PII,vector<PII>,greater<PII>>q;	
	q.push({100.0,A});	
	while(q.size())
	{
		PII t=q.top();
		q.pop();		
		if(st[t.second])continue;
		st[t.second]=1;		
		for(int i=h[t.second];~i;i=ne[i])
		{
			int j=e[i];
			if(d[j]>t.first/w[i])
			{
				d[j]=t.first/w[i];
				q.push({d[j],j});
			}
		}
	}	
}
int main()
{
	scanf("%d%d",&n,&m);	
	memset(h,-1,sizeof h);	
	for(int i=1;i<=m;i++)
	{
		int a,b;
		double c;	
		scanf("%d%d%lf",&a,&b,&c);		
		c=(100-c)/100.0;		
		add(a,b,c),add(b,a,c);
	}
	scanf("%d%d",&A,&B);
	dijkstra();
	printf("%.8lf",d[B]);
	return 0;
}

最优乘车

题目链接:最优乘车

返回目录:点这里

前面的题都比较简单,建图比较简单。这个题建图就需要一点思路了。

题意是你要去一个地方旅游,你在1号点,旅游的地方在n号点。给了你n条单程的公交路线,注意是单程的,让你求你从起点到终点至少要换乘多少次。

建图的本质其实是找到哪些点可以到达哪些点,这个题很明显,前面的车站可以到后面的车站,那么我们就从前面的车站向它后面的车站建边就好了。

然后边权都是1,换乘次数就是最短路-1。(为什么是最短路-1留给读者思考)

所以我们在读入的时候把所有的车站都向它后面的车站建边就好了。

数据范围比较小,边的数量近似于n2,可以做。

这里还是用的堆优化版的dijkstra。

  #include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;

const int N=520,M=N*N;
int h[N],e[M],ne[M],idx;
int st[N],d[N];
int n,m;
char a[M];
int temp[N];
typedef pair<int,int>PII;
void add(int a,int b)
{
	e[idx]=b;
	ne[idx]=h[a];
	h[a]=idx++;
}
void dijkstra()
{
	memset(d,0x3f,sizeof d);
	d[1]=0;
    priority_queue<PII,vector<PII>,greater<PII>>q;	
	q.push({0,1});
	while(q.size())
	{
		PII t=q.top();
		q.pop();
		if(st[t.second])continue;
		st[t.second]=1;
		for(int i=h[t.second];~i;i=ne[i])
		{
			int j=e[i];
			if(d[j]>t.first+1)
			{
				d[j]=t.first+1;
				q.push({d[j],j});
			}
		}
	}
}
int main()
{
   scanf("%d%d",&m,&n);
   cin.getline(a,1000);//把换行的回车读掉
   memset(h,-1,sizeof h);
   for(int i=1;i<=m;i++)
   {
   	   cin.getline(a,10000);
   	   int len=strlen(a);
   	   int cnt=0;
   	   for(int j=0;j<len;j++)
   	   {
   	   	  int res=0;
   	      while(j<len&&a[j]!=' ')
   	      {
   	   	    res*=10;
		    res+=a[j]-'0';
		    j++;
	      }
	      temp[++cnt]=res;
       }   
       for(int j=1;j<=cnt;j++)
         for(int k=j+1;k<=cnt;k++)
         add(temp[j],temp[k]);
   } 
   dijkstra();  
   if(d[n]==0x3f3f3f3f)puts("NO");
   else printf("%d\n",d[n]-1);
   return 0;
}

昂贵的聘礼

题目链接:昂贵的聘礼

返回目录:点这里

题意是你要娶国王的女儿,要10000金币,但是你可以通过获得其他物品来降低这个聘礼。其他物品有类似的性质,你可以通过获得另外的物品来降低某件物品的价格。最后问你用最优方案下,最少要花多少聘礼。而且这个题有一个限制,在整个交易过程中,你交易的人里面的等级限制不能超过k(每个人都有一个等级)

这个题目对于初学者来说,还是有难度的,我第一次做这个题的时候根本没有什么思路。这题有两个难点,第一是建图,第二是等级限制。

建图的话,我现在发现,只要贯彻这个思路就好了:“看下哪些点可以转移到哪些点”

这句话很重要,是很多题目建图的关键所在。比如这个题,题中叙述的:酋长说:”嗯,如果你能够替我弄到大祭司的皮袄,我可以只要 8000 金币。如果你能够弄来他的水晶球,那么只要 5000 金币就行了。”

这句话可以抽象出来的意思是,从大祭司的皮袄可以转移到聘礼,且边的权值时8000;也可以从水晶球转移到聘礼,且边的权值为5000。

所以我们只需要从大祭司的皮袄和水晶球分别向聘礼连一条权值为8000和5000的边就好了。这就是从某个物品可以转移到某个物品的抽象。

以此类推,物品之间的替换关系就出来了,那么就可以建图了。

不过这里还有一个难点,上面的建图虽然是完成了,那么我们最终跑最短路求的就是d[1](因为聘礼在1号点),那么我们从哪个点开始跑呢?

解法是构造一个编号为0的点,对于所有的物品,都从0号点向该物品连一条权值为该物品价值的边,比如可以从0号点向聘礼连一条权值为10000的边,表示我直接花10000作为聘礼。这样就可以解决起点问题,并且从0点开始跑就可以获得正确答案。(读者可以自行思考一下)

其实0号点在建图里面的应用很广泛,也被称作“虚拟源点”。在后面的题目还会用到。

终于到了最后一个难点,等级限制。其实如果不作什么特殊操作直接去跑最短路的话,你会发现几乎维护不了等级限制。所以思路是,枚举每一个等级区间,在每个区间里面分别跑最短路,如果最短路中发现了等级不在该等级区间里面的物品,就跳过它。由于数据范围比较小,该做法可行。然后对于每个区间取一个min就好了。

这里还是用的堆优化版的dijkstra算法。

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>

using namespace std;
const int N=1e2+10,M=3e5+10;
int h[N],e[M],ne[M],w[M],idx;
int level[N],price[N];
int st[N],d[N];
int k,n;
typedef pair<int,int>PII;
void add(int a,int b,int c)
{
	e[idx]=b;
	ne[idx]=h[a];
	w[idx]=c;
	h[a]=idx++;
}
void dijkstra(int l,int r)
{
	memset(d,0x3f,sizeof d);
	memset(st,0,sizeof st);
	d[0]=0;	
	priority_queue<PII,vector<PII>,greater<PII>>q;	
	q.push({0,0});	
	while(q.size())
	{
		PII t=q.top();
		q.pop();
		int id=t.second;		
		if(st[id])continue;
		st[id]=1;		
		for(int i=h[id];~i;i=ne[i])
		{
			int j=e[i];
			if(level[j]<l||level[j]>r)continue;
			if(d[j]>t.first+w[i])
			{
				d[j]=t.first+w[i];
				q.push({d[j],j});
			}
		}
	}
}
int main()
{
	scanf("%d%d",&k,&n);	
	memset(h,-1,sizeof h);	
	for(int i=1;i<=n;i++)
	{
		int p,l,x;		
		scanf("%d%d%d",&p,&l,&x);		
		price[i]=p,level[i]=l;		
		while(x--)
		{
			int a,c;
			scanf("%d%d",&a,&c);
			add(a,i,c);
		}
	}
	for(int i=1;i<=n;i++)add(0,i,price[i]);
	int ans=1e9;
	for(int l=1;l<=n;l++)//枚举区间
	{
		dijkstra(l,l+k);		
		ans=min(ans,d[1]);
	}	
	printf("%d\n",ans);	
	return 0;	
}

新年好

题目链接:新年好

返回目录:点这里

这个题目还是比较经典的。

题意,你在1号点,然后给你5个点,你要遍历这5个点,问你最小遍历距离的值。

比较容易想到的思路是,枚举遍历的顺序,然后跑5遍最短路,然后求和,取min。那这样的时间复杂度就是 5! x 5 x mlogn,大概平均是600 x 200000 x 14=1,680,000,000。1e9的一个量级,显然是跑不完的。(这个是用堆优化版的dijkstra算法算出来的,spfa最坏是O(nm),平均下来不如堆优化版的dijkstra)

那其实我们可以想到,可以先跑5遍最短路,统计出各个点之间的最短路,然后再dfs一下遍历的顺序,那么 5! x 5 x mlogn 就变成了 5! + 5 x mlogn,很明显的加速了算法的进行。

不过这里的5个点的编号范围是1到50000的,如果不做处理的话统计任意两点的最短距离的时候可能会不太方便,所以这里需要一个离散化。

这里用的依然是堆优化版的dijkstra算法。

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>

using namespace std;
const int N=1e5+10,M=2*N;
int h[N],e[M],ne[M],w[M],idx;
int id[N],name;
int n,m;
int d[N];
int st[N];
int g[7][7];
int node[7];
int temp[7];
int ans;
typedef pair<int,int>PII;
void add(int a,int b,int c)
{
	e[idx]=b;
	ne[idx]=h[a];
	w[idx]=c;
	h[a]=idx++;
}
void dijkstra(int u)
{
	memset(d,0x3f,sizeof d);
	d[u]=0;	
	priority_queue<PII,vector<PII>,greater<PII>>q;	
	q.push({0,u});	
	while(q.size())
	{
		PII t=q.top();
		q.pop();		
		int idi=t.second;		
		if(st[idi])continue;
		st[idi]=1;
		for(int i=h[idi];~i;i=ne[i])
		{
			int j=e[i];		
			if(d[j]>t.first+w[i])
			{
				d[j]=t.first+w[i];
				q.push({d[j],j});
			}
		}
	}
}
void dfs(int u)
{
	if(u==5)
	{
		int res=0;
		for(int i=0;i<=4;i++)res+=g[temp[i]][temp[i+1]];
		ans=min(res,ans);
		return;
	}
	for(int i=1;i<=5;i++)
	{
		if(!st[i])
		{
			st[i]=1;
			temp[++u]=i;
			dfs(u);
			u--;
			st[i]=0;
		}
	}
}
int main()
{
	scanf("%d%d",&n,&m);
	memset(h,-1,sizeof h);
	ans=1e9;
	for(int i=1;i<=5;i++)
	{
		scanf("%d",&node[i]);
		id[node[i]]=++name;
	}
	node[0]=1;
	id[node[0]]=0;	
	for(int i=1;i<=m;i++)
	{
		int a,b,c;		
		scanf("%d%d%d",&a,&b,&c);		
		add(a,b,c),add(b,a,c);
	}	
	for(int i=1;i<=5;i++)
	{
	    memset(st,0,sizeof st);
		dijkstra(node[i]);	
		for(int j=1;j<=5;j++)
		g[id[node[i]]][id[node[j]]]=d[node[j]];		
		g[id[node[0]]][id[node[i]]]=d[1];
	}	
	memset(st,0,sizeof st);
	dfs(0);	
	printf("%d\n",ans);	
	return 0;	
}

通信线路

题目链接:通信线路

返回目录:点这里

题意,你要从1走到n,但是你可以让路径中的k条边的权值变为0,让你求出路径中去掉k条边后的最大边权。

这个题难度倒不大,建图也比较裸。

所以我们只需要二分一下我们的最大边权就好了。如果当前二分的值为 x ,那么我们在求最短路的时候,把边分类成两种,第一种是边权大于x的边,第二种是边权小于等于x的边。在跑最短路的时候,我们把边权大于x的边看成权值为1的边,把权值小于等于x的边看成权值为0的边,二分的判断条件就是n点的距离是否大于k。

然后最后把答案二分出来就好了。

这里用的还是堆优化版的dijkstra算法。

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>

using namespace std;
const int N=1e4+10,M=2*N;
int h[N],e[M],ne[M],w[M],idx;
int st[N],d[N];
int n,m,k;
typedef pair<int,int>PII;
void add(int a,int b,int c)
{
	e[idx]=b;
	ne[idx]=h[a];
	w[idx]=c;
	h[a]=idx++;
}
bool dijkstra(int x)
{
	memset(d,0x3f,sizeof d);
	memset(st,0,sizeof st);	
	d[1]=0;	
	priority_queue<PII,vector<PII>,greater<PII>>q;	
	q.push({0,1});	
	while(q.size())
	{
		PII t=q.top();
		q.pop();		
		int id=t.second;		
		if(st[id])continue;
		st[id]=1;	
		for(int i=h[id];~i;i=ne[i])
		{
			int j=e[i];			
			int c=w[i]>x;			
			if(d[j]>t.first+c)
			{
				d[j]=t.first+c;
				q.push({d[j],j});
			}
		}
	}
	return d[n]<=k;
}
int main()
{
	scanf("%d%d%d",&n,&m,&k);	
	memset(h,-1,sizeof h);	
	for(int i=1;i<=m;i++)
	{
		int a,b,c;		
		scanf("%d%d%d",&a,&b,&c);		
		add(a,b,c),add(b,a,c);
	}	
	int l=0,r=1e9+10;	
	while(l<r)
	{
		int mid=l+r>>1;
		if(dijkstra(mid))
		r=mid;
		else
		l=mid+1;
	}
	printf("%d\n",l>(1e9+10)/2?-1:l);
	return 0;
}

道路与航线

题目链接:道路与航线

返回目录:点这里

特别具有代表性的一个题目!

这个题目解法叫做拓扑排序最短路(自己胡诌的)。

题意,给你一个带权图,让你求某个起点到其他每个点的距离的最小值,不过特殊的是这个图里面既有双向边和单向边,双向边的权值非负,单向边的权值可正可负,并且无法从单向边的终点回到单向边的起点。

这个题之所以要用拓扑排序最短路的话是因为spfa会被卡,而且这个题目很特殊,其具备一定的拓扑性质,所以可以利用拓扑排序+最短路。

思路大概就是,既然不能用spfa算法,那就只能选择dijkstra算法了,可是dijkstra算法是基于贪心的,通常情况下是无法解决负权图的,但是别忘了,我们这里可以把图处理成一个拓扑图,而在拓扑图上面,dijkstra算法总是成立的。也就是说,dijkstra算法可以解决负权拓扑图的最短路问题。

下面说下具体操作,我们将每个由无向边连接的点集视作一个连通块,再把每个连通块视作一个点,那么这样生成的图,根据题意那就是一个拓扑图。所以我们先将点集合并,合并成一个个连通块,然后再进行拓扑排序,在拓扑排序的过程中求一遍最短路。(注意,在负权图上只有按照拓扑序进行的dijkstra算法才是正确的)也就是一边做拓扑排序,一边求最短路。

更具体一些,我们在做拓扑排序的时候,按照拓扑序将每个连通块的点加入堆中,然后对于这个堆跑一遍dijkstra算法,那么这样子的dijkstra算法就是正确的。

这里其实有一个处理连通块的技巧,就是在加正向边之前处理连通块,那么就可以很容易的得到了。(只针对于本题的输入方式而言)

更多细节请读者阅读代码。

这里用的依然是堆优化版的dijkstra算法。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<vector>

using namespace std;
const int N=1e5+10,M=2*N;
int h[N],e[M],ne[M],w[M],idx;
int st[N],d[N];
int din[N];
vector<int>block[N];
int blockcnt;
int id[N];
int n,R,P,s;
queue<int>q;
typedef pair<int,int>PII;
priority_queue<PII,vector<PII>,greater<PII>>heap;
void add(int a,int b,int c)
{
	e[idx]=b;
	ne[idx]=h[a];
	w[idx]=c;
	h[a]=idx++;
}
void dfs(int u,int number)
{
	id[u]=number;	
	block[number].push_back(u);	
	for(int i=h[u];~i;i=ne[i])
	  if(!id[e[i]])
	    dfs(e[i],number);
}
void dijkstra(int u)
{
	for(int x:block[u])
	  heap.push({d[x],x});	  
	while(heap.size())
	{
		PII t=heap.top();		
		heap.pop();		
		int temp=t.second;	
		if(st[temp])continue;		
		st[temp]=1;		
		for(int i=h[temp];~i;i=ne[i])
		{
			int j=e[i];
			if(id[j]!=id[temp]&&--din[id[j]]==0)q.push(id[j]);			
			if(d[j]>t.first+w[i])
			{
				d[j]=t.first+w[i];				
				if(id[j]==id[temp])heap.push({d[j],j});
			}
		}
	}
}
void topsort()
{
	for(int i=1;i<=blockcnt;i++)
	  if(din[i]==0)
	    q.push(i);	    
	memset(d,0x3f,sizeof d);	
	d[s]=0;	    
	while(q.size())
	{
		int t=q.front();		
		q.pop();		
		dijkstra(t);
	}
}
int main()
{
	memset(h,-1,sizeof h);	
	scanf("%d%d%d%d",&n,&R,&P,&s);	
	for(int i=1;i<=R;i++)
	{
		int a,b,c;		
		scanf("%d%d%d",&a,&b,&c);		
		add(a,b,c),add(b,a,c);
	}	
    for(int i=1;i<=n;i++)if(!id[i])dfs(i,++blockcnt);  
    for(int i=1;i<=P;i++)
    {
    	int a,b,c;  	
    	scanf("%d%d%d",&a,&b,&c);  	
    	add(a,b,c); 	
    	din[id[b]]++;
	}	
	topsort();
	for(int i=1;i<=n;i++)
	  if(d[i]>0x3f3f3f3f/2)puts("NO PATH");
	  else printf("%d\n",d[i]);
	return 0;
}

最优贸易

题目链接:最优贸易

返回目录:点这里

题意,你要在一个图上买卖东西,不过只能买卖一次,你必须在卖之前买,然后每个点都有一个价值,你可以在图上任意走,没有要求,唯一的要求是从1号点出发最后回到n号点,求收益的最大值。

如果直接建图,然后去跑的话,其实就是一个暴力搜索了,数据范围比较大显然不太可取。

所以这里运用我们最短路的一个思想来解决这个问题。

思路大概是这样的,枚举每个点为买卖的中间点,也就是在这个点之前买入,在这个点之后卖出,然后求差值最大。

具体的实现就是建两个图,一个正向图,一个反向图,正向图用于求最小值,反向图用于求最大值,然后跑两遍spfa,跑完之后枚举每个点作为中间点,差值取最大即可。

这里用的是spfa,因为dijkstra的贪心算法可能会不成立。

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>

using namespace std;
const int N=1e5+10,M=10*N;
int hf[N],e[M],ne[M],idx,hb[N];
int w[N];
int st[N];
int n,m;
int dmax[N],dmin[N];
queue<int>q;
void add(int h[],int a,int b)
{
	e[idx]=b;
	ne[idx]=h[a];
	h[a]=idx++;
}
void spfa(int h[],int d[],int type)
{
	if(type==0)
	{
		memset(d,0x3f,sizeof dmin);
		d[1]=w[1];
		q.push(1);
	}
	else
	{
		memset(d,-0x3f,sizeof dmax);
		d[n]=w[n];
		q.push(n);
	}	
	while(q.size())
	{
		int t=q.front();
		q.pop();
		st[t]=0;		
		for(int i=h[t];~i;i=ne[i])
		{
			int j=e[i];			
			if(type==0)
			{
				if(d[j]>min(d[t],w[j]))
				{
					d[j]=min(d[t],w[j]);
					if(!st[j])
					{
					  	q.push(j);
					  	st[j]=1;
				    }
				}
			}
			else
			{
				if(d[j]<max(w[j],d[t]))
				{
					d[j]=max(w[j],d[t]);					
					if(!st[j])
					{
						q.push(j);
						st[j]=1;					    
					}
				}
			}
		}
	}
}
int main()
{
	scanf("%d%d",&n,&m);	
	memset(hf,-1,sizeof hf);
	memset(hb,-1,sizeof hb);	
	for(int i=1;i<=n;i++)scanf("%d",&w[i]);	
	for(int i=1;i<=m;i++)
	{
		int a,b,c;		
		scanf("%d%d%d",&a,&b,&c);		
		add(hf,a,b),add(hb,b,a);		
		if(c==2)
		{
			add(hf,b,a),add(hb,a,b);
		}
	}	
	spfa(hf,dmin,0);
	spfa(hb,dmax,1);	
	int res=-1;	
	for(int i=1;i<=n;i++)res=max(dmax[i]-dmin[i],res);	
	printf("%d\n",res);	
	return 0;
}

选择最佳路线

题目链接:选择最佳路线

返回目录:点这里

题意,你可以从若干个点开始,到达指定的地点,问你最短路径是多少。

第一时间想到的思路可能是枚举多个起点,然后每次都跑一遍最短路。不过这样显然时间复杂度过高。

其实有两个方法解决这个问题,第一就是之前提到过的“虚拟源点”,这里我们以0号点为源点,向每个可以选择的起点连一条权值为0的边,然后从0号点开始跑一遍最短路。

还有另外一个方法就是反向建图,从终点开始跑,然后跑完枚举起点,取最小值就可以了。

这里用的是虚拟源点的方法,最短路算法用的是堆优化版的dijkstra算法。

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
#include<set>
#include<stack>
#ifdef LOCAL
FILE*FP=freopen("只有和别人做不一样的事.txt","r",stdin);
#endif

using namespace std;
const int N=1e5+10,M=2*N;
int h[N],e[M],ne[M],w[M],idx;
int st[N];
int n,m,s;
int d[N];
typedef pair<int,int>PII;
priority_queue<PII,vector<PII>,greater<PII>>q;
int read() {
    int x=0,f=1;
    char c=getchar();
    while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}
    while(c>='0'&&c<='9') x=x*10+c-'0',c=getchar();
    return x*f;
}
void add(int h[],int a,int b,int c)
{
	e[idx]=b;
	ne[idx]=h[a];
	w[idx]=c;
	h[a]=idx++;
}
void dijkstra()
{
	while(q.size())
	{
		PII t=q.top();
		q.pop();
		int id=t.second;		
		if(st[id])continue;
		st[id]=1;		
		for(int i=h[id];~i;i=ne[i])
		{
			int j=e[i];			
			if(d[j]>t.first+w[i])
			{
				d[j]=t.first+w[i];				
				q.push({d[j],j});
			}
		}
	}
}
int main()
{
    while(~scanf("%d%d%d",&n,&m,&s))
    {
    	idx=0;    	
    	memset(h,-1,sizeof h);
    	for(int i=1;i<=m;i++)
    	{
    		int a,b,c;  		
    		scanf("%d%d%d",&a,&b,&c);   		
    		add(h,a,b,c);
		}		
		memset(st,0,sizeof st);		
		memset(d,0x3f,sizeof d);		
		scanf("%d",&m);		
		for(int i=1;i<=m;i++)
		{
			int x;
			scanf("%d",&x);
			add(h,0,x,0);
		}
		d[0]=0;
		q.push({0,0});
		dijkstra();
		printf("%d\n",d[s]>0x3f3f3f3f/2?-1:d[s]);
	}	
	return 0;
}

拯救大兵瑞恩

题目链接:拯救大兵瑞恩

返回目录:点这里

最短路计数

题目链接:最短路计数

返回目录:点这里

题意,给你一个无向图,让你求从1号点开始,到其他点的最短路的条路有多少。

这题一开始第一次做的时候还是有点不太会的,因为没做过这类型的题,没有思路的话其实还是有点难处理的。

但是看了题解以后就觉得恍然大悟了。

具体的解法是,在更新最短路的时候,如果当前最短路等于当前可以更新的情况,那就直接把它累加起来,也就是d[j]==d[t]+w[i]的时候,cnt[j]+=cnt[t];另一种情况,可以更新的情况,那就直接把该可以更新的情况赋值过来就就好了,也就是d[j]>d[t]+w[i]的时候,cnt[j]=cnt[t]。

就是这加上这两步简单的操作就够了。

其实这两步看起来很简单,但是没有接触过的同学其实可能想不到,这其实是很具有代表性的一步,它不仅仅代表一种思路,更代表一种思维方式,一种转移的方式,它曾让我收益匪浅。

所以我们只需要跑一遍最短路,然后在更新的时候进行状态计算就好了。

这里用的是堆优化版的dijkstra算法。

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
#include<set>
#include<stack>
#ifdef LOCAL
FILE*FP=freopen("只有和别人做不一样的事.txt","r",stdin);
#endif

using namespace std;

const int N=1e5+10,M=4*N,mod=100003;
int h[N],e[M],ne[M],w[M],idx;
int st[N];
int n,m;
int cnt[N];
int d[N];
typedef pair<int,int>PII;
priority_queue<PII,vector<PII>,greater<PII>>q;
int read() {
    int x=0,f=1;
    char c=getchar();
    while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}
    while(c>='0'&&c<='9') x=x*10+c-'0',c=getchar();
    return x*f;
}
void add(int h[],int a,int b,int c)
{
	e[idx]=b;
	ne[idx]=h[a];
	w[idx]=c;
	h[a]=idx++;
}
void dijkstra()
{
    q.push({0,1});
    memset(d,0x3f,sizeof d);
    d[1]=0;
	while(q.size())
	{
		PII t=q.top();
		q.pop();
		int id=t.second;		
		if(st[id])continue;
		st[id]=1;		
		for(int i=h[id];~i;i=ne[i])
		{
			int j=e[i];			
			if(d[j]>t.first+w[i])
			{
				d[j]=t.first+w[i];				
				cnt[j]=cnt[id]%mod;				
				q.push({d[j],j});
			}
			else if(d[j]==t.first+w[i])
			cnt[j]=(cnt[id]+cnt[j])%mod;
		}
	}
}
int main()
{
    n=read(),m=read();    
    memset(h,-1,sizeof h);   
    for(int i=1;i<=m;i++)
    {
    	int a,b;    	
    	a=read(),b=read();    	
    	add(h,a,b,1),add(h,b,a,1);
	}	
	for(int i=1;i<=n;i++)cnt[i]=1;	
	dijkstra();	
	for(int i=1;i<=n;i++)
	if(d[i]>0x3f3f3f3f/2)puts("0");
	else
	printf("%d\n",cnt[i]%mod);	
	return 0;
}

观光

题目链接:观光

返回目录:点这里

这道题目是基于次短路和最短路计数两个题的一个结合版。

题意,你需要求从1号点到n号点的最短路和次短路,并且记录它们的条数,如果次短路等于最短路+1的话,则输出二者的总和,否则输出最短路的条数。

这道题细节挺多的,综合了次短路和最短路计数两个题的细节,而且是有1+1>2的效果。

这个题其实和单纯求次短路和最短路计数的差异还是有的,难度也有很大分别。主要体现在,进行次短路和最短路同时计数的时候,要注意一个问题,就是更新的问题。这里我们在更新的时候用二维数组存最短路和次短路,然后在跑最短路更新的时候,还需要记录一下当前队列里面的元素的类型,是次短路还是最短路。

难就难在这个记录类型这个点,当然想到了就不算难了。由于次短路也有可能可以更新最短路,所以我们在入队的时候要记录它的类型。当然记录类型的目的更大程度是为了准确的记录最短路和次短路的条数,因为我们在状态转移的时候,需要知道当前转移过来的状态是什么状态,才能够正确的转移我们最短路和次短路的条数。

而且在转移的时候,需要注意求次短路的细节和求最短路条数的细节,转移的时候有四种状态:当前状态严格小于当前最短路,当前状态等于当前最短路,当前状态严格小于当前次短路,当前状态等于当前次短路。

所以这道题目细节很多,写的时候需要细嚼慢咽,慢慢体会这道题。

这道题用的是堆优化版的dijkstra算法。

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
#include<set>
#include<stack>
#ifdef LOCAL
FILE*FP=freopen("只有和别人做不一样的事.txt","r",stdin);
#endif

using namespace std;
const int N=1e5+10,M=2*N,mod=100003;
int h[N],e[M],ne[M],w[M],idx;
int st[N][2];
int n,m;
int cnt[N][2];
int d[N][2];
typedef pair<int,pair<int,int>>PII;
priority_queue<PII,vector<PII>,greater<PII>>q;
int read() {
    int x=0,f=1;
    char c=getchar();
    while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}
    while(c>='0'&&c<='9') x=x*10+c-'0',c=getchar();
    return x*f;
}
void add(int h[],int a,int b,int c)
{
	e[idx]=b;
	ne[idx]=h[a];
	w[idx]=c;
	h[a]=idx++;
}
void dijkstra()
{
	while(q.size())
	{
		PII t=q.top();
		q.pop();
		int id=t.second.second;
		int type=t.second.first;		
		if(st[id][type])continue;
		st[id][type]=1;//必须要有一个type来确定当前是如何被更新的 	
		for(int i=h[id];~i;i=ne[i])
		{
			int j=e[i];
			//四种情况分类讨论
			if(d[j][0]>t.first+w[i])
			{
				d[j][1]=d[j][0];
				cnt[j][1]=cnt[j][0];
				q.push({d[j][1],{1,j}});
				d[j][0]=t.first+w[i];
				cnt[j][0]=cnt[id][type];
				q.push({d[j][0],{0,j}});
			}
			else if(d[j][0]==t.first+w[i])cnt[j][0]+=cnt[id][type];
			else if(d[j][1]>t.first+w[i])
			{
				d[j][1]=t.first+w[i];
				cnt[j][1]=cnt[id][type];
				q.push({d[j][1],{1,j}});
			}
			else if(d[j][1]==t.first+w[i])cnt[j][1]+=cnt[id][type];
		}
	}
}
int main()
{
	int T;	
	T=read();	
	while(T--)
	{
		memset(st,0,sizeof st);
		int start,end;		
	    n=read(),m=read();
	    idx=0;	    
	    memset(h,-1,sizeof h);	    
	    for(int i=1;i<=m;i++)
	    {
	    	int a,b,c;	    	
	    	a=read(),b=read(),c=read();	    	
	    	add(h,a,b,c);
		}		
		start=read(),end=read();		
		for(int i=1;i<=n;i++)cnt[i][0]=cnt[i][1]=1;		
		memset(d,0x3f,sizeof d);		
		d[start][0]=0;		
		q.push({0,{0,start}});		
		dijkstra();			
	    printf("%d\n",d[end][0]==(d[end][1]-1)?(cnt[end][0]+cnt[end][1]):cnt[end][0]);
	}
	return 0;
}

好了,单源最短路的算法就先复习到这里,以后如果有好的题目再加上来。

二、最小生成树

最小生成树是指,在一个图的生成树中,边权之和最小的树,就叫最小生成树。

一般解题思路:

  1. 分析题意,转化题意
  2. 选择最小生成树算法
  3. 实现最小生成树算法
  4. 输出答案

一般来说,最小生成树的问题是你要清楚这个题该如何利用最小生成树来解决这个问题,和单源最短路一样,难点并不在算法本身,而是在如何运用算法来解决这个问题。

常用的最小生成树算法:

  1. 朴素版prim算法 (时间复杂度O(n2))
  2. 堆优化版prim算法 (时间复杂度O(mlogn))
  3. Kruskal算法 (时间复杂度O(mlogm))

前两种算法是类似的,只不过是一个优化问题。而prim算法和Kruskal算法的区别在于算法的流程不同,主要讲一下prim算法一般用于稠密图,Kruskal算法一般用于稀疏图。

大概介绍一下prim算法和Kruskal算法的流程。prim算法是和dijkstra算法很类似的,我不知道是否基于贪心,我觉得应该是的,它的流程的dijkstra算法很类似。而Kruskal算法流程则是利用并查集,把边权排序,取能够成生成树的最小边权即可。

我一般比较习惯用Kruskal算法,因为它写起来比较方便。

题目目录:

最短网络

题目链接:最短网络

返回目录:点这里

题意,你需要把n个点连起来,问你最小的花费是多少。

裸的最小生成树问题。由于数据范围比较小,所以Kruskal算法和prim算法都可以,由于输入给的是一个矩阵,所以是稠密图,所以这里选择prim算法。

一般选择算法还是看他的时间复杂度,和空间复杂度,不过我个人比较看重顺手程度。

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
#include<set>
#include<stack>
#ifdef LOCAL
FILE*FP=freopen("只有和别人做不一样的事.txt","r",stdin);
#endif

using namespace std;
const int N=110,M=2*N,mod=100003;
int n; 
int g[N][N];
int d[N];
int st[N];
long long ans=0;
int read() {
    int x=0,f=1;
    char c=getchar();
    while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}
    while(c>='0'&&c<='9') x=x*10+c-'0',c=getchar();
    return x*f;
}
void prim()
{
	for(int i=1;i<=n;i++)
	{
		int t=-1;		
		for(int j=1;j<=n;j++)
		  if(t==-1&&!st[j]||d[j]<d[t]&&!st[j])
		  t=j;		
		st[t]=1;		
		ans+=d[t];		
		for(int j=1;j<=n;j++)
		d[j]=min(g[j][t],d[j]);	
	}
}
int main()
{
    n=read();    
    for(int i=1;i<=n;i++)
      for(int j=1;j<=n;j++)
      {
      	g[i][j]=read();
	  }
	memset(d,0x3f,sizeof d);	
	d[1]=0;	
	prim();	
	printf("%lld\n",ans);	
	return 0;
}

局域网

题目链接:局域网

返回目录:点这里

题意,现在有一个图已经被连接起来,但是图中的边有点浪费,所以你需要尽可能的去掉一些边,使得原来的点依然连通,并且去掉的边的边权之和最大。

看起来好像有点奇怪,正面去考虑好像没那么简单。但是仔细一想,我们可以从反面去考虑,减去的边权值和最大那不就等价于留下的边权最小吗?所以我们只需要先把原来所有边的权值res加起来,然后求一遍最小生成树的权值ans,做一个差,那么答案就是res-ans。

所以我们只需要跑一遍最小生成树算法就可以了。这里我选择的是Kruskal算法,因为这里是稀疏图。当然了由于数据范围比较小,选择prim算法也是可以的,但是没必要。

这里用的是Kruskal算法。

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
#include<set>
#include<stack>
#ifdef LOCAL
FILE*FP=freopen("只有和别人做不一样的事.txt","r",stdin);
#endif

using namespace std;
const int N=210,M=2*N,mod=100003;
struct node{
	int a,b,w;	
	bool operator<(const node&t)const
	{
		return w<t.w;
	 } 
}q[N];
int n,k; 
int p[N];
long long ans=0,res=0;
int read() {
    int x=0,f=1;
    char c=getchar();
    while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}
    while(c>='0'&&c<='9') x=x*10+c-'0',c=getchar();
    return x*f;
}
int find(int x)
{
	if(x!=p[x])p[x]=find(p[x]);	
	return p[x];
}
void Kruskal()
{
	for(int i=1;i<=n;i++)p[i]=i;	
	sort(q+1,q+k+1);	
	for(int i=1;i<=k;i++)
	{
		int a=find(q[i].a),b=find(q[i].b);		
		if(a!=b)
		{
			p[a]=b;
			ans+=q[i].w;
		}
	}
} 
int main()
{
    n=read(),k=read();    
    for(int i=1;i<=k;i++)
    {
    	int a,b,c;  	
    	a=read(),b=read(),c=read();    	
    	q[i]={a,b,c};    	
    	res+=c;
	}	
	Kruskal();    	
	printf("%lld\n",res-ans);	
	return 0;
}

繁忙的都市

题目链接:繁忙的都市

返回目录:点这里

题意,给你一个图,让你求它的最小生成树的边数以及它的边权的最大值。

比较简单的一个题目,我们只需要跑一遍Kruskal算法,在跑的时候记录一下边的条路,然后更新一下边权的最大值就好了。这里Kruskal算法处理起来会简单很多,如果用prim算法应该会比较麻烦一些。数据范围比较小,完全可以通过。

这里用的是Kruskal算法。

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
#include<set>
#include<stack>
#ifdef LOCAL
FILE*FP=freopen("只有和别人做不一样的事.txt","r",stdin);
#endif

using namespace std;
const int N=1e5+10,M=2*N,mod=100003;
struct node{
	int a,b,w;	
	bool operator<(const node&t)const
	{
		return w<t.w;
	 } 
}q[N];
int n,m; 
int p[N];
long long ans=0,res=0;
int read() {
    int x=0,f=1;
    char c=getchar();
    while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}
    while(c>='0'&&c<='9') x=x*10+c-'0',c=getchar();
    return x*f;
}
int find(int x)
{
	if(x!=p[x])p[x]=find(p[x]);	
	return p[x];
}
void Kruskal()
{
	for(int i=1;i<=n;i++)p[i]=i;	
	sort(q+1,q+m+1);	
	for(int i=1;i<=m;i++)
	{
		int a=find(q[i].a),b=find(q[i].b);		
		if(a!=b)
		{
			p[a]=b;
			ans=q[i].w;
			res++;
		}
	}
} 
int main()
{
    n=read(),m=read();    
    for(int i=1;i<=m;i++)
    {
    	int a,b,c;    	
    	a=read(),b=read(),c=read();    	
    	q[i]={a,b,c};
	}	
	Kruskal();    	
	printf("%lld %lld\n",res,ans);	
	return 0;
}

联络员

题目链接:联络员

返回目录:点这里

题意,给你一个图,图里面有两种边,一种是必选边,另外一种是非必选边,你需要求的是,在所有必选边都选择的情况下,使得图中所有点都连通的,边权值和的最小值。

乍一看怎么感觉不太对,没什么思路,这和最小生成树有什么关系啊?但是仔细一想,我们可以先把必选边先连起来,然后再按非必选边的边权大小来考虑选择非必选边。

也就是说,我们先用并查集维护好必选边的连通性,然后再按边权从小到大的顺序维护整体连通性即可。所以我们在Kruskal算法运行的时候,排序时把必选边放在最前面,先选完必选边,再选非必选边,维护连通性即可。

这里用的是Kruskal算法,因为比较好维护连通性。

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
#include<set>
#include<stack>
#ifdef LOCAL
FILE*FP=freopen("只有和别人做不一样的事.txt","r",stdin);
#endif

using namespace std;
const int N=1e5+10,M=2*N,mod=100003;
struct node{
	int a,b,w;
	int flag;	
	bool operator<(const node&t)const
	{
		if(flag!=t.flag)return flag<t.flag;		
		return w<t.w;
	 } 
}q[N];
int n,m; 
int p[N];
long long ans=0;
int read() {
    int x=0,f=1;
    char c=getchar();
    while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}
    while(c>='0'&&c<='9') x=x*10+c-'0',c=getchar();
    return x*f;
}
int find(int x)
{
	if(x!=p[x])p[x]=find(p[x]);	
	return p[x];
}
void Kruskal()
{
	for(int i=1;i<=n;i++)p[i]=i;	
	sort(q+1,q+m+1);	
	for(int i=1;i<=m;i++)
	{
		int a=find(q[i].a),b=find(q[i].b);		
		if(a!=b||q[i].flag==1)
		{
			p[a]=b;
			ans+=q[i].w;
		}
	}
} 
int main()
{
    n=read(),m=read();    
    for(int i=1;i<=m;i++)
    {
    	int a,b,c,u;    	
    	u=read(),a=read(),b=read(),c=read();    	
    	q[i]={a,b,c,u};
	}	
	Kruskal();    	
	printf("%lld\n",ans);	
	return 0;
}

连接格点

题目链接:连接格点

返回目录:点这里

题意,有一个n x m的网格,你需要把所有的点连通起来,问你花费的最小值,其中有一些点已经连接起来了。

这个题可能没有之前的题那么形象,看起来比较抽象,其实这个题还算比较简单。

我们只需要把网格中的每个点看作一个点就好了,然后把它和周围的4个点连起来就好了,然后跑一遍最小生成树算法即可。
不过在连点的时候注意不要重复连,而且这里有一个小技巧,我们可以把二维的点的坐标离散化成一维的数值,这样就方便存储。

这里用的依然是是Kruskal算法。

不过这里可以有一个优化,就是省去排序的时间,因为纵向边的权值是小于横向边的,所以我们在加边的时候可以先枚举纵向边,这样就不需要排序,加进来的就是从小到大的顺序。

朴素版本:

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
#include<set>
#include<stack>
#define cancel ios::sync_with_stdio(false);
#ifdef LOCAL
FILE*FP=freopen("只有和别人做不一样的事.txt","r",stdin);
#endif

using namespace std;
const int N=1010,M=20*N,mod=100003;
struct node{
	int a,b,w;	
	bool operator<(const node&t)const
	{
		return w<t.w;
	 } 
}q[N*N*2];
int n,m; 
int p[N*N*2];
int g[N][N];
long long ans=0,cnt=0;
int read() {
    int x=0,f=1;
    char c=getchar();
    while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}
    while(c>='0'&&c<='9') x=x*10+c-'0',c=getchar();
    return x*f;
}
int find(int x)
{
	if(x!=p[x])p[x]=find(p[x]);	
	return p[x];
}
void Kruskal()
{	
	sort(q+1,q+cnt+1);	
	for(int i=1;i<=cnt;i++)
	{
		int a=find(q[i].a),b=find(q[i].b);		
		if(a!=b)
		{
			p[a]=b;
			ans+=q[i].w;
		}
	}
} 
void inital()
{
	int dx[4]={0,0,1,-1},dy[4]={1,-1,0,0};
	for(int i=1;i<=n;i++)
	  for(int j=1;j<=m;j++)
	  {
	  	for(int u=0;u<4;u++)
	  	{
	  		int x=i+dx[u],y=j+dy[u];
	  		if(x<1||x>n||y<1||y>m)continue;
	  		int a1=g[i][j],b1=g[x][y];
	  		int a=find(a1),b=find(b1);
	  		if(a1<b1&&a!=b)
	  		{
	  			int t=1;	  			
	  			if(u<2)t++;	  			
				q[++cnt]={a1,b1,t};
			}
		}
	  }
}
int main()
{
    n=read(),m=read();
    for(int i=1,t=1;i<=n;i++)
      for(int j=1;j<=m;j++)
      g[i][j]=t++;      
    int x1,y1,x2,y2;    
    for(int i=1;i<=n*m+10;i++)p[i]=i;    
    while(cin>>x1>>y1>>x2>>y2)
    {
    	int a=find(g[x1][y1]),b=find(g[x2][y2]);
    	p[a]=b;
	}	
	inital();	
	Kruskal();
	printf("%lld\n",ans);	
	return 0;
}

优化版本:

#include<iostream>
#include<algorithm>
#include<cstring>

using namespace std;
const int N=1010,M=2*N*N;
int p[M];
int g[N][N];
int ans;  
struct node{
    int a,b,w;
}q[M];
bool cmp(node a,node b)
{
    return a.w<b.w;
}
int n,m;
int cnt=0;
int find(int x)
{
    if(x!=p[x])p[x]=find(p[x]);  
    return p[x];
}
void Kruskal()
{
    for(int i=0;i<cnt;i++)
    {
        if(find(q[i].a)!=find(q[i].b))
        {
            p[find(q[i].a)]=p[q[i].b];
            ans+=q[i].w;
        }
    }
}
void build()
{
    int dx[4]={-1,0,1,0},dy[4]={0,1,0,-1},dw[4]={1,2,1,2};
    for(int z=0;z<2;z++)
      for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
         for(int u=0;u<4;u++)
         if(u%2==z)//先枚举纵向边
         {
             int x=i+dx[u],y=j+dy[u],w=dw[u];             
             if(x<1||x>n||y<1||y>m)continue;            
             if(g[i][j]<g[x][y])
             {
                 q[cnt++]={g[i][j],g[x][y],w};
             }
         }
}
int main()
{
    scanf("%d%d",&n,&m);    
    for(int i=1,t=1;i<=n;i++)
      for(int j=1;j<=m;j++,t++)
      g[i][j]=t;     
    for(int i=1;i<=n*m+10;i++)p[i]=i;    
    int x1,y1,x2,y2;    
    while(cin>>x1>>y1>>x2>>y2)
    {
        if(find(g[x1][y1])!=find(g[x2][y2]))
        p[find(g[x1][y1])]=p[g[x2][y2]];
    }    
    build();   
    Kruskal();    
    cout<<ans;    
    return 0;
}

新的开始

题目链接:新的开始

返回目录:点这里

题意,有n个工厂要供电,然后要建发电站。对于每个工厂供电有两种选择一是在自己工厂建发电站,二是与其他工厂的发电站相连,然后获得电力,问你怎样建站才能得到最小花费。

这道题还是比较简单的,也用到了我们最短路算法里面的一个虚拟源点的一个概念,我们把题意中的在自己的工厂建发电站转化为有一个超级发电站,那么在自己工厂建站就等价与向这个超级发电站连一条边,花费值等于在自身建发电站的花费。

也就是我们给每个工厂都向超级发电站连一条边,然后花费对应过去,并把超级源点设为0号点,然后把0号点加入点的集合跑一遍最小生成树算法即可。

这题由于数据范围和输入格式,采用的是prim算法。

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
#include<set>
#include<stack>
#include<map>
#include<cmath>
#include<climits>
#define in(x) x=read()
#define out(x) printf("%d\n",x);
#define f(x,a,b,c) for(int x=a;x<=b;x+=c) 
#define in2(x,y) scanf("%d%d",&x,&y);
#define in3(x,y,z) scanf("%d%d%d",&x,&y,&z);
#define in4(x,y,z,v) scanf("%d%d%d%d",&x,&y,&z,&v);
#define out2(a,b) printf("%d %d\n",a,b);
#define out3(a,b,c) printf("%d %d %d\n",a,b,c);
#define out4(a,b,c,d) printf("%d %d %d %d\n",a,b,c,d);


using namespace std;
typedef pair<int,int>PII;
typedef long long LL;
const int N=310,M=2*N,mod=23333333;
int n,k;
int T;
PII a[N];
int d[N],g[N][N],st[N],ans;
void prim(int u)
{
    memset(d,0x3f,sizeof d);
    d[u]=0;
    memset(st,0,sizeof st);
    f(i,0,n,1)
    {
        int t=-1;
        f(j,0,n,1)
          if(t==-1&&!st[j]||d[j]<d[t]&&!st[j])
          t=j;
        ans+=d[t];
        st[t]=1;
        f(j,0,n,1)
        d[j]=min(d[j],g[j][t]);
    }
}
inline int read() {
    int x=0,f=1;
    char c=getchar();
    while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}
    while(c>='0'&&c<='9') x=x*10+c-'0',c=getchar();
    return x*f;
}
int main()
{
  // freopen("C:/Users/ASUS/Desktop/output.txt","w",stdout);
  // freopen("C:/Users/ASUS/Desktop/input.txt","r",stdin);
   in(n);
    memset(g,0x3f,sizeof g);
   f(i,1,n,1)
   {
       int x;
       in(x);
       g[i][0]=g[0][i]=min(g[0][i],x);
   }
   f(i,1,n,1)
     f(j,1,n,1)
       in(g[i][j]);   
   prim(0);  
   out(ans);   
	return 0;
}

北极通讯网络

题目链接:北极通讯网络

返回目录:点这里

题意,现在有n个村庄需要相互联系起来,联系的方式有两种,一种的无线电收发机,另外一种是卫星接收器,卫星接收器可以无视距离进行连接,而无线电收发机只能在规定的范围内进行相互联系。而且只有双方都具有卫星接收器,才能够无视距离连接。为了使村庄连通,所以如果卫星接收器不够的话就要买无线电收发机,收发机有一个距离限制,让你求最小的距离,使得村庄之间可以互通。

其实这题目对于初学者来说难度不算很大,但也算有一点点难度。我第一次写这题的时候,有思路,写了半天,发现过不去,然后看题解,发现和题解思路不太一样,所以就用了题解的思路。第二次做这个题的时候,还是第一时间想到了用第一次的思路,然后还是WA,后来又看题解,又回想起题解的思路了。

正确思路大概是这样的,我们在跑Kruskal算法的时候统计一下当前还剩下多少条边,如果当前剩下的边小于等于卫星接收器-1的时候,我们就退出算法,得到答案。

这个一定是正确的,因为Kruskal算法是从小到大枚举边权的,所以剩下的边权肯定更大,所以不连剩下的边权。

关于这里为什么是now<=k-1就退出循环解释一下,因为k个卫星接收器意味着可以构成k-1条边,一开始把now赋值成n-1,每找到一条边now就减一,那么当now变成k-1了,那就意味着只剩下k-1条边需要连,而此时卫星接收器可以完成这个任务,所以就可以退出算法了。

这里因为是用到了Kruskal算法的一个性质,所以写的是Kruskal算法。

#pragma GCC diagnostic error "-std=c++11"  
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
#include<set>
#include<stack>
#include<map>
#include<cmath>
#include<climits>
#define in(x) x=read()
#define out(x) printf("%d\n",x);
#define f(x,a,b,c) for(int x=a;x<=b;x+=c) 
#define in2(x,y) scanf("%d%d",&x,&y);
#define in3(x,y,z) scanf("%d%d%d",&x,&y,&z);
#define in4(x,y,z,v) scanf("%d%d%d%d",&x,&y,&z,&v);
#define out2(a,b) printf("%d %d\n",a,b);
#define out3(a,b,c) printf("%d %d %d\n",a,b,c);
#define out4(a,b,c,d) printf("%d %d %d %d\n",a,b,c,d);

using namespace std;
typedef pair<int,int>PII;
typedef long long LL;
const int N=510,M=N*N,mod=23333333;
int n,m,k,s;
int T;
int p[M];
PII a[N];
int g[N][N];
inline int read() {
    int x=0,f=1;
    char c=getchar();
    while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}
    while(c>='0'&&c<='9') x=x*10+c-'0',c=getchar();
    return x*f;
}
struct node{
    int a,b;
    double dist;
    bool operator<(const node&t)const
    {
        return dist<t.dist;
    }
}q[M];
int find(int x)
{
    if(x!=p[x])p[x]=find(p[x]);
    return p[x];
}
double Kruskal()
{
    sort(q+1,q+m+1);
    f(i,1,n*n,1)p[i]=i;
    double ans=0;
    int now=n-1;
    f(i,1,m,1)
    {
        if(now<=k-1)
        {
            return ans;
        }
        int a=find(q[i].a),b=find(q[i].b);
        if(a!=b)
        {
            now--;
            p[b]=a;
            ans=q[i].dist;
        }
    }
    return ans;
}
double get_dist(int x1,int y1,int x2,int y2)
{
    return sqrt((x1-x2)*(x1-x2)+(y1-y2)*(y1-y2));
}
int main()
{
  // freopen("C:/Users/ASUS/Desktop/output.txt","w",stdout);
  // freopen("C:/Users/ASUS/Desktop/input.txt","r",stdin);
   in2(n,k);
   f(i,1,n,1)
   {
       int x,y;
       in2(x,y);
       a[i].first=x,a[i].second=y;
   }
   f(i,1,n,1)
     f(j,i+1,n,1)
      {
          m++;
          q[m].a=i;
          q[m].b=j;          	
          q[m].dist=get_dist(a[i].first,a[i].second,a[j].first,a[j].second);
      }  
    printf("%.2lf",Kruskal());
	return 0;
}

走廊泼水节

题目链接:走廊泼水节

返回目录:点这里

题意,给你一棵树,你需要把它扩充成一个完全图,使得这个图的最小生成树还是它而只能是它。

那题意就很明确,你加的边要严格大于原来的边(相对而言),相对而言的意思就是加的边,对应的那个点原来树上那条边。比如原来A到B的权值是C,那么新加的边里面,A到其他点的边都要严格大于C。

这道题其实还是有难度的。如果是第一次做这道题可能很难想到这个解法。

思路就是首先把每个点看成独立的一个连通块,然后再去和其他连通块合并。连通块连接的时候,想要加的权值最小,那其实还是要按照Kruskal算法把边从小到大排序,然后按照Kruskal算法的一个逻辑,进行连边,连边的时候,加的权值就是当前边权值+1,然后连完边记得要把连通块的数量合并一下。

更具体一点就是,首先每个点都视作一个连通块,cnt值为1,然后去和其他连通块合并,合并时,比如有两个连通块,数量分别是cnt[A]和cnt[B],那么新加的边权总值应该是 (q[i].w+1)*(cnt[A]+cnt[B]-1)。要减一的原因是原来有一条边,所以可以减一条边。然后记得合并,合并要按照并查集的原则来合并,要根据父节点的合并来合并连通块的数量。

这题其实挺抽象的,读者可以好好理解,配合代码一起理解。

这里因为要用Kruskal算法的规则,所以这里用的是Kruskal算法。

#pragma GCC diagnostic error "-std=c++11"  
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
#include<set>
#include<stack>
#include<map>
#include<cmath>
#include<climits>
#define in(x) x=read()
#define out(x) printf("%d\n",x);
#define f(x,a,b,c) for(int x=a;x<=b;x+=c) 
#define in2(x,y) scanf("%d%d",&x,&y);
#define in3(x,y,z) scanf("%d%d%d",&x,&y,&z);
#define in4(x,y,z,v) scanf("%d%d%d%d",&x,&y,&z,&v);
#define out2(a,b) printf("%d %d\n",a,b);
#define out3(a,b,c) printf("%d %d %d\n",a,b,c);
#define out4(a,b,c,d) printf("%d %d %d %d\n",a,b,c,d);

using namespace std;
typedef pair<int,int>PII;
typedef long long LL;
const int N=1e4+10,M=2*N,mod=23333333;
int n,m,k,s;
int T;
int p[M];
PII a[N];
int cnt[N];
LL ans=0;
inline int read() {
    int x=0,f=1;
    char c=getchar();
    while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}
    while(c>='0'&&c<='9') x=x*10+c-'0',c=getchar();
    return x*f;
}
struct node{
    int a,b;
    int w;
    bool operator<(const node&t)const
    {
        return w<t.w;
    }
}q[M];
int find(int x)
{
    if(x!=p[x])p[x]=find(p[x]);
    return p[x];
}
void Kruskal()
{
    sort(q+1,q+n-1+1);
    f(i,1,n,1)p[i]=i;
    f(i,1,n-1,1)
    {
        int a=find(q[i].a),b=find(q[i].b);
        if(a!=b)
        {
            p[b]=a;
            ans+=((q[i].w+1)*(cnt[a]*cnt[b]-1));
            cnt[a]+=cnt[b];
        }
    }
}
int main()
{
   //freopen("C:/Users/ASUS/Desktop/output.txt","w",stdout);
   //freopen("C:/Users/ASUS/Desktop/input.txt","r",stdin);
   in(T);
   while(T--)
   {
       in(n);
       ans=0;      
       f(i,1,n,1)cnt[i]=1;
       f(i,1,n-1,1)
       {
           int a,b,c;
           in3(a,b,c);
           q[i]={a,b,c};
       }
      Kruskal();
      out(ans);
   }
}

秘密的牛奶运输

题目链接:秘密的牛奶运输

返回目录:点这里

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值