【图论专题】单源最短路的扩展应用

题目列表:

题目 算法
AcWing 1137. 选择最佳线路 最短路+超级源点
AcWing 1131. 拯救大兵瑞恩 拆点建图+思维+双端队列BFS+状态压缩
AcWing 1134. 最短路计数 记录最短路条数
AcWing 383. 观光 求最短路和次短路的方案数,特解,不可拓展
P3953 逛公园 求最短路等等到第k次短路方案数,通解,可拓展

A、AcWing 1137. 选择最佳线路(最短路+超级源点)

【图论专题】单源最短路的扩展应用_第1张图片
本题是一道多起点,自己选择单起点单终点最短路

可以直接建反图从终点出发跑最短路。但是这种方法不可拓展,没有什么意思。

这里使用可拓展的方法,可以拓展为多起点多终点选择一个起点终点最短路最优问题。时间复杂度为Dij的 O ( m l o g n ) O(mlogn) O(mlogn),而不是使用Floyd的 O ( n 3 ) O(n^3) O(n3)

方法如下:
建立一个超级源点,源点与各起点连权值为0的边,本题中只有一个终点。(若为多终点,即为二分图匹配问题,需要再建超级汇点,跑最大流)

代码如下:
我顺便用了spfa的SLF优化

#include
using namespace std;

typedef long long ll;
typedef pair<int,int> PII;

const int N = 30001, M = 300010, INF = 0x3f3f3f3f;

int ver[M], edge[M], head[N], nex[M], tot = 1;
int n, m, s;
int ed;
int dis[N];
bool vis[N];

void add(int x,int y,int z){
     
    ver[++tot] = y;
    edge[tot] = z;
    nex[tot] = head[x];
    head[x] = tot;
}

int spfa(){
     
    memset(dis,0x3f,sizeof dis);
    memset(vis,0,sizeof vis);
    deque<int>q;
    dis[s] = 0;
    vis[s] = true;
    q.push_back(s);
    while(q.size()){
     
        int x = q.front();
        q.pop_front();
        vis[x] = false;
        for(int i = head[x];i;i = nex[i]){
     
            int y = ver[i],z = edge[i];
            if(dis[y] > dis[x] + z){
     
                dis[y] = dis[x] + z;
                if(!vis[y]){
     
                    vis[y] = true;
                    if(q.size() && dis[y] < dis[q.front()])
                        q.push_front(y);
                    else q.push_back(y);
                }
            }
        }
    }
    if(dis[ed] == INF)return -1;
    return dis[ed];
}

int main(){
     
    while(~scanf("%d%d%d",&n,&m,&ed)){
     
        memset(head,0,sizeof head);
        tot = 1;
        s = 0;
        while(m--){
     
            int x,y,z;
            scanf("%d%d%d",&x,&y,&z);
            add(x,y,z);
        }
        int w;
        scanf("%d",&w);
        while(w--){
     
            int x;
            scanf("%d",&x);
            add(s,x,0);
        }
        printf("%d\n",spfa());
    }
    return 0;
}

B、AcWing 1131. 拯救大兵瑞恩(拆点+双端队列BFS)

【图论专题】单源最短路的扩展应用_第2张图片
样例:

输入样例
4 4 9
9
1 2 1 3 2
1 2 2 2 0
2 1 2 2 0
2 1 3 1 0 
2 3 3 3 0
2 4 3 4 1
3 2 3 3 0
3 3 4 3 0
4 3 4 4 0
2
2 1 2 
4 2 1


输出样例
14

【图论专题】单源最短路的扩展应用_第3张图片
从集合角度来分析最优化问题。dp和最短路都可用看成在集合上的最优化问题。
【图论专题】单源最短路的扩展应用_第4张图片
双端队列BFS实际上就是Dijkstra的简化版,既是是最坏的时间复杂度也为线性 O ( n ) O(n) O(n),用于解决权值只有0和1的最短路问题,优于Dijkstra和spfa。

这里的权值就是0(没墙没门),1(有门)的问题,所以使用双端队列BFS解决更优。

#include
using namespace std;

typedef long long ll;
typedef pair<int,int> PII;

const int N = 1000, M = 10000, P = 1 << 10, INF = 0x3f3f3f3f;

int ver[M], edge[M], head[N], nex[M], tot = 1;
int n, m, k, p;
int g[N][N];
int ed;
int key[N * N];//当前钥匙的状态
int dis[N][N];//编号+状态
bool vis[N][N];
set<PII>edges;

void add(int x,int y,int z){
     
    ver[++tot] = y;
    edge[tot] = z;//这里的edge实际上存的是钥匙的种类,或者0是空地
    nex[tot] = head[x];
    head[x] = tot;
}

void build(){
     //构建所有的可以直接走的空边(空地)(权值为0)
    int dx[4] = {
     1,-1,0,0}, dy[4] = {
     0,0,1,-1};
    for(int i = 1;i <= n;++i)
        for(int j = 1;j <= m;++j)
            for(int u = 0; u < 4;++u){
     //四个方向
                int nx = i + dx[u],ny = j + dy[u];
                if(!nx || nx > n || !ny || ny > m)
                    continue;
                int a = g[i][j], b = g[nx][ny];//取出左边的编号
                if(!edges.count({
     a,b}))//如果不是墙或者门就添边,0代表是空地
                    add(a,b,0);
            }
}
/*0入队头1入队尾*/
int bfs(){
     
    memset(dis,0x3f,sizeof dis);
    memset(vis,0,sizeof vis);//spfa的vis是不用初始化的
    
    dis[1][0] = 0;//起点是1当前状态是0(没有钥匙)
    
    deque<PII>q;
    q.push_back({
     1,0});
    while(q.size()){
     
        PII x = q.front();
        q.pop_front();
        //点的编号和状态
        int id = x.first, now = x.second;
        if(vis[id][now])continue;
        vis[id][now] = true;
        if(id == n * m)//到达终点
            return dis[id][now];
            
        /*0有钥匙有门入队头*/
        if(key[id]){
     //如果这个地方有钥匙就全部拿起来最优
            int state = now | key[id];//更新状态,一直累加
            if(dis[id][state] > dis[id][now]){
     //更新
                dis[id][state] = dis[id][now];
                q.push_front({
     id,state});
            }
        }
        
        /*1没门没墙可以直接走,入队尾*/
        for(int i = head[id];i;i = nex[i]){
     
            int y = ver[i],z = edge[i];
            if(z && !(now >> z & 1))//如果有门并且没有这种钥匙
                continue;
            if(dis[y][now] > dis[id][now] + 1){
     
                dis[y][now] = dis[id][now] + 1;
                q.push_back({
     y,now});
            }
        }
    }
    return -1;
}

int main(){
     
    cin>>n>>m>>p>>k;
    for(int i = 1,t = 1;i <= n;++i)
        for(int j = 1;j <= m;++j)
            g[i][j] = t++;
    while(k--){
     
        int x1, y1, x2, y2, z;
        scanf("%d %d %d %d %d",&x1, &y1, &x2, &y2, &z);
        int a = g[x1][y1],b = g[x2][y2];
        edges.insert({
     a, b}),edges.insert({
     b, a});

        if(z)//如果是门就连通路
            add(a, b, z),add(b, a, z);
    }
    build();
    
    int s;
    cin>>s;
    while(s--){
     
        int x, y, z;
        cin >> x >> y >> z;//z 这里我习惯从第0位开始
        key[g[x][y]] |= 1 << z;
        /*
           当前点是否有钥匙,有什么钥匙,
           因为一个点可能有多个,所以用|=
                                           */
    }
    cout<<bfs()<<endl;
    return 0;
}

C、AcWing 1134. 最短路计数(记录最短路条数)

【图论专题】单源最短路的扩展应用_第5张图片
记录最短路条数
要求最短路计数首先满足条件是不能存在值为0的环,因为存在的话那么被更新的点的条数就为INF了。
要把图抽象成一种最短路树(拓扑图)。
求最短的算法有以下几种

  • BFS 只入队一次,出队一次。可以抽象成拓扑图, 因为它可以保证被更新的点的父节点一定已经是最短距离了,并且这个点的条数已经被完全更新过了。这个性质是核心性质。
  • dijkstra 每个点只出队一次。也可以抽象成拓扑图, 同理由于每一个出队的点一定已经是最短距离,并且它出队的时候是队列中距离最小的点,这就代表他的最短距离条数已经被完全更新了,所以构成拓扑性质。
  • bellman_ford算法 spfa 本身不具备拓扑序,因为更新它的点不一定是最短距离,所以会出错。
    如下图所示:
    【图论专题】单源最短路的扩展应用_第6张图片
    但如果图中存在负权边只能用该算法做,也能做但是比较麻烦
    先跑一遍spfa找到每个点的最短距离,把最短路拓扑树建立出来,看哪一条边 d i s t [ j ] = = d i s t [ t ] + w [ i ] dist[j] == dist[t] + w[i] dist[j]==dist[t]+w[i],然后更新它。
    上述题解来源
#include
using namespace std;

typedef long long ll;
typedef pair<int,int> PII;

const int N = 300000, M = 3000005, INF = 0x3f3f3f3f,mod = 100003;

int n,m;
int ver[M],nex[M],head[N],tot = 1;
int dist[N],cnt[N];//距离和方案数
bool vis[N];

void add(int x,int y){
     
    ver[++tot] = y;
    nex[tot] = head[x];
    head[x] = tot;
}
queue<int>q;

void bfs(){
     
    memset(dist,0x3f,sizeof dist);
    memset(vis,0,sizeof vis);
    dist[1] = 0;
    cnt[1] = 1;
    q.push(1);
    while(q.size()){
     
        int x = q.front();
        q.pop();
        if(vis[x])continue;
        vis[x] = true;
        for(int i = head[x];~i;i = nex[i]){
     
            int y = ver[i];
            if(dist[y] > dist[x] + 1){
     
                dist[y] = dist[x] + 1;
                cnt[y] = cnt[x];
                q.push(y);
            }
            else if(dist[y] == dist[x] + 1){
     
                cnt[y] = (cnt[y] + cnt[x]) % mod;
            }
        }
    }
}

int main(){
     
    scanf("%d%d",&n,&m);
    memset(head,-1,sizeof head);
    while(m--){
     
        int x,y;
        scanf("%d%d",&x,&y);
        add(x,y),add(y,x);
    }
    bfs();

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

D、AcWing 383. 观光(求最短路和次短路的方案数)

【图论专题】单源最短路的扩展应用_第7张图片

注意求次短路的时候,每次更新最短路时顺带更新次短路并讲次短路的相关信息入队。
判断完不能更新最短路的话就判断一下能否更新次短路。

方案的求法:

如果子节点直接被更新,子结点的方案数就直接等于父节点的方案数。

如果子结点长度等于父节点长度 + 两者的距离,那么方案数累加。

当然图里没有子结点父节点这一说,大家自行体会

#include
#include
#include
#include
#include
using namespace std;

const int N = 30000, M = 300005, INF = 0x3f3f3f3f,mod = 100003;

int n,m;
int ver[M],nex[M],edge[M],head[N],tot = 1;
int dist[N][2],cnt[N][2];/*0是最短路,1是次短路*/
bool vis[N][2];
int S,T,t;

struct node{
     
    int id,type,dis;
    bool operator<(const node &t)const{
     
        return dis > t.dis;
    }
};

void add(int x,int y,int z){
     
    ver[++tot] = y;
    edge[tot] = z;
    nex[tot] = head[x];
    head[x] = tot;
}

int Dijkstra(){
     
    memset(dist,0x3f,sizeof dist);
    memset(vis,0,sizeof vis);
    memset(cnt,0,sizeof cnt);

    dist[S][0] = 0;
    cnt[S][0] = 1;//注意这里应该是初始化为 1
    priority_queue<node>heap;
    heap.push({
     S,0,0});

    while(heap.size()){
     
        node x = heap.top();
        heap.pop();
        int id = x.id,type = x. type,distance = x.dis,counts = cnt[id][type];

        if(vis[id][type])continue;
        vis[id][type] = true;

        for(int i = head[id];~i;i = nex[i]){
     
            int y = ver[i],z = edge[i];
            
            /*先更新最短路*/
            
            if(dist[y][0] > distance + z){
     
                dist[y][1] = dist[y][0],cnt[y][1] = cnt[y][0];//次短路继承了最短路更新前的状态
                heap.push({
     y,1,dist[y][1]});
                
                dist[y][0]  = distance + z;
                cnt[y][0] = counts;
                heap.push({
     y,0,dist[y][0]});
            }
            else if(dist[y][0] == distance + z)cnt[y][0] += counts;
            
            /*再更新次短路*/
            
            else if(dist[y][1] > distance + z){
     //如果不能更新最短路,那就看看能否更新次短路
                dist[y][1] = distance +z;
                cnt[y][1] = counts;
                heap.push({
     y,1,dist[y][1]});
            }
            else if(dist[y][1] == distance + z)cnt[y][1] += counts;
        }
    }
    int res = cnt[T][0];
    if(dist[T][0] + 1 == dist[T][1])
        res += cnt[T][1];
    return res;
}

int main(){
     
    cin>>t;
    while(t--){
     
        memset(head,-1,sizeof head);
        tot = 1;
        scanf("%d%d",&n,&m);
        for(int i = 1;i <= m;++i){
     
            int x,y,z;
            scanf("%d%d%d",&x,&y,&z);
            add(x,y,z);
        }
        scanf("%d%d",&S,&T);
        printf("%d\n",Dijkstra());
    }
    return 0;
}

E、P3953 逛公园(求最短路等等到第k次短路方案数,通解)

【图论专题】单源最短路的扩展应用_第8张图片

你可能感兴趣的:(#,最短路算法,#,第三章,图论,AcWing算法提高课)