『学习笔记』分层图
分层图,顾名思义,就是将图分成多层。
若要求解一个最短路问题,但是可以将其中 \(k\) 条边的权值改为 \(0\),那么就是使用分层图的时候了。对于一些其它类似的网络流问题,有涉及到改边权的,也可以用到。
为方便描述,上面的改权值操作下文中称为 "优惠"。
分层图的步骤
-
分 \(k+1\) 层图,从 \(0\) 开始编号,每层图与原图一样。其中第 \(i\) 层图是用了 \(i\) 次优惠到达的图。
-
对于任意一条边从 \(u\) 连向 \(v\),若其在第 \(n\) 层图中,那么就新连一条边从 \(u\) 指向第 \(n+1\) 层图的 \(v\),权值为优惠的值。例如,上面的问题是改为 \(0\),那么这一条新边的权值就为 \(0\)。需要将所有这样的边都进行处理。
-
从第 \(0\) 层图的起始顶点开始跑单源最短路,跑完后即可按需求取答案。例如要求用完所有优惠券,那么就要输出第 \(k\) 层的终点与第 \(0\) 层的起点的最短路了。
原理
来看看两层图中间连的边:只要走了,到达下一层图了,就回不来了,即使图中存在环也是。
跑最短路过程中,要从第 \(0\) 层图跑到第 \(k\) 层图。
而且走了优惠边后去到的那个顶点是下一层图的 \(v\),和直接走原边到达的顶点可以说是一样的。只不过用了一次优惠。
所以结论就是:到达第 \(n\) 层图后,就说明一定用了 \(k\) 次优惠。
如果在任意一层图上到达了终点,那么也可以尝试更新答案。
P4568 [JLOI2011]飞行路线
题目大意
给定一张无向带权图,其中有 \(k\) 次机会可以将一条边的权值变为 \(0\),求最短路。
思路
板子一道。
对于建边,我们可以在输入时进行。
先按正常情况建双向边,再从 \(1\) 循环到 \(k\),每次将第 \(i\) 层图的边建立起来,并连两条从第 \(i-1\) 层到第 \(i\) 层,权值为 \(0\),一条从第 \(i-1\) 层的 \(u\) 连向第 \(i\) 层的 \(v\),另一条则相反,\(i-1\) 层的 \(v\) 连向 \(i\) 层的 \(u\)。
还要注意一下数据范围。
\(2 \leq n \leq 10^4\),\(1 \leq m \leq 5 \times 10^4\),\(0 \leq k \leq 10\)。
那么,要开的顶点数和边数就是:
\(\texttt{MAXN}=nk=10^4 \times 10=10^5\),\(\texttt{MAXM}=2mk=2 \times 5 \times 10^4 \times 10=5 \times 10^5 \times 2=10^6\)。
这个 \(k\) 不知道咋回事,要两倍才行。还有这个 \(m\) 也是,要多开一点。\(\texttt{MAXM}\) 我开到了 \(5 \times 10^6\) 才行。
代码
#include <iostream>
#include <queue>
using namespace std;
template<typename T=int>
inline T read(){
T X=0; bool flag=1; char ch=getchar();
while(ch<'0' || ch>'9'){if(ch=='-') flag=0; ch=getchar();}
while(ch>='0' && ch<='9') X=(X<<1)+(X<<3)+ch-'0',ch=getchar();
if(flag) return X;
return ~(X-1);
}
template<typename T=int>
inline void write(T X){
if(X<0) putchar('-'),X=~(X-1);
T s[20],top=0;
while(X) s[++top]=X%10,X/=10;
if(!top) s[++top]=0;
while(top) putchar(s[top--]+'0');
putchar('\n');
}
const int N=1e6+5,M=5e6+5,inf=0x3f3f3f3f;
struct edge{
int to,nxt,val;
}e[M]; // 链式前向星用
int n,m,k,s,t,u,v,w,z,ans=inf; // n,m,k,s,t 题目给出,u,v,w 临时村边,z 临时变量,ans 计算答案用
int head[N],top; // 链式前向星用
int dist[N],vis[N]; // dij 用,分别记录起始顶点到各个顶点的距离,是否访问过
priority_queue<pair<int,int>> q; // dij 用,优先队列
void add(int u,int v,int w){
top++;
e[top].to=v;
e[top].val=w;
e[top].nxt=head[u];
head[u]=top;
}
void dijkstra(){ // dij 板子
for(int i=0,l=n*k+n; i<=l; i++) // 注意初始化的范围,是从 0 到第 k 层图的最后一个顶点
dist[i]=inf;
dist[s]=0;
q.push(make_pair(0,s));
while(!q.empty()){
u=q.top().second;
q.pop();
if(vis[u]) continue;
vis[u]=1;
for(int i=head[u]; i; i=e[i].nxt){
v=e[i].to;
if(dist[v]>dist[u]+e[i].val){
dist[v]=dist[u]+e[i].val;
q.push(make_pair(-dist[v],v));
}
}
}
}
int main(){
n=read(),m=read(),k=read(),s=read(),t=read();
while(m--){
u=read(),v=read(),w=read();
add(u,v,w);
add(v,u,w); // 原图建边
for(int i=1; i<=k; i++){ // 然后再建 1~k 层图的边
z=i*n; // 很多重复运算,所以用 z 代替。表示第 i 层图的起始下标
add(u+z,v+z,w);
add(v+z,u+z,w); // 复制上面的边到每层图中
add(u+z-n,v+z,0);
add(v+z-n,u+z,0); // 建立优惠边
}
}
dijkstra();
for(int i=0; i<=k; i++)
ans=min(ans,dist[t+n*i]);
// 由于最短路的终点可能是任意一层的终点
// 所以需要取所有终点的最小值
write(ans);
return 0;
}
\(n\) 倍经验
- P4822 [BJWC2012]冻结
- P2939 [USACO09FEB]Revamping Trails G
- P1948 [USACO08JAN]Telephone Lines S
- P3119 [USACO15JAN]Grass Cownoisseur G
按照个人认为的难度排序。前两道就是板子,后面两道就有点难度了。