单源最短路的拓展问题

1.选择最佳路线(多个起点一个终点)

 思路:这道题目有一个特点,就是有多个起点。
我们可以建立一个虚拟的原点,把原点向所有起点连一条边,价值为0,这样就不会影响答案。
所以,直接以虚拟原点为起点,做一遍spfa。

#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
#include<bits/stdc++.h>
using namespace std;
const int N = 1010,M=40010;
int n,m,T;
int h[N], e[M], w[M], ne[M], idx;
int q[N], dist[N];
bool st[N];
void add(int a,int b,int c)//加边操作
{
    e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
int spfa() //求最短路
{
    memset(dist, 0x3f, sizeof dist);
    dist[0] = 0;
    int hh = 0, tt = 1;
    q[0] = 0;

    while (hh != tt)
    {
        int t = q[hh ++ ];
        if (hh == N) hh = 0;
        st[t] = false;

        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                if (!st[j])   
                {
                    q[tt ++ ] = j;
                    if (tt == N) tt = 0;
                    st[j] = true;
                }
            }
        }
    }

    if (dist[T] == 0x3f3f3f3f) return -1;
    return dist[T];
}

int main()
{
    while(scanf("%d%d%d",&n,&m,&T)!=-1)//有多组测试数据
    {
        memset(h,-1,sizeof h);
        memset(st, 0, sizeof st);
        idx=0;
        while(m--)
        {
            int a,b,c;
            scanf("%d%d%d",&a,&b,&c);
            add(a, b, c);//原来就有的边
        }
        int s;
        scanf("%d",&s);
        while(s--)
        {
            int ver;
            scanf("%d",&ver);//输入多个原点
            add(0,ver,0);//建立价值为0的边
        }
        printf("%d\n",spfa());
    }
    return 0;
}

2.有钥匙走迷宫(dp思想)

题目分析
对于每个点,它们的属性不光有(x,y)
坐标这个属性,还有是否有钥匙这个属性,所以一个点(x,y),可以根据其属性来写为三维状态f(x,y,state),表示从起点到(x,y)
这个点且当前拥有个要是状态是state的路线最短路
看似可以用dp(动态规划来做这个题),但是用dp做,计算当前状态就需要之前的所有状态,但是这是一个网格状的棋盘,是可以走回路的
比如说,一个人在(x,y)先去拿钥匙,再回(x,y),出现环形结构,那么计算当前状态反而需要当前状态的值,出现矛盾,不太好计算
所以可以把f(x,y,state),当成一个三维坐标,但和正常的三维坐标不同的是,第三个状态不是位置,而是一种钥匙的状态,但不妨碍可以借鉴三维坐标。
先用dp的思想来想状态来如何转移key
来表示当前位置的钥匙存在状态
此时这是拿当前位置钥匙的方程转移f(x,y,state)=min(f(x,y,state),f(x,y,state|key)),拿钥匙不消耗时间
如果不拿钥匙,往四个方向走,需要消耗1个时间点
此时f(x,y,state)=min(f(a,b,state)+1,f(x,y,state))
需要的结构是拓扑序,导致不能正常转移有环形依赖的结构则需要最短路解决

通过上述的状态转移方程,发现当前位置有钥匙的话,捡起钥匙,不需要消耗体力,所以就相当于边权为0
如果不捡位置,取周围4个方向的位置(能走过去的话),则需要消耗1个体力,相当于边权为1
此时问题就简化为了,一个三维状态点,且边权是0和1的最短路问题
边权是0和1的可以用Spfa/双端队列广搜/Dijkstra
来做,双端队列BFS时间复杂度是线性的,这里题解代码就仿照y总,写了个双端BFS双端版

处理细节
1.降维:对于状态f(x,y,state)
可以把二维坐标(x,y)降维成一维,把状态简化成f(t.state)
2.钥匙状态表示:对于钥匙的状态,可以用二进制来表示,拥有那种钥匙,就是2的多少次方
例如,拥有1号钥匙,就是21,二进制下就是10状态状态state,拿起当前位置的钥匙就是或运算,state|=key
3.对于门和墙和普通通道的表示,可以用邻接表来表示点与点之间是什么门
先用set来存两个点之间的不同门,对于墙,标记一下,不用存,代表该边不可走
最后遍历其他所有的点对,看看是否在set里,不在的话,说明是普通通道,标记为普通就行

#include <bits/stdc++.h>
using namespace std;
#define x first
#define y second
typedef pair<int, int> PII;
const int N = 11, M = 400, P = 1 << 10;
int n, m, k, p;
int h[N * N], e[M], w[M], ne[M], idx;
int g[N][N], key[N * N];
int dis[N * N][P];
bool vis[N * N][P];
set<PII> edge;
int dx[4] = {-1, 0, 1, 0};
int dy[4] = {0, 1, 0, -1};
void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
//没门没墙的就是普通通道
void build() {
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            for (int k = 0; k < 4; k++) {
                int x = i + dx[k];
                int y = j + dy[k];
                if (x < 1 || x > n || y < 1 || y > m) continue;
                int a = g[i][j], b = g[x][y];
                if (!edge.count({a, b})) {
                    add(a, b, 0);
                }
            }
        }
    }
}
//经典双端队列BFS处理01边权最短路
int bfs() {
    memset(dis, 0x3f, sizeof(dis));
    dis[1][0] = 0;
    deque<PII> deq;
    deq.push_back({1, 0});
    while (deq.size()) {
        PII t = deq.front();
        deq.pop_front();

        if (vis[t.x][t.y]) continue;
        vis[t.x][t.y] = 1;

        if (t.x == n * m) return dis[t.x][t.y];

        //捡起钥匙的情况
        if (key[t.x]) {
            int state = t.y | key[t.x];
            if (dis[t.x][state] > dis[t.x][t.y]) {
                dis[t.x][state] = dis[t.x][t.y];
                deq.push_front({t.x, state});
            }
        }
        for (int i = h[t.x]; i != -1; i = ne[i]) {
            int j = e[i];
            //这是当前有门,但是没钥匙的情况
            if (w[i] != 0 && !(t.y >> w[i] - 1 & 1)) continue;
            if (dis[j][t.y] > dis[t.x][t.y] + 1) {
                dis[j][t.y] = dis[t.x][t.y] + 1;
                deq.push_back({j, t.y});
            }
        }
    }
    return -1;
}
int main() {
    scanf("%d%d%d%d", &n, &m, &p, &k);
    //二维降维一维
    for (int i = 1, t = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            g[i][j] = t++;
        }
    }
    //初始化
    memset(h, -1, sizeof(h));
    //处理门和墙
    while (k--) {
        int x1, y1, x2, y2, s;
        scanf("%d%d%d%d%d", &x1, &y1, &x2, &y2, &s);
        int a = g[x1][y1], b = g[x2][y2];
        edge.insert({a, b});
        edge.insert({b, a});//点明此处是墙
        if (s != 0) add(a, b, s), add(b, a, s);
    }
    //建立普通通道
    build();
    int s;
    scanf("%d", &s);
    //二进制数字表示钥匙状态
    while (s--) {
        int x, y, c;
        scanf("%d%d%d", &x, &y, &c);
        key[g[x][y]] |= 1 << c - 1;//地图存在钥匙时的状态
    }
    printf("%d\n", bfs());
    return 0;
}

3.最短路计数

 

BFS做法 

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010, M = 400010, mod = 100003;

int n, m;
int h[N], e[M], ne[M], idx;
int dist[N], cnt[N];
int q[N];

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

void bfs()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    cnt[1] = 1;

    int hh = 0, tt = 0;
    q[0] = 1;

    while (hh <= tt)
    {
        int t = q[hh ++ ];

        for (int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[t] + 1)
            {
                dist[j] = dist[t] + 1;
                cnt[j] = cnt[t];
                q[ ++ tt] = j;//第一次遍历的一定是最短路
            }
            else if (dist[j] == dist[t] + 1)
            {
                cnt[j] = (cnt[j] + cnt[t]) % mod;
            }
        }
    }
}

int main()
{
    scanf("%d%d", &n, &m);
    memset(h, -1, sizeof h);

    while (m -- )
    {
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, b), add(b, a);
    }

    bfs();

    for (int i = 1; i <= n; i ++ ) printf("%d\n", cnt[i]);

    return 0;
}

4.观光(求最短路和次短路)

#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstdio>
#include<queue>
using namespace std;
const int N = 1010,M = 10010;
int h[N],e[M],ne[M],w[M],idx;
//状态0表示的是求最小,状态1表示求的是次小
int cnt[N][2];  //cnt[i][0]表示当前到达节点是i,且求的是0状态下的所有路径中最短路径的边数
int dist[N][2]; //dist[i][0]表示当前到达节点是i,且求的是0状态下的最短路径的值
bool st[N][2];  //与上面同理
int n,m,S,T;  //S表示起点,T表示终点

struct node{   //小根堆,重载大于号
    int id,type,distance;   //分别是编号,状态,和当前点到起点的最小或次小距离
    bool operator> (const node& a) const{  //从大到小排序
        return distance > a.distance;
    }
};


void add(int a,int b,int c){
    w[idx] = c;
    e[idx] = b;
    ne[idx] = h[a];
    h[a] = idx++;
}

int dijkstra(){

    memset(st, 0, sizeof st);
    memset(dist, 0x3f, sizeof dist);
    memset(cnt, 0, sizeof cnt);

    priority_queue<node,vector<node>,greater<node>> heap;
    dist[S][0] = 0;
    cnt[S][0] = 1;
    heap.push({S,0,0});

    while(heap.size()){

        node t = heap.top();
        heap.pop();

        int ver = t.id , type = t.type , distance = t.distance; 
        if(st[ver][type]) continue;
        st[ver][type] = true;


        for(int i = h[ver];i != -1;i = ne[i]){
            int j = e[i];

            //先考虑最短的情况(大于、等于)
            if(dist[j][0] > dist[ver][type] + w[i]){
                //dist[j][0]成为次小,先要赋值给dist[j][]中次小的状态
                dist[j][1] = dist[j][0]; cnt[j][1] = cnt[j][0];
                heap.push({j, 1, dist[j][1]});  //发生改变就要入队

                dist[j][0] = dist[ver][type] + w[i];  cnt[j][0] = cnt[ver][type];  //直接转移
                heap.push({j,0,dist[j][0]});

            }else if(dist[j][0] == dist[ver][type] + w[i]){  

                cnt[j][0] += cnt[ver][type]; //从t经过的最短路,在j上经过的时候也是最短路

            //轮到枚举次小
            }else if(dist[j][1] > dist[ver][type] + w[i]){

                dist[j][1] = dist[ver][type] + w[i];
                cnt[j][1] = cnt[ver][type];
                 heap.push({j, 1, dist[j][1]});

            }else if(dist[j][1] == dist[ver][type] + w[i]){
                cnt[j][1] += cnt[ver][type];  //从t经过的最短路,在j上经过的时候也是最短路
            } 
        }
    }
    int res = cnt[T][0];
    //最后还要特判以下最小和次小的路径之间是否相差1符合要求
    if (dist[T][0] + 1 == dist[T][1]) res += cnt[T][1];  
    return res;
}


int main(){
    int t;
    cin >> t;
    while(t--){
        memset(h,-1,sizeof h);
        cin >> n >> m;
        for(int i = 0;i < m;++i){
            int a,b,c;
            scanf("%d%d%d",&a,&b,&c);
            add(a,b,c);
        }
        scanf("%d%d",&S,&T);
        cout << dijkstra() << endl;
    }
    return 0;
}

作者:牛蛙点点
链接:https://www.acwing.com/solution/content/60646/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值