分层图最短路
定义
一些图论题,比如最短路、网络流等,题目对边的权值提供可选的操作,比如 可以将一定数量的边权减半或变为零,在此基础上求解最优解。
此时,我们可以利用分层图来解决。
算法
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 条边。
如此,整个图建好,接下来就是在该图上按题目中描述的限制求最优解了。
模板题
我们按上面描述的思路建图,我们将图分成 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 次优惠机会这个状态有没有被标记
我们需要对普通的迪杰斯特拉转移的时候进行一些改变:
- 不使用免费机会的时候:
如果 到点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
- 使用免费机会的时候:
如果 到点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 伊吹萃香