分层图

算法笔记:状态扩展最短路与分层图进阶

在传统图论中,最短路通常只维护到达点 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];反之亦然。交替更新。
  • 后处理:根据最终到达终点的 disJdisO,结合题目给定的区间步长 lr 进行分类讨论计算最终答案。
点击查看代码
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 时,存在两种决策:
  1. 不使用免费(同层转移):花费正常边权 w。若 d[v][cnt] > d[u][cnt] + w,则更新并入队 {v, d, cnt}
  2. 使用免费(跨层转移):花费为 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 左右),可以作为数组第二维。
  • 分类转移
  1. 下坡(清零):如果 h[u] > h[v],疲劳值变为 0。若 d[v][0] > d[u][cnt] + w,更新 d[v][0] 并入队。
  2. 上坡/平地(累积):新疲劳值 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。
  • 状态转移(速度继承)
  1. 如果邻接边限速 vel == 0,则用当前速度 v 计算时间,并以速度 v 抵达下一节点。
  2. 如果限速 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
  • 执行流程
  1. 手动跨层转移:在外层循环处理第 t 层前,先遍历所有节点,根据跨层边 g2,直接将上一层 t-1 的最短路结果手动推平到第 t 层的初始状态,并将这些有效起点压入队列。
  2. 单层 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;
	}


}

posted @ 2026-05-16 19:39  r_123  阅读(18)  评论(0)    收藏  举报