分层图
算法笔记:状态扩展最短路与分层图进阶
在传统图论中,最短路通常只维护到达点 u 的最小代价 dis[u]。但当题目引入了“限制条件”(如免费次数、奇偶性、速度、疲劳值)时,一个点就不再仅仅是一个点,而是一个多维状态。我们需要将 dis[u] 扩展为 dis[u][state],在状态空间中跑最短路。
1. 基础状态拆分:奇偶最短路 (BFS)
来源文件:滑板火箭.cpp
- 🏷️ 题面抽象:在一个网格图中,按特定的 8 个方向移动,避开障碍物到达终点
(n, m)。由于移动方式的步数(奇数步或偶数步)会对最终的计算产生不同影响,需要求出到达终点的特定奇偶步数下的最短距离。 - 💡 核心思路:
- 边权均为 1,所以用标准的 BFS 即可,无需 Dijkstra。
- 状态拆分:由于奇数步和偶数步的后续限制不同,将
dis[x][y]拆分为disJ[x][y](到达该点的奇数步最短路)和disO[x][y](到达该点的偶数步最短路)。 - 转移逻辑:如果当前步数
st加 1 是奇数,则用disO[x][y] + 1更新disJ[nx][ny];反之亦然。交替更新。 - 后处理:根据最终到达终点的
disJ和disO,结合题目给定的区间步长l和r进行分类讨论计算最终答案。
点击查看代码
struct Node {
int x, y, st;
};
void solve() {
int n, m;
cin >> n >> m;
int l, r;
cin >> l >> r;
vector<vector<char>> g(n + 1, vector<char>(m + 1));
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++) {
cin >> g[i][j];
}
vvi disJ(n + 1, vi(m + 1, 1e9));
vvi disO(n + 1, vi(m + 1, 1e9));
queue<Node> q;
q.push({1, 1, 0});
disO[1][1] = 0;
while (q.size()) {
auto [x, y, st] = q.front();
q.pop();
for (int i = 0; i < 8; i++) {
int nx = x + dx[i], ny = y + dy[i];
if (nx < 1 || nx > n || ny < 1 || ny > m) continue;
if (g[nx][ny] == '0') continue;
int nst = st + 1;
if (nst % 2) {
if (disJ[nx][ny] == 1e9) {
disJ[nx][ny] = disO[x][y] + 1;
q.push({nx, ny, nst});
}
} else {
if (disO[nx][ny] == 1e9) {
disO[nx][ny] = disJ[x][y] + 1;
q.push({nx, ny, nst});
}
}
}
}
if (disJ[n][m] == 1e9 && disO[n][m] == 1e9) {
cout << -1 << endl;
return;
}
if (l != r) {
int mn = min((disJ[n][m] + r - 1) / r, (disO[n][m] + r - 1) / r);
cout << mn << endl;
return;
}
if (l == r) {
if (l % 2) {
int ans1 = (disJ[n][m] + r - 1) / r;
if (ans1 % 2 == 0) ans1++;
int ans2 = (disO[n][m] + r - 1) / r;
if (ans2 % 2) ans2++;
cout << min(ans1, ans2) << endl;
} else {
if (disO[n][m] == 1e9)
cout << -1 << endl;
else
cout << (disO[n][m] + r - 1) / r << endl;
}
}
}
2. 经典分层图:免费乘坐 K 次航班
来源文件:P_4568_JLOI_2011_飞行路线.cpp
- 🏷️ 题面抽象:给定一张无向图,你可以选择最多
k条边,使这些边的权值变为 0(免费航班)。求起点到终点的最小花费。 - 💡 核心思路:分层图最短路的教科书级模板。
- 状态定义:
d[u][cnt]表示到达节点u,且已经使用了cnt次免费机会的最小花费。 - 状态转移:在遍历
u的邻居v时,存在两种决策:
- 不使用免费(同层转移):花费正常边权
w。若d[v][cnt] > d[u][cnt] + w,则更新并入队{v, d, cnt}。 - 使用免费(跨层转移):花费为 0,免费次数加 1。前提是
cnt < k。若d[v][cnt+1] > d[u][cnt],则更新并入队{v, d, cnt+1}。
- 答案获取:终点
ed可能并没有用完所有免费次数,因此答案是min(d[ed][0...k])。
点击查看代码
struct Node{
int x,w,cnt;
bool operator < (const Node &v) const {
return w > v.w;
}
};
struct node{
int x,w;
};
vector <node> g[N];
int d[N][15];
int vis[N][15];
//15 * m * n * logn
int st,ed;
//开二维 dis 表状态
void dij(){
memset(d,0x3f,sizeof d);
priority_queue<Node> q;
q.push({st,0,0});
d[st][0] = 0;
while(q.size()){
auto [u,_,cnt] = q.top();q.pop();
if(vis[u][cnt]) continue;
vis[u][cnt] = 1;
for(auto [v,w]:g[u]){
//选择用次数
if(cnt < k){
if(d[v][cnt+1] > d[u][cnt]){
d[v][cnt+1] = d[u][cnt];
if(!vis[v][cnt+1])
q.push({v,d[v][cnt+1],cnt+1});
}
}
if(d[v][cnt] > d[u][cnt] + w){
d[v][cnt] = d[u][cnt] + w;
if(!vis[v][cnt])
{
q.push({v,d[v][cnt],cnt});
}
}
}
}
int ans = 1e18;
for(int i=0;i<=k;i++)
ans = min(ans,d[ed][i]);
cout<<ans;
}
void solve() {
cin >> n >> m >> k;
cin >> st >> ed;
st++,ed++;
for(int i=1;i<=m;i++){
int u,v,w;
cin >> u >> v >> w;
u++,v++;
g[u].push_back({v,w});
g[v].push_back({u,w});
}
dij();
}
3. 条件触发型状态:下坡清零疲劳值
来源文件:B爬山.cpp
- 🏷️ 题面抽象:给定节点的海拔
h[i],移动有边权花费。同时移动会累积“疲劳值”:如果去海拔更高或平级的地方,疲劳值增加(高度差 + 当前疲劳值);如果去海拔更低的地方,疲劳值瞬间清零。要求任意时刻疲劳值不得超过H。求到达每个点的最小边权花费。 - 💡 核心思路:带条件触发的 2D Dijkstra。
- 状态定义:
d[u][j]表示到达节点u且当前疲劳值为j的最小花费。因为疲劳值有上限H且不大(题意暗示 100 左右),可以作为数组第二维。 - 分类转移:
- 下坡(清零):如果
h[u] > h[v],疲劳值变为 0。若d[v][0] > d[u][cnt] + w,更新d[v][0]并入队。 - 上坡/平地(累积):新疲劳值
nt = h[v] - h[u] + cnt。必须满足nt <= H才能走。若满足且可以松弛,则更新d[v][nt]并入队。
点击查看代码
struct Node{
int x,w,cnt;
bool operator < (const Node &v) const {
return w > v.w;
}
};
struct node{
int x,w;
};
vector <node> g[N];
int d[N][110]; //走到 i 疲劳值为 j
int vis[N][110];
int h[N];
//开二维 dis 表状态
void dij(){
memset(d,0x3f,sizeof d);
int st = 1;
priority_queue<Node> q;
q.push({st,0,0});
d[st][0] = 0;
while(q.size()){
auto [u,_,cnt] = q.top();q.pop();
if(vis[u][cnt]) continue;
vis[u][cnt] = 1;
for(auto [v,w]:g[u]){
//从u-v
if(h[u] > h[v]){
//清空 j
if(d[v][0] > d[u][cnt] + w){
d[v][0] = d[u][cnt] + w;
if(!vis[v][0])
q.push({v,d[v][0],0});
}
}
else{
if(h[v] - h[u] + cnt <= H){
int nt = h[v] - h[u] + cnt;
if(d[v][nt] > d[u][cnt] + w){
d[v][nt] = d[u][cnt] + w;
if(!vis[v][nt]){
q.push({v,d[v][nt],nt});
}
}
}
}
}
}
for(int i=2;i<=n;i++){
int ans = 1e18;
for(int j=0;j<=101;j++){
ans = min(ans,d[i][j]);
}
if(ans >= 1e18)
cout << -1 <<' ';
else
cout << ans << ' ';
}
// int ans = 1e18;
// for(int i=0;i<=k;i++)
// ans = min(ans,d[ed][i]);
// cout<<ans;
}
void solve() {
cin >> n >> m >> H;
for(int i=1;i<=n;i++)
cin >> h[i];
for(int i=1;i<=m;i++){
int u,v,w;
cin >> u >> v >> w;
if(u == v) continue;
g[u].push_back({v,w});
g[v].push_back({u,w});
}
dij();
}
4. 连续状态继承与路径打印:速度限制
来源文件:P_1266_速度限制.cpp
- 🏷️ 题面抽象:有向图,边有长度
l和速度限制v。通过一条边耗时l / v。特殊机制:如果一条边的限速为 0,则要求继承到达该起点时的速度继续行驶。求起点到终点的最短时间,并打印具体路径。 - 💡 核心思路:状态继承 + 递归打印路径。
- 状态定义:
d[u][v]表示到达节点u且到达速度为v的最短时间(注意类型是double)。最大速度为 500。 - 状态转移(速度继承):
- 如果邻接边限速
vel == 0,则用当前速度v计算时间,并以速度v抵达下一节点。 - 如果限速
vel > 0,则用新速度vel计算时间,并以速度vel抵达下一节点。
- 路径记录:额外开一个
dpnode dp[u][v]数组,每次发生成功松弛时,记录是从哪个节点、以哪个速度转移过来的,即dp[to][new_v] = {u, old_v}。 - 打印路径:写一个递归函数
out(x, v),利用dp数组从终点不断回溯找前驱,直到起点,然后在回溯出栈时打印节点编号。
点击查看代码
//记录路径的dij
struct Node{
int x,v,l;
};
const int N = 155;
vector<Node> g[N];//转移图
const int M = 550;
struct dpnode{
int x,v;
// int cnt;//次数
};
dpnode dp[N][M];//记录上一个状态
struct node{
int x;
double w;
int v;//上一个速度
bool operator < (const node &t) const {
return w > t.w;
}
};
void out(int x,int v){
if(x == 0) {
cout<<x<<' ';
return;
}
out(dp[x][v].x,dp[x][v].v);
cout<<x<<' ';
}
void dij(){
vector<vector<double>> d(n+1,vector<double>(550,1e18));
vector<vector<int>> vis(n+1,vector<int>(550,0));
priority_queue<node> q;
q.push({0,0.0,70});
d[0][70] = 0.0;
while(q.size()){
auto [u,w,v] = q.top();q.pop();
if (u == ed) {
out(u,v);
return;
}
if(vis[u][v]) continue;
vis[u][v] = 1;
for(auto [to,vel,len]:g[u]){
if(vel == 0){
if(d[to][v] > d[u][v] + ((double)len/(double)v)){
d[to][v] = d[u][v] + ((double)len/(double)v);
dp[to][v] = {u,v};
if(!vis[to][v])
q.push({to,d[to][v],v});
}
}
else if (d[to][vel] > d[u][v] + ((double)len/(double)vel)){
d[to][vel] = d[u][v] + ((double)len/(double)vel);
dp[to][vel] = {u,v};
if(!vis[to][vel])
q.push({to,d[to][vel],vel});
}
}
}
}
void solve() {
cin >> n >> m >> ed ;
for(int i=1;i<=m;i++){
int x,y,v,l;
cin >> x >> y >> v >> l;
if(x == y) continue;
g[x].push_back({y,v,l});
// g[y].push_back({x,v,l});
}
dij();
}
5. 极端数据的降维打击:分层图的“层序”拆解
来源文件:2025icpc网络赛一M.cpp
- 🏷️ 题面抽象:图存在极多的分层(层数高达
n),不仅有同层内的边,还有明确的跨层转移图g2。求从起点出发,在第 0 层到第n层每个状态下的路径相关结果。 - 💡 核心思路:解决优先队列爆炸问题(防 TLE/MLE)。
- 痛点:如果在有
N个点、N个层的图上直接跑一次全局二维 Dijkstra,优先队列里的状态数会极其庞大,排序带来的常数和log时间开销会直接爆炸。 - 降维优化:因为层与层之间的转移是单向、无后效性的(只能从
t-1层跳到t层,类似 DP),我们可以将全局 Dijkstra 拆解为n次独立的单层 Dijkstra。 - 执行流程:
- 手动跨层转移:在外层循环处理第
t层前,先遍历所有节点,根据跨层边g2,直接将上一层t-1的最短路结果手动推平到第t层的初始状态,并将这些有效起点压入队列。 - 单层 Dijkstra:只在当前第
t层内部跑常规的 Dijkstra(队列里只装当前层的节点,不跨层)。
- 这个优化将复杂度从庞大且臃肿的全局 PQ 调度,降解为了 $N$ 次小规模图的独立求解,是 ICPC 比赛中应对极端数据的经典常数/架构优化手段。
点击查看代码
// 需要注意的是,在本题中分层数量很多(n),如果不一层层转移而是直接一次性跑dijkstra,
// 那么优先队列排序所用时间会非常恐怖,因此我们需要每层手动转移。
//层数为n 手动转移的分层图
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
int n,m;
const int N = 5500;
struct Node{
int x,w,cnt;
bool operator < (const Node &v) const {
return w > v.w;
}
};
struct node{
int x,w;
};
vector <node> g[N];
int d[N][N];
int vis[N][N];
vector<int> g2[N];//转移图
//开二维 dis 表状态
void dij(int t){ //第几层
priority_queue<Node> q;
//第一层不操作
if(t==0){
q.push({1,0, 0});
d[1][0] = 0;
} //手动转移
//从t-1层转移到t层
else {
for(int u=1;u<=n;u++){
for(auto v : g2[u]){
if(d[v][t] > d[u][t-1]){
d[v][t] = d[u][t-1];
q.push({v,d[v][t],t});
}
}
}
}
while(q.size()){
auto [u,_,cnt] = q.top();q.pop();
if(vis[u][cnt]) continue;
vis[u][cnt] = 1;
for(auto [v,w]:g[u]){
if(d[v][cnt] > d[u][cnt] + w){
d[v][cnt] = d[u][cnt] + w;
if(!vis[v][cnt])
{
q.push({v,d[v][cnt],cnt});
}
}
}
}
}
void solve() {
cin >> n >> m ;
memset(d,0x3f,sizeof d);
for(int i=1;i<n;i++){
int u,v,w;
cin >> u >> v >> w;
g[u].push_back({v,w});
g[v].push_back({u,w});
}
for(int i=1;i<=m;i++){
int u,v;
cin >> u >> v;
g2[u].push_back(v);
g2[v].push_back(u);
}
//转移的时候需要要不然原来没有边的点没有更新全是无穷
for(int i=1;i<=n;i++)
g2[i].push_back(i);
for (int i = 0; i <= n; i ++ )
{
dij(i);
}
for (int i = 0; i <= n; i ++ )
{
int res = 0;
for (int j = 1; j <= n; j ++ )
{
res += d[j][i];
}
cout << res << endl;
}
}

浙公网安备 33010602011771号