欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

[Usaco2004 Nov]Til the Cows Come Home 带奶牛回家(Dijkstra的优化)

程序员文章站 2022-03-22 22:18:35
...

Problem

Time Limit: 1 Sec Memory Limit: 128 MB
Description
贝茜在谷仓外的农场上,她想回到谷仓,在第二天早晨农夫约翰叫她起来挤奶之前尽可能多地睡上一觉.由于需要睡个好觉,贝茜必须尽快回到谷仓.农夫约翰的农场上有N(2≤N≤1000)个路标,每一个路标都有唯一的编号(1到N).路标1是谷仓,路标N是贝茜一整天呆在那里的果树园.农场的所有路标之间共有T(1≤T≤2000)条不同长度的供奶牛走的无向小路.贝茜对她识别方向的能力不是很自信,所以她每次总是从一条小路的头走到尾,再以这条路的尾作为下一条路的头开始走. 现给出所有路标之间的小路,要求输出贝茜回到谷仓的最短路程(每组输入数据都保证有解).
Input
第1行:2个整数T和N.
第2到T+1行:每行用空格隔开的三个整数描述一条小路.前两个整数是这条小路的尾和头,
第三个整数是这条小路的长度(不大于100).
Output
一个整数,表示贝茜从路标N到路标1所经过的最短路程
Sample Input

5 5
1 2 20
2 3 30
3 4 20
4 5 20
1 5 100

Sample Output

90

HINT

共有5个路标,贝茜可以依次经过路标4,3,2,1到家。

1.题目分析

此题明显最短路,是求1 to n的最短路。
这里我们选择Dijkstra算法来求最短路。
这里强调一下,Dijkstra算法的核心就是贪心,这也正是为什么它不能处理负边权的原因。
[PS:注意是无向路]

2.Dijkstra算法的逆袭

①朴素的Dijkstra&卡数据

直接朴素的算法走一波,但要注意一点,其实也不能说注意,主要是数据的问题,
或者说邻接表的局限,所以这里我们要特殊处理一下[#滑稽]。

未优化Dijkstra Code

#include <cstdio>
#include <cstring>
#include <algorithm>
#pragma GCC optimize(2)
#define ri register int //放寄存器里,卡常数
#define N 1001
using namespace std;

int t,n;
bool vis[N];
int dis[N],g[N][N];

void Dijkstra()
{
	for (ri i=1;i<=n;++i) dis[i]=g[1][i]; //dis初始化
	dis[1]=0,vis[1]=true; //标记
	int min,u; //u表示当前找到的最短边所连接的未被访问点
	for (ri i=1;i<n;++i)
	{
		min=0x3f3f3f3f,u=0; //min,u初始化
		for (ri p=1;p<=n;++p)
			if (!vis[p]&&min>dis[p]) u=p,min=dis[p];
			//如果当前找的点未被访问且比上次找的最短路小,更新
		if (!u) break; //如果未被更新,则说明都已求出最短路,退出
		vis[u]=true; //标记当前找的点为true
		for (ri v=1;v<=n;++v)
			if (u!=v&&dis[v]>dis[u]+g[u][v])//找到点不与其重合且小于目前所找的最短距离
				dis[v]=dis[u]+g[u][v]; //更新
	}
}

int main()
{
	memset(g,0x7f,sizeof(g)); //g初始化
	scanf("%d%d",&t,&n);
	for (ri u,v,w,i=1;i<=t;++i)
	{
		scanf("%d%d%d",&u,&v,&w);
		g[u][v]=g[v][u]=min(w,g[u][v]);
		//好了这里就是关键中的关键[划去]
		//由于数据里出现奇葩的都是描述一组边的,
		//而答案偏偏是这些同一条边中权值最小的,所以用min...,不然输出最后一组边的长
	}
	Dijkstra();
	printf("%d",dis[n]);
	return 0;
}

好了,这就是我们经过数据洗礼的Dijkstra算法的代码,但这是因为本蒟蒻偷看了数据才知道的问题,比赛有时也像这么坑,那么我们应该怎样用一种新东西来替代邻接表呢?所以,这里要引进一种高级的链式前向星!!!

小插曲:链式前向星

这个名字确实很屌,但它实际上就是邻接表的静态实现。——转自链式前向星实现以及它的遍历,Kirai大佬的良言
那么它的优点是能够节省空间上的浪费,同时也能够有向的访问每条边及每个节点,效率高
自然如此高超的东西编程复杂度也就有点小高,但是其实也不难,本蒟蒻个人认为
跟建树的过程有点像
我们先来看看建树的代码:

1. 建树的Code

int tot,pre[N],now[N],son[N];
//pre表示当前新加的边的前一条边,now表示以x点为起点的未加新边之前的最后一条边,son表示当前边所指向的后一个节点,tot表示当前边的数量,即编号
void add(int x,int y)
{
	pre[++tot]=now[x]; //将当前新加边的前继赋值为当前边,同时tot先自增
	now[x]=tot; //将当前点x连接的最后一条边赋值为当前边数量,即编号
	son[tot]=y; //将当前编号边所指向的节点赋值为y
}

果然非常的绕,但实际上我们只要弄清楚什么是当前边,前继边,当前节点,后继点与编号就行了。
那么这里now数组所储存的当前边的编号一定是最后一条与当前点相连的边。
所以,我们可以知道,在使用这样的形式遍历树时,其实是倒序遍历,看看代码就清楚了。

2. 树的遍历Code

int tot,pre[N],now[N],son[N];
bool fl[N];
//N根据题目情况认定
void dfs(int w)
{
	fl[w]=true; //将当前点标记为已遍历
	for (int p=now[w];p;p=pre[p])
	//这里从最后一条边开始遍历,注意根它是没有前驱,所以for的终止条件是p==0,每次将p赋值为它的前驱边
	{
		int t=son[p]; //当前边所连接的后驱节点
		if (!fl[t]) //当前节点未被访问
			dfs(t); //继续遍历
	}
}

这里需要自己多行体会,本蒟蒻就不再多赘述了。
好的,我们来回归正题:链式前向星
其实它的原理跟存树差不多,所以我们定一个结构体。

3. 链式前向星Code

struct Edge
{
	int pre; //当前点所连接边的前驱边
	int to; //相当于前文的son
	int w; //这里多了个权值,很好理解
} edge[T]; //边的存储
int now[T]; //同上文的now
//T因题的数据范围而定

那么加边也大同小异。

4. 建立图Code

inline void add_edge(int u,int v,int w) //起止点与权值
{
	edge[++tot].pre=now[u];
	//now[u]表示u点当前边的最后一条边
	edge[tot].to=v;
	//当前边所连的点
	edge[tot].w=w; //权值
	now[u]=tot; //将当前u点所连接的最后一条边的序号更新
}

访问更不用说。

5. 图的遍历Code


struct Edge
{
	int pre; //当前点所连接边的前驱边
	int to; //相当于前文的son
	int w; //这里多了个权值,很好理解
} edge[T]; //边的存储
int now[T]; //同上文的now
bool vis[N];
//T因题的数据范围而定
void dfs(int u)
{
	for (int p=now[u];p;p=edge[p].pre)
	{
		int v=edge[p].to;
		if (!vis[v]) dfs(v);
	}
}
//同遍历树那里

好了,总结一下:
链式前向星每添加一条边就会更新now数组,使得now数组存放的总是最新添加的某点的出边,此出边的now总指向now数组之前存放的出边的序号。——仍然转自先前那位大佬,多有冒犯
所以说我们在遍历整个图时,是倒序遍历的。

②插曲结束&Dijkstra的回顾

好了,再次回到Dijkstra算法,我们知道,它的思路就是贪心,具体的做法[pascal]是这样的:

Var
   i,j,u,v,min:longint;
   min,u:longint; {min存储当前找到的边的最小值,u表示当前边通向的点}
   vis:array[0..100] of boolean; {当前是否被访问}
   dis:array[0..100] of longint; {最短距离}
   G:array[0..100,1..100] of longint; {邻接表存图}
Begin
   {读入及存图部分,省略}
   for i:=1 to n-1 do
   begin
      min:=999999;
      u:=0; {min,u初始化}
      for j:=1 to n do
         begin
         if (vis[j]=false)and(min>dis[j]) then {如果当前点没被访问且小于当前找到的最短距离}
            begin
               u:=j; {更新点}
               min:=dis[j]; {更新距离,便于下一次比较}
            end;
         end;
      if u=0 then break; {如果没有找到新节点,退出}
      vis[u]:=true; {标记为已访问}
      for v:=1 to n do
         if (u<>v)and(dis[u]+w[u][v]) then {点不能重合且小于当前最短距离}
            dis[v]:=dis[u]+w[u][v]; {更新最短距离}
   end;
End.

[毕竟不是专学Pascal的,写得不好请见谅]
其中我们发现这样一段代码:

for j:=1 to n do
begin
   if (vis[j]=false)and(min>dis[j]) then {如果当前点没被访问且小于当前找到的最短距离}
   begin
      u:=j; {更新点}
      min:=dis[j]; {更新距离,便于下一次比较}
   end;
end;

这里就是在找当前没被访问的最短距离最小的点,每次找到一条边权最小的边,那么我们的做法就是很愚蠢的枚举,这时,我们想到了!!!

③优化!优化!堆的运用

堆其实就是优先队列,升级了一点,它就是便于我们快速地取出具有优先属性的元素,本蒟蒻不再赘述对堆的描述。
那么我们具体的做法就是,每找到一条能够更新的最短权值的边时,我们就将其丢入堆中,同时定义结构体规定其优先性,由于此时堆的结构变得复杂,我们还需要运算符重载
注意此时要不仅记录当前最短路的数值,还要记录当前最短路到达的节点。

优化AC具体代码如下

#include <queue>
#include <cctype>
#include <cstdio>
#include <cstring>
#pragma GCC optimize(2)
#define ri register int //卡常数
#define T 4001
#define N 1001
using namespace std;

int t,n,tot;
struct Edge //定义链式前向星
{
	int pre;
	int to;
	int w;
} edge[T];
int now[T];
int dis[N];
struct path //这就是关键的结构体
{
	int pos,val;
	//表示当前最短路到达的点 及 最短距离的值
	bool operator < (const path &x) const //对'<'进行重载
	{
		return val>x.val; //这里就是指将最短距离从小到大排
		//注意写法是:(element元素) (比较规则) x.(element元素);
	} //运算符重载,有兴趣的神犇可以去了解
	/*
    	具体格式如下:
		(返回类型) operator (运算符) (const (结构体名称) &x) const
    	{
    		...; //具体代码,对其重载
    	}
	*/
} ;
bool vis[N]; //标记当前节点是否被访问

void Dijkstra()
{
	priority_queue<path> heap;
	//定义一个path类型的大根堆,由于数据越大的越在堆上部,而运算符重载使得最短路越小的越大,所以最短路越小的越在上部
	memset(dis,0x7f,sizeof(dis)); //dis数组初始化
	dis[1]=0,heap.push((path){1,0});
	//dis出发点赋值,以及结构体放入堆中的写法:(结构体名称){...}
	//PS注意{}中的元素按顺序,别弄反了,同时这时不将vis赋值,不然直接退出while,自行体会
	while (!heap.empty()) //堆非空
	{
		path u=heap.top(); //定义一个path的u,取出最短路点
		heap.pop(); //取出来
		if (vis[u.pos]) continue; //若当前最短路径所到达的点已被访问,continue
		vis[u.pos]=true; //当前节点标记为已访问
		for (int p=now[u.pos];p;p=edge[p].pre) //遍历
		{
			int v=edge[p].to; //取出它所连接的边
			if (dis[v]>dis[u.pos]+edge[p].w) //比较
			{
				dis[v]=dis[u.pos]+edge[p].w; //更新
				heap.push((path){v,dis[v]});
				//注意当前已被修改,那么当前的点与其所匹配的最短距离也可能修改其它点的最短路,丢入堆中
			}
		}
	}
}

inline void add_edge(int u,int v,int w) //建图
{
	edge[++tot].pre=now[u];
	edge[tot].to=v;
	edge[tot].w=w;
	now[u]=tot;
}

inline void read(int &x) //快读
{
	x=0;int f=1;char ch=getchar();
	while (!isdigit(ch)) {if (ch=='-') f=-f;ch=getchar();}
	while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
	x*=f;
}

int main()
{
	read(t),read(n);
	for (ri i=0;i<t;++i)
	{
		int u,v,w;
		read(u),read(v),read(w);
		add_edge(u,v,w),add_edge(v,u,w);
	} //读入,注意是无向图,要加两次边
	Dijkstra(); //链式前向星+堆 优化
	printf("%d",dis[n]);
	return 0;
}

好了,本篇博客到此结束,Dijkstra完成了它的优化,时间复杂度大大减少,应该是O((m+n)log n)的。

3. 如有不当之处请各位神犇及时指出!