一种 SPFA 的新优化

前言

其实这玩意是这样出来的:校内膜你赛有一道最短路,标算 DIJ ,结果 我们有稳定的评测机 几乎卡着时限过(甚至过不去)。于是我交了一份 SPFA ,发现被卡了一个点,然后因为边数少所以队列里几乎没东西。于是我搞出一个玄学的优化碾了标算,于是 LHF 大佬也加入了优化 SPFA 的队列(???),于是搞出了两个能用的东西。

读者仍然需要确保自己会了堆优 DIJ 再搞这些奇技淫巧。

我的思路

奉旨吹 B

用随机化打败毒瘤出题人。

主要思想仍然是维护优先队列。

CODE-FAKE

请不要直接COPY,这份代码有巨大缺陷

#include<ctime>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define reg register
#define uni unsigned
using namespace std;
const uni N=100010,W=2,WT=0,INF=0x7fffffff;    //这里的W与WT的参数小到离谱,竟然过了
uni n,m,s,d[N<<4],a[N],b[N<<2][3],t[N],seed=time(0),w[N],OnF,st[10];
bool vis[N];
char BuF[1<<24],*InF=BuF,WuF[1<<23];
uni read(){
    reg uni x=0;
    for(;47>*InF||*InF>58;++InF);
    for(;47<*InF&&*InF<58;x=x*10+(*InF++^48));
    return(x);
}
void write(uni x){
    if(!x){
        WuF[OnF++]=48;
    }
    for(;x;x/=10){
        st[++st[0]]=x%10+48;
    }
    for(;st[0];WuF[OnF++]=st[st[0]--]);
    WuF[OnF++]=' ';
}
uni rnd(uni l,uni r){
    seed^=seed<<17;seed^=seed>>15;seed^=seed<<3;
    return(seed%(r-l+1)+l);
}
void spfa(uni s){
    memset(w,127,sizeof(w));
    w[d[0]=s]=0;
    for(reg uni h=0,t=0;h<=t;++h){
/*        for(reg uni i=0;i<=WT&&h+W+W+1<t;++i){    //这部分是随机化,等一会会提到
            reg uni x=rnd(h+W+1,t-W-1);
            if(w[d[h]]>w[d[x]]){
                swap(d[h],d[x]);
            }
        }*/
        for(reg uni i=0;i<=W&&h+i+i<t;++i){
            if(w[d[h]]>w[d[h+i+1]]){
                swap(d[h],d[h+i+1]);
            }
            if(w[d[h]]>w[d[t-i]]){
                swap(d[h],d[t-i]);
            }
        }
        for(reg uni i=a[d[h]],mi=INF;i;i=b[i][0]){
            reg uni nxt=b[i][1];
            if(w[nxt]>w[d[h]]+b[i][2]){
                w[nxt]=w[d[h]]+b[i][2];
                if(!vis[nxt]){
                    d[++t]=nxt;
                    if(mi>w[nxt]){
                        if(mi<INF){
                            swap(d[t],d[t-1]);
                        }
                        mi=w[nxt];
                    }
                    vis[nxt]=1;
                }
            }
        }
        vis[d[h]]=0;
    }
}
int main(){
    fread(BuF,1,1<<24,stdin);
    n=read();m=read();s=read();
    for(reg uni last=0;m;--m){
        reg uni x=read(),y=read(),z=read();
        b[++last][0]=a[x];
        b[a[x]=last][1]=y;
        b[last][2]=z;
    }
    spfa(s);
    for(reg uni *i=w+1,*r=w+n+1;i!=r;++i){
        write(*i);
    }
    fwrite(WuF,1,OnF,stdout);
    fclose(stdin);
    fclose(stdout);
    return(0);
}

初始思想是(因为那题数据跑出来队列很短)每次松弛前把前若干项判一下,小于队头就交换。于是把原来的题过了。

接下来为了过 luogu 版题,把后面若干项也做了相同的操作,加上了随机化(在这份代码里没有)。

还有一个重要的优化,就是存一个最小值,如果新进来的数大于最小值就与倒数第二项交换,保证每次松弛的最小值在队尾,下次松弛就有可能换到前面去。

其实上面的代码真的非常非常非常扯淡,你只需要一个像这样的菊花图就可以卡掉:

#include<bits/stdc++.h>
using namespace std;
int main(){
	int n=99999,m=199996;
	printf("%d %d\n",n,m);
	for(int i=n;i>=2;--i) printf("1 %d %d\n%d %d 1\n",i,(n-i+1)*2+1,i,i-1);
}

但其在 luogu 版题的表现如下:

复活 SPFA_SPFA

甚至冲上了第一页(懒得加底层优化了不然更快)喜提第一页(前几页?)唯一的 SPFA ,而且代码比前面的巨佬短很多,可见 luogu 的数据真的水到爆炸……

为我冲榜时调参浪费的 luogu 评测资源默哀,但考虑到这是为了重铸 SPFA 荣光这一伟大事业 *背景微波辐射噪音* 。

快来个人把我卡掉

正常的部分

还是先放一份代码,这次可以正常使用了。

CODE-REAL

#include<ctime>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define reg register
#define uni unsigned
using namespace std;
const uni N=100010,W=5,WT=10,INF=0x7fffffff;    //W为取首位若干项数,WT为随机次数
uni n,m,s,d[N<<4],a[N],b[N<<2][3],t[N],seed=time(0),w[N],OnF,st[10];
bool vis[N];
char BuF[1<<24],*InF=BuF,WuF[1<<23];
uni read(){
    reg uni x=0;
    for(;47>*InF||*InF>58;++InF);
    for(;47<*InF&&*InF<58;x=x*10+(*InF++^48));
    return(x);
}
void write(uni x){
    if(!x){
        WuF[OnF++]=48;
    }
    for(;x;x/=10){
        st[++st[0]]=x%10+48;
    }
    for(;st[0];WuF[OnF++]=st[st[0]--]);
    WuF[OnF++]=' ';
}
uni rnd(uni l,uni r){
    seed^=seed<<17;seed^=seed>>15;seed^=seed<<3;
    return(seed%(r-l+1)+l);
}
void spfa(uni s){
    memset(w,127,sizeof(w));
    w[d[0]=s]=0;
    for(reg uni h=0,t=0;h<=t;++h){
        for(reg uni i=0;i<=WT&&h+W+W+1<t;++i){
            reg uni x=rnd(h+W+1,t-W-1);
            if(w[d[h]]>w[d[x]]){
                swap(d[h],d[x]);    //随机判断交换
            }
        }
        for(reg uni i=0;i<=W&&h+i+i<t;++i){
            if(w[d[h]]>w[d[h+i+1]]){
                swap(d[h],d[h+i+1]);    //与队首判断
            }
            if(w[d[h]]>w[d[t-i]]){
                swap(d[h],d[t-i]);    //与队尾判断
            }
        }
        for(reg uni i=a[d[h]],mi=INF;i;i=b[i][0]){
            reg uni nxt=b[i][1];
            if(w[nxt]>w[d[h]]+b[i][2]){
                w[nxt]=w[d[h]]+b[i][2];
                if(!vis[nxt]){
                    d[++t]=nxt;
                    if(mi>w[nxt]){
                        if(mi<INF){
                            swap(d[t],d[t-1]);    //保持单次松弛最小值在队尾
                        }
                        mi=w[nxt];
                    }
                    vis[nxt]=1;
                }
            }
        }
        vis[d[h]]=0;
    }
}
int main(){
    fread(BuF,1,1<<24,stdin);
    n=read();m=read();s=read();
    for(reg uni last=0;m;--m){
        reg uni x=read(),y=read(),z=read();
        b[++last][0]=a[x];
        b[a[x]=last][1]=y;
        b[last][2]=z;
    }
    spfa(s);
    for(reg uni *i=w+1,*r=w+n+1;i!=r;++i){
        write(*i);
    }
    fwrite(WuF,1,OnF,stdout);
    fclose(stdin);
    fclose(stdout);
    return(0);
}

首先 WT 和 W 被调大了,在比赛环境下可以自由调节,调小了容易被卡但跑的飞快,调大了不会被卡但常数巨大。看代码是骗分还是正解决定。

以下内容可能包含巨大错误,如果错了请大声 D 我。

为什么我这么自信不会被卡?

因为实际上我们并不需要维护一个真正的优先队列,我们只需要保证在队首的是一个不能再松弛的点即可。

如果队列长度为 \(d\) ,有 \(x\) 个点已经求出最短路,那么命中的概率为 \(\frac{WT\cdot x}{d-2W}\) (当然实际小一点),也就是说当 \(WT\) 为 10 时,我们需要 \(\frac{x}{d}\) 小于 10 才不能期望命中,而这样需要每个结点有 10 的度,考虑到大部分环境下图并不会这么稠密,我们可以期望它是命中的,如果图真的这么稠密 反正大家都跑不过去就别跑了正解肯定不是最短路 ,那么(为了让 DIJ 过)点数不会太大,也就意味着队长不会太大,此时只需要稍微调大 WT 即可提高命中率。

以及,这样需要非常高精度的边权。

而且,大部分卡 SPFA 的数据会想办法(需要)让最短路点位于队伍较末尾的位置,所以只要把 W(或者甚至可以队尾队首使用不同的常数)调大就可以规避大部分这样的数据。

实测在这样很多卡 SPFA 的数据中这会比 DIJ 更快。

总之,我们可以期望这样一个随机化的 SPFA 的时间复杂度是 \(O(k(n+m))\) , \(k\) 需要的大小可以手动调调参搞出来。

所以我是不是可以命个名叫 MHTR 。

Minimum Head Tail Random/Relax

快来人卡掉这个异教徒

LHF大佬的思路

一开始跟上面差不多,就是随机取 min ,后来搞出了一个截然不同的科技。


先跑一遍带有入队上限的 SPFA ,然后对权值排序放入队列,然后跑一遍无限制的 SPFA 。

SPFA 的过程中再用一些简单的优化。

虽然时间复杂度下界因为有排序是 \(O(n\log n)\) 的,但是跑得更快是吧。

至于 上界/期望/平均 什么的,我不会。大佬教我

具体请移步 大佬的博客 。虽然写的好像快比这里还少

他那边好像没有放代码,这里放一份代码 ⁄(⁄ ⁄•⁄ω⁄•⁄ ⁄)⁄ 。

CODE

#include<cstdio>
#include<algorithm>
#include<cstdlib>
#define ll long long
#define N 100010
using namespace std;
ll f[N];
int k[N],n;
bool type;
bool cmp(int a,int b)
{
	return f[a]<f[b];
}
struct my_queue{
	private:
	int q[26214000],p[N],head,tail,w,i,cnt;
	public:
	void push(int x)
	{
		if(p[x]) return;
		p[x]=1;
		q[tail++]=x;
		if(head==tail-1) return;
		if(f[q[head]]>f[q[tail-1]])
			swap(q[head],q[tail-1]);
	}
	int top()
	{
		p[q[head]]=0;
		head++;
		if(head+1<tail&&f[q[head]]>f[q[tail-1]])
			swap(q[head],q[tail-1]);
		return q[head-1];
	}
	void clear(){for(int i=1;i<=n;i++) p[i]=0;head=tail=0;}
	bool empty(){return head==tail;}
}q;
struct edge{
	int next,to,s;
}e[N<<1];
int first[N],len,m,s,x,y,z,c[N],B=30;
void add(int a,int b,int c)
{
	e[++len].to=b;
	e[len].next=first[a];
	first[a]=len;
	e[len].s=c;
}
int main()
{
	scanf("%d%d%d",&n,&m,&s);
	for(int i=1;i<=n;i++) f[i]=0x7fffffff,k[i]=i;
	f[s]=0;
	for(int i=1;i<=m;i++)
	{
		scanf("%d%d%d",&x,&y,&z);
		add(x,y,z);
	}
	q.push(s);
	while(!q.empty())
	{
		x=q.top();
		for(int i=first[x],v;i;i=e[i].next)
			if(f[x]+e[i].s<f[(v=e[i].to)]&&++c[v]<=B)
			{
				f[e[i].to]=f[x]+e[i].s;
				q.push(e[i].to);
			}
	}
	sort(k+1,k+n+1,cmp);
	q.clear();
	for(int i=1;i<=n;i++) q.push(k[i]);
	type=1;
	while(!q.empty())
	{
		x=q.top();
		for(int i=first[x];i;i=e[i].next)
			if(f[x]+e[i].s<f[e[i].to])
			{
				f[e[i].to]=f[x]+e[i].s;
				q.push(e[i].to);
			}
	}
	for(int i=1;i<=n;i++) printf("%lld ",f[i]);
}