AcWing 383 观光

题目描述:

“您的个人假期”旅行社组织了一次比荷卢经济联盟的巴士之旅。

比荷卢经济联盟有很多公交线路。

每天公共汽车都会从一座城市开往另一座城市。

沿途汽车可能会在一些城市(零或更多)停靠。

旅行社计划旅途从 S 城市出发,到 F 城市结束。

由于不同旅客的景点偏好不同,所以为了迎合更多旅客,旅行社将为客户提供多种不同线路。

游客可以选择的行进路线有所限制,要么满足所选路线总路程为 S 到 F 的最小路程,要么满足所选路线总路程仅比最小路程多一个单位长度。

3463_1.png

如上图所示,如果S = 1,F = 5,则这里有两条最短路线1->2->5,1->3->5,长度为6;有一条比最短路程多一个单位长度的路线1->3->4->5,长度为7。

现在给定比荷卢经济联盟的公交路线图以及两个城市 S 和 F,请你求出旅行社最多可以为旅客提供多少种不同的满足限制条件的线路。

输入格式

第一行包含整数 T,表示共有 T 组测试数据。

每组数据第一行包含两个整数 N 和 M,分别表示总城市数量和道路数量。

接下来 M 行,每行包含三个整数 A,B,L,表示有一条线路从城市 A 通往城市 B,长度为 L。

需注意,线路是 单向的,存在从A到B的线路不代表一定存在从B到A的线路,另外从城市A到城市B可能存在多个不同的线路。

接下来一行,包含两个整数 S 和 F,数据保证 S 和 F 不同,并且S、F之间至少存在一条线路。

输出格式

每组数据输出一个结果,每个结果占一行。

数据保证结果不超过10^9。

数据范围

2≤N≤1000,
1≤M≤10000,
1≤L≤1000,
1≤A,B,S,F≤N

输入样例:

2
5 8
1 2 3
1 3 2
1 4 5
2 3 1
2 5 3
3 4 2
3 5 4
4 5 3
1 5
5 6
2 3 1
3 2 1
3 1 10
4 5 2
5 2 7
5 2 7
4 1

输出样例:

3
2

分析:

本题虽然属于dijkstra算法的扩展应用,但是从不论是解题思路还是最终AC都没那么容易。题目的关键字有:正权边,存在重边,求最短路径条数与比最短路径长度多一的路径条数之和,换而言之,就是求出最短路径条数与次短路径条数之和,规定次短路径只能比最短路径长度多一。

先来说下我最初的思路,虽然只过了一半用例,但是其思想还是值得深究的。dijkstra在求最短路径的过程中核心思想是松弛操作,即d[j] = min(d[j],d[u] + w),这个式子充满了DP的思想,只不过不是按照正常的线性顺序去扩展状态的。现在对于一个节点而言,不仅有这个起点到节点的最短距离以及最短路径的条数这两个状态,还有次短距离以及次短路径的条数这两个状态,当状态比较多时,用状态机去求解就很方便了。f[i]和c1[i]表示到i点的最短路径长度和条数,g[i]和c2[i]表示到i点次短路径的长度和条数。当c2[i]不为0时,g[i] == f[i] + 1,c2[i]为0时表示没有次短路径。下面分析状态的转移,点j的最短路只能由点u的最短路转移而来,即d[j] = min(d[j],d[u] + w)这个式子还是不变的,c1[j]在f[j]被更新时同步被更新为c1[u],在遇见等于最短路径的情况时c1[j] += c1[u]。次短路径可能由u的次短路径转移而来,也可能由u的最短路径转移而来。如果u的最短路径加上边权恰好是j的最短路径,那么u的次短路径加上边权就是j的次短路径;如果u的最短路径加上边权恰好比j的最短路径大一,则u的最短路径加上边权就是j的次短路径,用式子表示就是f[u] + w < f[j]时,f[j] = f[u] + w,g[j] = g[u],c1[j] = c1[u],c2[j] = c2[u];f[u] + w == f[j]时,c1[j] += c1[u],c2[j] += c2[u];f[u] + w == f[j] + 1时,g[j] = g[u],c2[j] += c2[u];如果画个状态图上面状态转移的关系就一目了然了。我最初的思路就是dijkstra算法主体不变,只将最短路径长度加入堆中,然后在堆顶元素出堆时,加上上面的状态转移操作。逻辑似乎没有错误,但是为什么只过了一半用例呢?这是因为动态规划要求状态的无后效性,dijkstra算法正确性的前提就是取出堆中距离最小的顶点后,它的最短距离后面不会被更新为更小的了,这就可以理解为状态的无后效性,一个顶点出队时它的状态就不会再改变了。

如上图所示,dijkstra求最短路长度和条数的时候A最先出堆,然后是B和C出堆,然后C出堆,然后D出堆,我们假设C比B先出堆,C出堆后,A经过B是不可能更新C的最短路的,所以C的最短路径长度和最短路径的条数都不会被更新了。但是如果同时更新次短路的状态呢?A出堆后更新C和B的状态,然后C出堆,此时C的最短路径长度是1,最短路径路径条数是1,然后C去更新D的状态,D的次短路径此时是不存在的,次短路径的条数自然是0;然后B出堆,更新了C的次短路状态,但是C已经出堆了,不会再去更新D的次短路状态了,这时的算法就出错了。用一句话概括下就是我们固然可以通过一个顶点的状态去更新另一个顶点的状态,但是必须保证前一个顶点(出堆的时候)状态已经确定下来了,否则B更新C的状态后无法传递到D。

正确求解本题只需要将上述思想略加调整,既然dijkstra算法出堆时只有顶点的最短路信息被确定了,那么我们将次短路信息也加入到堆中,只有一个顶点次短路出堆时,它的次短路信息才会被确定下来。对应在算法上的改变就是B在更新C的状态时,不仅需要尝试把C的最短路径长度加入堆中(当然这里1 + 1 > 1没有松弛成功不用入堆),C的次短路径长度被更新为2也要加入到堆中,然后尽管C作为最短路时出堆了,作为次短路还可以再出堆一次,出堆时C的次短路信息就定下来了,也可以去更新D的次短路信息了。

整理下思路,我们需要放进优先级队列中顶点的信息有:路径长度、节点编号、路径类型,类型type为0时表示最短路径,type为1时表示次短路径,在执行状态转移时这里并不需要分类讨论出堆的是最短路径还是次短路径。设dist为路径长度加上边权之和,当dist小于最短路径长度时,尝试更新最短路径和次短路径,这时不用管type的原因在于只有最短路径才会满足这个分支条件;当dist等于最短路径长度时,更新下最短路径信息即可,这个分支被触发的前提也只可能是type是最短路径的情况下,如果u的次短路径出队加上边权等于j的最短路径的话,先于u的次短路径出堆的最短路径出堆时一定会更新掉j的最短路径;当dist等于最短路径长度+1时,如果j的次短路径条数为0表明这是第一次更新j的次短路,需要将次短路入队,同时更新次短路信息,这个分支type去0或者1的情况下都可能触发,对应了上面分析中次短路径可以由次短路径或者最短路径转移而来。具体的实现见代码:

#include <iostream>
#include <algorithm>
#include <queue>
#include <cstring>
using namespace std;
const int N = 1005,M = 10005;
int idx,h[N],e[M],w[M],ne[M];
int n,m,d[N][2],cnt[N][2];
bool st[N][2];
struct Node{
    int dist,idx,type;
    bool operator < (const Node X)const{
        return dist > X.dist;//把小于号定义为大于号,大根堆就变成小根堆了
    }
};
priority_queue<Node> pq;
void add(int a,int b,int c){
    e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
int dijkstra(int s,int t){
    memset(d,0x3f,sizeof d);
    memset(cnt,0,sizeof cnt);
    memset(st,false,sizeof st);
    d[s][0] = 0,cnt[s][0] = 1;
    pq.push({d[s][0],s,0});
    while(pq.size()){
        Node node = pq.top();
        int u = node.idx,type = node.type;
        pq.pop();
        if(st[u][type])   continue;
        st[u][type] = true;
        for(int i = h[u];~i;i = ne[i]){
            int j = e[i];
            int dist = d[u][type] + w[i];
            if(dist < d[j][0]){
                if(dist + 1 == d[j][0]){
                    d[j][1] = d[j][0],cnt[j][1] = cnt[j][0]; 
                    pq.push({d[j][1],j,1});
                }
                d[j][0] = dist,cnt[j][0] = cnt[u][0];
                pq.push({d[j][0],j,0});
            }
            else if(dist == d[j][0])    cnt[j][0] += cnt[u][0];
            else if(dist == d[j][0] + 1){
                if(!cnt[j][1]){
                    d[j][1] = dist;
                    pq.push({d[j][1],j,1});
                }
                cnt[j][1] += cnt[u][type];//这里一定要写成type,因为type是0或者1都可能触发该分支
            }   
        }
    }
    return cnt[t][0] + cnt[t][1];
}
int main(){
    int T,S,F,a,b,c;
    scanf("%d",&T);
    while(T--){
        memset(h,-1,sizeof h);
        idx = 0;
        scanf("%d%d",&n,&m);
        for(int i = 0;i < m;i++){
            scanf("%d%d%d",&a,&b,&c);
            add(a,b,c);
        }
        scanf("%d%d",&S,&F);
        printf("%d\n",dijkstra(S,F));
    }
    return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值