分层图最短路问题

本文介绍了如何利用分层图解决图论中的最短路径和网络流问题,特别是当边权值可以被特定操作修改时。通过构建多层图并应用Dijkstra或SPFA算法,可以在考虑各种操作条件下找到最优解。同时,提供了两种不同的实现方式,一种是通过复制节点和边,另一种是通过扩展状态数组。此外,文章还给出了相关模板题和练习题,帮助读者深入理解和应用这一方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

分层图最短路

定义

一些图论题,比如最短路、网络流等,题目对边的权值提供可选的操作,比如 可以将一定数量的边权减半或变为零,在此基础上求解最优解。

此时,我们可以利用分层图来解决。

算法

Dijkstra 或 SPFA。

思路

把图复制 kk 次,如此,分成 kk 层,每层仍然是 nn 个点,每层内部的连边方式仍然和原图相同。

除此以外,层与层之间该如何连边呢?这取决于题目中对边的权值提供的可选的操作:对于层内的每条边,复制一条起点相同,终点为下一层的同节点的边,边权修改为经操作后的边权。以下面模板题中的“免票”操作为例:

我们假设现在在第 ii 层,点 uu 和点 vv 之间有一条权值 ww 的边,对于下一层,即 i+1i+1 层,u'u′ 与 v'v′ 之间仍是有一条权为 ww 的边,且对于 uu 与 v'v′ 之间应该加上一条边权为 00 的边。所以如果每一层有 mm 条边,第 ii 层和第 i+1i+1 层之间也一定是有 mm 条边。

如此,整个图建好,接下来就是在该图上按题目中描述的限制求最优解了。

模板题

P3879- JLOI2011-飞行路线

我们按上面描述的思路建图,我们将图分成 k+1k+1 层,第一层的节点 ii,其对应的在第 kk 层的同节点编号为 i+k*ni+k∗n。样例中的数据建出的图如下所示:

题中说到,可以使用 kk 次免票,即有 k+1k+1 层图,每相邻两层图中用边权为 00 的边相连接。

图建好后,我们跑一遍 Dijkstra 或 SPFA 即可。

因为不一定要把所有免费的机会用完,因此,最终输出答案 ans = \min_{i\in[1,k]}\{dis[t+i*n]\}ans=mini∈[1,k]​{dis[t+i∗n]};(以上面第一张图为例,如果我们要求 1\rightarrow 31→3 的最短距离,那么只要用一次减半机会就可以了,因此不用跑到最后一层的 33 号节点,也不能跑到,如果跑到该节点,那么就不是真正的最短路了)。

参考代码

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+5;

struct Edge
{
    int to,next,cost;
}edge[2500001];
int cnt,head[110005];

void add_edge(int u,int v,int c=0)
{
    edge[++cnt]=(Edge){v,head[u],c};
    head[u]=cnt;
}

int dis[110005];
bool vis[110005];
void Dijkstra(int s)
{
    memset(dis,0x3f,sizeof(dis));
    dis[s]=0;
    priority_queue<pair<int,int>,vector<pair<int,int> >,greater<pair<int,int> > > q;
    q.push(make_pair(0,s));
    while(!q.empty())
    {
        int u=q.top().second;
        q.pop();
        if(!vis[u])
        {
            vis[u]=1;
            for(int i=head[u];i;i=edge[i].next)
            {
                int to=edge[i].to;
                if(dis[to]>dis[u]+edge[i].cost) 
                {
                    dis[to]=dis[u]+edge[i].cost;
                    q.push(make_pair(dis[to],to));
                }
            }
        }
    }
}

int main()
{
    int n, m, k, s, t, u, v, c;
    cin >> n >> m >> k >> s >> t;
    for(int i=0;i<m;++i)
    {
        cin >> u >> v >> c;
        add_edge(u,v,c);
        add_edge(v,u,c);
        // 加各层之间边
        for(int j=1;j<=k;++j)
        {
            add_edge(u+(j-1)*n,v+j*n);
            add_edge(v+(j-1)*n,u+j*n);
            add_edge(u+j*n,v+j*n,c);
            add_edge(v+j*n,u+j*n,c);
        }
    }
    Dijkstra(s);
    int ans = 0x3f3f3f3f;
    for (int i = 0; i <= k; i++) 
        ans = min(ans, dis[t+i*n]);
    cout << ans;
    return 0;
}

Copy

写法二、我们也可以对 dis[]dis[] 数组多开一维,用来记录层数,如此,就不需要多复制节点建图。这有点类似于动态规划的思想,用 dis[][]dis[][] 来进行转移。

我们定义:

  • dis[i][j]dis[i][j]:从起点 stst 到 ii 点,使用了 jj 次优惠机会所使用的最小花费.
  • vis[i][j]vis[i][j]:从起点 stst 到 ii 点,使用了 jj 次优惠机会这个状态有没有被标记

我们需要对普通的迪杰斯特拉转移的时候进行一些改变:

  1. 不使用免费机会的时候:
如果 到点u的花费dis[u][k]+从u到v的边权w<到v的边权dis[v][k]
正常转移更新:dis[v][k]=dis[u][k]+w
加入堆q.push(node(v,k,dis[v][k]))

Copy

  1. 使用免费机会的时候:
如果 到点u的时候使用了k次机会的花费dis[u][k]<到v点的时候使用了k+1次机会的边权dis[v][k+1]
这条边免费更新dis[v][k+1]=dis[u][k]
加入堆q.push(node(v,k+1,dis[v+1][k]))

Copy

剩下的就是迪杰斯特拉的堆优化模板了。

#include <bits/stdc++.h>
using namespace std;
#define mem(a, b) memset(a, b, sizeof(a))
const int N = 1e5 + 10;
const int inf = 0x3f3f3f3f;
int n, m, k;
int first[N], tot;

struct edge
{
    int v, w, next;
} e[N * 2];
void add_edge(int u, int v, int w)
{
    e[tot].v = v, e[tot].w = w;
    e[tot].next = first[u];
    first[u] = tot++;
}
struct node
{
    int id, now, k;
    node() {}
    node(int _id, int _k, int _now)
    {
        id = _id, now = _now, k = _k;
    }
    bool friend operator<(node a, node b)
    {
        return a.now > b.now;
    }
};
/*
dis[i][j]:表示从st到i点用了j次免费机会的最小花费
vis[i][j]:表示从st到i点用了j次免费机会有没有被标记过
*/
int dis[N][12], vis[N][12];
void dijkstra(int st)
{
    for (int i = 1; i <= n; i++)
    {
        for (int j = 0; j <= k; j++)
        {
            dis[i][j] = inf;
            vis[i][j] = 0;
        }
    }
    dis[st][0] = 0;
    priority_queue<node> q;
    q.push(node(st, 0, 0));
    while (!q.empty())
    {
        node u = q.top();
        q.pop();
        if (!vis[u.id][u.k])
        {
            vis[u.id][u.k] = 1;
            for (int i = first[u.id]; ~i; i = e[i].next)
            {
                int v = e[i].v, w = e[i].w;
                if (!vis[v][u.k] && dis[u.id][u.k] + w < dis[v][u.k])
                {
                    dis[v][u.k] = dis[u.id][u.k] + w;
                    q.push(node(v, u.k, dis[v][u.k]));
                }
                if (u.k < k && !vis[v][u.k + 1] && dis[u.id][u.k] < dis[v][u.k + 1])
                {
                    dis[v][u.k + 1] = dis[u.id][u.k];
                    q.push(node(v, u.k + 1, dis[v][u.k + 1]));
                }
            }
        }
    }
}
void init()
{
    mem(first, -1);
    tot = 0;
}
int main()
{
    int u, v, w, st, ed;
    scanf("%d%d%d%d%d", &n, &m, &k, &st, &ed);
    st++, ed++;
    init();
    for (int i = 1; i <= m; i++)
    {
        scanf("%d%d%d", &u, &v, &w);
        u++, v++;
        add_edge(u, v, w);
        add_edge(v, u, w);
    }
    dijkstra(st);
    int ans = inf;
    for (int i = 0; i <= k; i++)
        ans = min(ans, dis[ed][i]);
    printf("%d\n", ans);
    return 0;
}

Copy

对于我们当前找到的终点,尝试起点的状态去更新,不选择此条边免费的状态和选择此条边免费的状态,再将这两个状态压入队列去更新可以到达的其他状态。

以上就是这道题的思路。类似题有 Bzoj 2763 飞行路线 和 [USACO09FEB]改造路RevampingTrail

我们有时也用分层图解决一些其它问题,主要是最短路上的涂色问题

这种类型主要思路仍然是多开一维记录当前状态,然后根据当前状态更新其对应状态。我们每找到一个终点,首先尝试用起点状态去更新起点颜色不同的状态,再去更新终点在此时刻的状态,将终点状态压入队列更新其他状态。

类似题有 Codevs 1391 伊吹萃香

练习题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

10247D

我会继续努力,信息技术会越好

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值