• caioj 1914 & CH 0x20搜索(0x27A*)例题1:第K短路 Remmarguts'Date


    【题意】
    给定一张N个点(编号1,2…N),M条边的有向图,求从起点S到终点T的第K短路的长度,路径允许重复经过点或边。

    【输入格式】
    第一行包含两个整数N和M。
    接下来M行,每行包含三个整数A,B和L,表示点A与点B之间存在有向边,且边长为L。
    最后一行包含三个整数S,T和K,分别表示起点S,终点T和第K短路。

    【输出格式】
    输出占一行,包含一个整数,表示第K短路的长度,如果第K短路不存在,则输出“-1”。

    【数据范围】
    1S,TN1000,1≤S,T≤N≤1000,
    0M1050≤M≤10^5,
    1K1000,1≤K≤1000,
    1L1001≤L≤100

    【输入样例】
    2 2
    1 2 5
    2 1 4
    1 2 2

    【输出样例】
    14

    方法1: 爆搜,但很明显边界不好设定,所以既不能保证正确性也会T。

    方法2: 输出-1,可以水到40分。

    其实S,T不连通就一定要输出-1。我们可以递归判联通性。

    方法3: 有算法的暴力。

    前置知识:dijkstra——dijkstra本质上是贪心:每次取路程最小的点出来贡献,然后把它删除。如果不用堆的话,那么每次取点时就需要O(N)枚举。算法的朴素做法复杂度O(N2)O(N^2),用堆优化则O(Nlog2N)O(N log_2N)
    注意:dijkstra只能用在只有非负权边的图!!

    模板题
    朴素代码:

    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #define l b[0]
    #define g getchar()
    using namespace std;
    const int N=1e5+10;
    const int M=N<<1;
    const int inf=0x3f3f3f3f;
    
    void qr(int &x)
    {
        char c=g;x=0;
        while(!('0'<=c&&c<='9'))c=g;
        while('0'<=c&&c<='9')x=x*10+c-'0',c=g;
    }
    void write(int x)
    {
        if(x/10)write(x/10);
        putchar(x%10+'0');
    }
    
    //边 
    struct edge{int y,d,nxt;}a[M];int last[N],len;
    void ins(int x,int y,int d){a[++len]=(edge){y,d,last[x]};last[x]=len;}
    //堆 
    int n,m,s,b[N],d[N],p[N];
    void up(int j)
    {
        int i=j>>1;
        while(i&&d[b[i]]>d[b[j]])
        {
            swap(b[i],b[j]);
            p[b[i]]=i;p[b[j]]=j;
            j=i;i=j>>1;
        }
    }
    void down(int i)
    {
        int j=i<<1;
        if(j<l&&d[b[j]]>d[b[j+1]])j++;
        while(j<=l&&d[b[i]]>d[b[j]])
        {
            swap(b[i],b[j]);
            p[b[i]]=i;p[b[j]]=j;
            i=j;j=i<<1;
            if(j<l&&d[b[j]]>d[b[j+1]])j++;
        }
    }
    void add(int x){b[++l]=x;p[b[l]]=l;up(l);}
    void del(){b[1]=b[l--];p[b[1]]=1;down(1);}
    //dijkstra
    void dijkstra()
    {
        //初始化 
        for(int i=1;i<=n;i++)d[i]=inf;
        l=d[1]=0;add(1);
        while(l!=0)
        {
            int x=b[1];del();
            for(int k=last[x];k;k=a[k].nxt)
            {
                int y=a[k].y;
                if(d[y]>d[x]+a[k].d)
                {
                    d[y]=d[x]+a[k].d;
                    if(p[y])up(p[y]);
                    else add(y);
                }
            }
        }
        for(int i=1;i<=n;i++)write(d[i]),putchar(' ');
        puts("");
    }
    int main()
    {
        qr(n);qr(m);qr(s);
        int x,y,c;
        while(m--)qr(x),qr(y),qr(c),ins(x,y,c);
        dijkstra();
        return 0;
    }
    

    用stl的代码:

    #include<queue>
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #define g getchar()
    #define mk(x,y) make_pair(x,y)
    using namespace std;
    void qr(int &x)
    {
    	char c=g;x=0;
    	while(!('0'<=c&&c<='9'))c=g;
    	while('0'<=c&&c<='9')x=x*10+c-'0',c=g;
    }
    void write(int x)
    {
    	if(x/10)write(x/10);
    	putchar(x%10+'0');
    }
    
    const int N=1e5+10;
    const int M=N<<1;
    struct edge{int y,d,next;}a[M];int last[N],len;
    void ins(int x,int y,int d){a[++len]=(edge){y,d,last[x]};last[x]=len;}
    
    priority_queue<pair<int,int> >q;
    int n,m,d[N];bool v[N];
    void dijkstra()
    {
    	memset(d,63,sizeof(d));d[1]=0;
    	q.push(mk(0,1));
    	while(q.size())//!q.empty()
    	{
    		int x=q.top().second;q.pop();
    		if(v[x])continue;	 v[x]=1;//每个点只贡献一次
    		for(int k=last[x];k;k=a[k].next)
    		{
    			int y=a[k].y;
    			if(d[y]>d[x]+a[k].d) 
    			{
    				d[y]=d[x]+a[k].d;
    				q.push(mk(-d[y],y));//大根堆
    			}
    		}
    	}
    	for(int i=1;i<=n;i++)write(d[i]),putchar(' ');
    	puts("");
    }
    int main()
    {
    	memset(last,0,sizeof(last));len=0;//初始化是好习惯 
    	qr(n);qr(m);
    	int x,y,c;qr(x);
    	while(m--)qr(x),qr(y),qr(c),ins(x,y,c);
    	dijkstra();
    	return 0;
    }	
    

    思路:这道题朴素的spfa是做不了的。为什么呢?

    我们知道朴素spfa有两种实现方法:深搜,广搜。深搜在这道题中是不可取的。

    而广搜是用队列来实现的,如果以下标为x坐标,路程为y坐标。会出现下面的情况:
    在这里插入图片描述
    路程并不有序,所以我们不知道什么时候才是到T的第k短路。

    那我们还是用dijkstra吧。(用数据结构使得路程有序)

    代码:(只有80分,还不如直接输出-1)

    //80分。 
    #include<queue>
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #define g getchar()
    #define mk(x,y) make_pair(x,y)
    using namespace std;
    void qr(int &x)
    {
    	char c=g;x=0;
    	while(!('0'<=c&&c<='9'))c=g;
    	while('0'<=c&&c<='9')x=x*10+c-'0',c=g;
    }
    void write(int x)
    {
    	if(x/10)write(x/10);
    	putchar(x%10+'0');
    }
    
    const int N=1010;
    const int M=1e5+10;
    struct edge{int y,d,next;}a[M];int last[N],len;
    void ins(int x,int y,int d){a[++len]=(edge){y,d,last[x]};last[x]=len;}
    priority_queue<pair<int,int> >s;//大根堆 
    int n,m,st,ed,k,vis,tot;bool v[N];
    bool dfs(int x)//判断联通性
    {
    	if(x==ed)return 1;
    	v[x]=1;
    	for(int k=last[x];k;k=a[k].next)
    	{
    		int y=a[k].y;
    		if(!v[y])
    		{
    			if(dfs(y)==true)return 1;
    		}
    	}
    	return 0;
    }
    void dijkstra()
    {
    	if(!dfs(st)){puts("-1");return;}
    	s.push(mk(0,st));//第一关键字为路程,第二关键字为标号
    	if(st==ed)vis=-1;
    	while(s.size())
    	{
    		int d=s.top().first,x=s.top().second;s.pop();
    		if(x==ed)
    		{
    			if(++vis==k)
    				{write(-d);return;}
    			tot=0;
    		}
    		else if(++tot==N*15)break;//防止搜过多 
    		for(int k=last[x];k;k=a[k].next)
    		{
    			int y=a[k].y;
    			s.push(mk(d-a[k].d,y));
    		}
    	}
    	puts("-1");
    }
    		
    int main()
    {
    	freopen("1914.in","r",stdin);
    	freopen("1914.out","w",stdout);
    	qr(n);qr(m);
    	int x,y,c;
    	while(m--)qr(x),qr(y),qr(c),ins(x,y,c);
    	qr(st);qr(ed);qr(k);
    	dijkstra();
    	return 0;
    }
    
    
    

    方法4:正解(AA^*

    如果给定一个“目标状态”,需要求出从初态到目标状态的最小代价,那么优先队列BFS的这个“优先策略”显然是不完善的。一个状态的当前代价最小,只能说明从起始状态到该状态的代价很小,而在未来的搜索中,从该状态到目标状态可能会花费很大的代价:另外一些状态虽然当前代价略大,但是未来到目标状态的代价可能会很小,于是从起始状态到目标状态的总代价反而更优。优先队列BFS会优先选择前者的分支.

    为了提高搜索效率,我们很自然地想到,可以对未来可能产生的代价进行预估。详细地讲,我们设计一个“估价函数”,以任意“状态”为输入,计算出从该状态到目标状态所需代价的估计值。在搜索中,我们仍然维护一一个堆,不断从堆中取出“当前代价+未来估价”最小的状态进行扩展。

    为了保证第-次从堆中取出目标状态时得到的就是最优解,我们设计的估价函数需要满足一个基本准则: 设当前状态statestate到目标状态所需代价的估计值为f(state)f(state)。 设在未来的搜索中,实际求出的从当前状态state 到目标状态的最小代价为g(state)g(state)。 对于任意的state,应该有f(state)g(state)f(state) ≤g(state)。

    也就是说,估价函数的估值不能大于未来实际代价,估价比实际代价更优。

    为什么呢?
    因为如果本来在最优解搜索路径上的状态被错误地估计了较大的代价,被压在堆中无法取出,就会导致非最优解搜索路径上的状态不断扩展,直至在目标状态上产生错误的答案。
    如果我们设计估价函数时遵守上述准则,保证估值不大于未来实际代价,那么即使估价不太准确,导致非最优解搜索路径上的状态s先被扩展,但是随着“当前状态”的不断累加,在目标状态被取出之前的某个时刻:
    1 .根据s并非最优,s的“当前状态”就会大于从起始状态到目标状态的最小代价。
    2 .对于最优解搜索路径上的某个状态t,因为f(t)g(t)f(t)le g(t),所以
    t+f(t)t"+g(t)()<st的“当前状态”+f(t)le t的“当前状态"+g(t)(最小代价)le 最优解<s的“当前代价”
    t这样t就一定能成为堆顶,出来贡献到目标状态
    综上:只要f(state)g(state)f(state) ≤g(state),就一定能保证当目标状态取出的代价为最小代价。

    哇!好优秀的算法!

    在这里插入图片描述
    很快就搜完了

    于是乎,算出恰当的ff很关键,ff要尽量大但不超过最优解

    在这里插入图片描述

    第几次出队就是第几短,于是终点出了k次就是第k短路了

    按照dijsktradijsktra的思想,我们每次取出d[x]+f[x]d[x]+f[x]最小的

    然后更新所有能到达的点

    发现f[x]f[x]可以取到终点的距离,这样尽量大且一定比现在的解小

    于是先倒着dijkstra(f)dijkstra一遍(搞出f)

    然后AA*,直到终点第k次。
    代码:

    #include<queue>
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #define g getchar()
    #define mk(x,y) make_pair(x,y)
    using namespace std;
    void qr(int &x)
    {
    	char c=g;x=0;
    	while(!('0'<=c&&c<='9'))c=g;
    	while('0'<=c&&c<='9')x=x*10+c-'0',c=g;
    }
    void write(int x)
    {
    	if(x/10)write(x/10);
    	putchar(x%10+'0');
    }
    
    const int N=1010;
    const int M=1e5+10;
    //建边 
    struct edge{int y,d,next;}a[M],b[M];int last[N],len,lastb[N];//b为反向边 
    void ins_a(int x,int y,int d){a[++len]=(edge){y,d,last[x]};last[x]=len;}
    void ins_b(int x,int y,int d){b[len]=(edge){y,d,lastb[x]};lastb[x]=len;}
    
    priority_queue<pair<int,int> >q;//注意:这是大根堆 
    int n,m,f[N],st,ed,p,v[N];
    void dijkstra() 
    {
    	memset(f,63,sizeof(f));f[ed]=0;
    	memset(v,-1,sizeof(v));
    	q.push(mk(0,ed));
    	while(q.size())//等价于while(!q.empty())
    	{
    		int x=q.top().second;q.pop();
    		if(!v[x])continue;	 v[x]=0;//每个点只贡献一次
    		for(int k=lastb[x];k;k=b[k].next)
    		{
    			int y=b[k].y;
    			if(f[y]>f[x]+b[k].d) 
    			{
    				f[y]=f[x]+b[k].d;
    				q.push(mk(-f[y],y));//大根堆
    			}
    		}
    	}
    }
    void A_star()
    {
    	if(f[st]==f[0]){puts("-1");return;}//不连通 
    	if(st==ed)p++;//题意没讲清楚。它规定路径必须有边。 
    	q.push(mk(-f[st],st));
    	while(q.size())
    	{
    		int x=q.top().second,d=-q.top().first-f[x];
    		q.pop();
    		v[x]++;
    		if(v[ed]==p){write(d);puts("");return;}
    		for(int k=last[x];k;k=a[k].next)
    		{
    			int y=a[k].y;
    			if(v[y]!=p)q.push(mk(-d-a[k].d-f[y],y));
    //重要剪枝——因为默认为大根堆并且每次取最小值,所以必须插入相反数或重载运算符。 
    		}
    	}
    	puts("-1"); 
    }
    int main()
    {
    	freopen("1914.in","r",stdin);
    	freopen("1914.out","w",stdout);
    	memset(last,0,sizeof(last));
    	memset(lastb,0,sizeof(lastb));
    	len=0;//初始化是好习惯
    	qr(n);qr(m);
    	int x,y,c;
    	while(m--)qr(x),qr(y),qr(c),ins_a(x,y,c),ins_b(y,x,c);
    	qr(st);qr(ed);qr(p);
    	dijkstra();//跑反图,求出优秀的估价函数
    	A_star(); 
    	return 0;
    }	
    
    

    最后,谈谈数据范围。

    为什么不用开long  longlong~~long呢?
    这就需要我们求极值了。
    极值需要满足以下情况:(正常人都想得到)

    1. 边数不多不少(那不就是要链状并且点数最大化(1000)吗)
    2. 每条边的长度最大化(100)
    3. k最大化(1000)
      形如这样:(连边为双向边)
      在这里插入图片描述
      从7到1的最短路为7~1,走到1号再返回(其实就是不动)
      次短路为7~1,走到2号再返回.
      第三短路为7~1,走到3号再返回.

    这不是规律很明显吗。
    如果是1000个点的第1000短路,不就是1000号点走到1号点,走回1000号点再返回吗。
    dis=9991003=299700dis=999*100*3=299700。

    终于讲完了,记得点赞。

  • 相关阅读:
    返回数组指针的函数形式
    zoj 2676 网络流+01分数规划
    2013 南京理工大学邀请赛B题
    poj 2553 强连通分支与缩点
    poj 2186 强连通分支 和 spfa
    poj 3352 边连通分量
    poj 3177 边连通分量
    poj 2942 点的双连通分量
    poj 2492 并查集
    poj 1523 求割点
  • 原文地址:https://www.cnblogs.com/zsyzlzy/p/12373906.html
Copyright © 2020-2023  润新知