1 条题解

  • 0
    @ 2025-8-24 21:13:49

    自动搬运

    查看原文

    来自洛谷,原作者为

    avatar BlachSnake
    Go hard or go home.

    搬运于2025-08-24 21:13:48,当前版本为作者最后更新于2021-07-03 20:56:57,作者可能在搬运后再次修改,您可在原文处查看最新版

    自动搬运只会搬运当前题目点赞数最高的题解,您可前往洛谷题解查看更多

    以下是正文


    2021/8/29 Update:修改了题解中的两处错误,并在“链式前向星”部分加上了部分注释、增加了建边函数

    1.题意

    给出一张有 nn 个节点,mm有向边的图,求 11 号点到图中每个节点的最短路。

    2.单源最短路问题

    给定一个源点(此题中为 11 号点),求从这个源点到图中每个点的最短路径。

    接下来先考虑怎么把这个有向图存下来。

    用二维数组(设 ai,ja_{i,j} 为点 ii 到点 jj 的距离,空间复杂度 O(n2)O(n^2))存图?肯定不行,题目规定 n3×105n\le3\times10^5,妥妥的 MLE。

    所以就有了这个东西——

    3.链式前向星

    链式前向星,是一种利用单向链表存图的数据结构。

    那么它长什么样呢?

    lsqxx

    其实它就是 nn 个单向链表,第 ii 个单向链表存储从点 ii 开始的每条边所指向的点(图中的 to)以及这条边的边权(图中的 weight)。

    然后把每个链表的头部用一个 head 数组存起来,如果要遍历某个点开始的所有边,直接写

    for(int i=head[x];i;i=next[i])//遍历每一条边
    
    

    就行了。

    那么如何加边呢?

    很简单,只需要在链表的头部插入,就像这样:

    next[++t]=head[x];//记录后继
    to[t]=y;//记录这条边指向的地方
    weight[t]=z;//记录边权
    head[x]=t;//更新链表头部
    

    存图的问题是解决了,但怎么求最短路呢?

    如果用 Dfs 暴搜的话,时间复杂度为 O(n!)O(n!),稳稳的 TLE。

    用 Floyd(其实就是 DP)来做的话,时间复杂度为 O(n3)O(n^3),也会 TLE。

    于是,就有了这么一个算法:

    4.Dijkstra 求最短路

    Dijkstra 单源最短路算法,是一种用贪心实现的最短路算法,只能在边权值均为正的情况下运行。至于为什么只能在边权值为正的情况下运行,等一下会提到。

    这里我们用一个 dis\textit{dis} 数组存储从源点(这里为点 11)到图中每一个点的最短路径长度。

    先来看一下算法流程: dij1

    初始化,把 dis1\textit{dis}_1(源点到源点本身)设为 00dis\textit{dis} 的 其余值设为 \infty

    dij2

    找到 dis\textit{dis} 数组的最小值所在的下标 11,将其打上标记,并用点 11dis\textit{dis} 值去更新它周围的点的 dis\textit{dis} 值,使得 dis2\textit{dis}_2 更新为 33dis3\textit{dis}_3 更新为 22dis5\textit{dis}_5 更新为 66

    这里我把 dis1\textit{dis}_1 的数值以及点 11 标蓝了,是因为这个点的最短路已经被求出来了,所以以后不再更新这个点的 dis\textit{dis} 值。

    至于为什么不再更新这个点的 dis\textit{dis} 值,等一下讲负边权的时候会讲。(这句话你不已经说过一遍了吗……)

    22、点 33 和点 55 因为被点 11 成功更新了 dis\textit{dis} 值,所以我把它们的 dis\textit{dis} 值标黄了。

    44 和点 66 由于没有被遍历到,所以我把它们的 dis\textit{dis} 值标黑了。

    dij3

    找到 dis\textit{dis} 数组没打过标记的最小值所在的的下标 33,将其打上标记,并用点 33dis\textit{dis} 值去更新它周围没打过标记的点的 dis\textit{dis} 值,使得 dis5\textit{dis}_5 更新为 33dis6\textit{dis}_6 更新为 77

    11 因为已经被打上标记了,所以不用更新它的 dis\textit{dis} 值。

    22 因为没被点 33 更新到 dis\textit{dis} 值,所以我这里把他标红了。

    dij4

    找到 dis\textit{dis} 数组没打过标记的最小值所在的的下标 22,将其打上标记,并用点 22dis\textit{dis} 值去更新它周围没打过标记的点的 dis\textit{dis} 值,使得 dis4\textit{dis}_4 更新为 1010

    dij5

    找到 dis\textit{dis} 数组没打过标记的最小值所在的的下标 55,将其打上标记,并用点 55dis\textit{dis} 值去更新它周围的没打过标记点的 dis\textit{dis} 值,然而并没有更新到任何点的 dis\textit{dis}

    dij6

    找到 dis\textit{dis} 数组没打过标记的最小值所在的的下标 66,将其打上标记,并用点 66dis\textit{dis} 值去更新它周围的没打过标记点的 dis\textit{dis} 值,但仍然没有更新到任何点的 dis\textit{dis}

    另外点 22 到点 66 之间那条边的权值是 88,我画图的时候漏了……

    dij7

    找到 dis\textit{dis} 数组没打过标记的最小值所在的的下标 44,将其打上标记,并用点 44dis\textit{dis} 值去更新它周围的没打过标记点的 dis\textit{dis} 值,结果还是没有更新到任何点的 dis\textit{dis}

    所有的点都已经被打上了标记,算法结束。

    为什么这个算法是正确的呢?

    因为你把一个点打上标记后,如果这条路不是最短路,那就必须得从其他的地方绕过来,而这样求出来的路的长度一定比用 Dijkstra 求出来的路要长。

    除非这条路上有负边权,不然你怎样也不可能让这条路变成最短路。

    这就是为什么 Dijkstra 不能跑负权图的原因。(滑稽)

    但是题目规定 1w1071\le w\le10^7,没有负边权啊!(哈哈哈)

    好的,然后我们就可以开开心心地 AC 了这道题……

    了吗?

    当然不。

    由于 Dijkstra 算法查找 dis\textit{dis} 值的最小值的下标的时间复杂度为 O(n)O(n),而查找下标时一共要查找 O(n)O(n) 次,更新每个节点的 dis\textit{dis} 值时一共又要 O(m)O(m) 次,所以总的时间复杂度为 O(n2+m)O(n^2+m),还是过不了……

    那么,怎么优化这个算法呢?

    5.Dijkstra算法的优化

    注意每次查找最小值的时候时间复杂度都是 O(n)O(n) 的,算法的瓶颈就在这里。

    那么我们每次就把这些被更新的点扔进某一个神奇数据结构里,之后只要对着这个神奇数据结构问一发:此时 dis\textit{dis} 数组里没被打上标记的最小值的下标是多少,再把这个点拉出来,把更新后的点扔进去就行了~

    那么问题来了这个神奇数据结构叫什么呢?

    二叉堆啊!

    当把符合要求的那个点弹出堆后,就把其他更新后的点扔进堆中;

    由于扔进去的点的 dis\textit{dis} 值一定比原先在堆里的这个点的 dis\textit{dis} 值要小(就是长江后浪推前浪),所以原先在堆里的那些点可以不用管它,因此如果这个点已经被弹出过(就是被标记过)就直接跳过。/xyx

    时间复杂度为 O(mlogm)O(m\log m),可以通过此题。

    至于堆的写法可以参考一下P3378的题解

    6.Code

    终于可以上代码了……

    不过我这里自己手写了一个堆和一个 pair,实际上也可以用 STL 的 priority_queue,但我不想用……

    #include<cstring>
    #include<cstdio>
    #include<cctype>
    #define ll long long
    using namespace std;
    const int N=300003,M=5555555;
    template<class T>inline void swap(T &x,T &y){T z=x;x=y,y=z;}//交换函数
    namespace io{
    	inline int Read(){//快速读入数字
    		int x=0;
    		bool d=0;
    		char c=getchar();
    		for(;!isdigit(c);c=getchar())if(c=='-')d=1;
    		for(;isdigit(c);c=getchar())x=(x<<3)+(x<<1)+(c^48);
    		if(d)x=~x+1;
    		return x;
    	}
    	inline void Print(ll x){//快速输出数字
    		if(x){
    			if(x<0)x=~x+1,putchar('-');
    			short a[22],l;
    			for(l=0;x;x/=10)a[++l]=x%10;
    			for(;l;--l)putchar(a[l]+48);
    		}
    		else putchar('0');
    		putchar(' ');
    	}
    }
    namespace Classes{
    	int nxt[N],to[N],w[N],h[N],t;//链式前向星
    	inline void AE(int x,int y,int z){//加边操作
    		nxt[++t]=h[x];
    		to[t]=y;
    		w[t]=z;
    		h[x]=t;
    	}
    	template<class T1,class T2>
    	class Pair{//自己手写的pair,也可以用STL的pair
    	public:
    		T1 x;
    		T2 y;
    		inline bool operator<(const Pair<T1,T2>b)const{return x==b.x?y<b.y:x<b.x;}//排序函数
    	};
    	template<class T1,class T2>
    	inline Pair<T1,T2> Make_Pair(T1 x,T2 y){//把两个元素做成一个pair
    		Pair<T1,T2>a;
    		a.x=x,a.y=y;
    		return a;
    	}
    	template<class T>
    	class Priority_Queue{//手写堆,也可以用STL的priority_ueue
    	private:
    		T q[N];
    		int l;
    		inline void Up(int x){
    			int y=x>>1;
    			while(y&&q[x]<q[y]){
    				swap(q[x],q[y]);
    				x=y,y=x>>1;
    			}
    		}
    		inline void Down(int x){
    			int y=x<<1;
    			while(y<=l){
    				if(y<l&&q[y+1]<q[y])++y;
    				if(q[y]<q[x])swap(q[x],q[y]),x=y,y=x<<1;
    				else break;
    			}
    		}
    	public:
    		Priority_Queue(){l=0;}
    		inline bool Empty(){return l==0;}
    		inline void Clear(){l=0;}
    		inline void Push(T x){q[++l]=x,Up(l);}
    		inline void Pop(){q[1]=q[l--],Down(1);}
    		inline T Top(){return q[1];}
    	};
    }
    using namespace io;
    using namespace Classes;
    ll d[N];//dis数组,注意一定要开long long
    bool b[N];//打标记的数组
    Priority_Queue<Pair<ll,int> >q;
    inline void Dijkstra(){
    	memset(d,63,sizeof(d));//初始化
    	d[1]=0;//让源点1的dis值为0
    	int x,y,z;
    	q.Push(Make_Pair(0ll,1));//把源点插入堆中(假装你已经更新过了源点的dis值)
    	while(!q.Empty()){
    		x=q.Top().y,q.Pop();
    		if(b[x])continue;//如果这个点已经被弹出过就跳过
    		b[x]=1;//否则就给它打上标记
    		for(int i=h[x];i;i=nxt[i]){
    			y=to[i],z=w[i];
    			if(d[y]>d[x]+z){//如果这个点的dis值不是最优的
    				d[y]=d[x]+z;//更新这个点的dis值
    				q.Push(Make_Pair(d[y],y));//把更新后的点插入堆中
    			}
    		}
    	}
    }
    int main(){
    	int n=Read(),m=Read(),x,y,z;
    	for(int i=1;i<=m;i++)x=Read(),y=Read(),z=Read(),AE(x,y,z);
    	Dijkstra();
    	for(int i=1;i<=n;i++)Print(d[i]>=0x3f3f3f3f3f3f3f3fll?-1:d[i]);//三元运算符,如果某个点的dis值没被更新过(就是dis值还是初始化的那个值),直接输出-1
    	//否则输出这个点的dis值
    	return 0;
    }
    

    7.一些闲话

    可能会有人就会问了:STL 这么好用,为什么不用 STL 呢?

    因为 STL 的堆使用的数组是动态开空间的 vector,而动态开空间又很慢,因此最好能不用 STL 就不用 STL。

    另外还有一个叫 SPFA 的最短路算法,时间复杂度为 O(km)O(km),其中 kk 为常数,平均值为 22,在随机数据下极快。

    那我们为什么不能用 SPFA 做这道题呢?

    因为 SPFA 在某些特殊构造的数据下,时间复杂度会从 O(km)O(km) 退化成 O(nm)O(nm),而此题的 nnmm 的最大值都为 3×1053\times10^5,因此会超时。

    所以 NOI2018 就出了这样一个梗:/doge

    SPFA died

    • 1

    信息

    ID
    6851
    时间
    2000ms
    内存
    512MiB
    难度
    3
    标签
    (无)
    递交数
    0
    已通过
    0
    上传者