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

Menu

  • Problem
  • 1.题目分析
  • 2.Dijkstra算法的逆袭
    • ①朴素的Dijkstra&卡数据
      • 未优化Dijkstra Code
    • 小插曲:链式前向星
      • 1. 建树的Code
      • 2. 树的遍历Code
      • 3. 链式前向星Code
      • 4. 建立图Code
      • 5. 图的遍历Code
    • ②插曲结束&Dijkstra的回顾
    • ③优化!优化!堆的运用
      • 优化AC具体代码如下
  • 3. 如有不当之处请各位神犇及时指出!

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 
#include 
#include 
#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 
#include 
#include 
#include 
#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. 如有不当之处请各位神犇及时指出!

你可能感兴趣的:(入门OJ,Usaco2004,Nov,图论算法,最短路)