分层图(一)

阅读前提示:
点进来了就往下看呗,其实前面的概念不重要,强烈建议从例题开始看,也可以直接看代码和注释哒!
请注意:本文需要前置知识:Dijkstra单源最短路算法

C++分层图的概念与应用

分层图是一种在图论算法中常用的优化技巧,特别适用于处理带有额外约束条件的最短路问题。它的核心思想是通过构建多层图,将状态信息(如剩余可使用次数、当前费用等)融入图的结构中,从而将复杂问题转化为标准的最短路径问题。

分层图在信息竞赛中的重要性

在算法竞赛中,许多题目会涉及带有特殊限制的最短路问题,例如“允许免费通过k条边”或“在特定条件下切换路径”。传统的最短路算法(如Dijkstra或SPFA)难以直接处理这类情况,而分层图提供了一种清晰的建模方式,能够高效地结合动态规划的思想,将状态转移转化为图的边和节点。

C++实现分层图的优势

C++以其高效的执行速度和灵活的数据结构支持,成为实现分层图的首选语言。STL中的priority_queuevector等容器可以方便地管理多层图的节点和边,而模板元编程和泛型特性允许代码在保持高性能的同时具备可扩展性。分层图的实现通常结合邻接表或链式前向星存储,以确保算法的时间复杂度可控。

引言总结

分层图不仅是解决复杂最短路问题的强大工具,也是信息学竞赛选手需要掌握的高级技巧之一。通过合理设计层间转移和状态管理,许多看似困难的问题可以被高效解决。本文将通过具体例题和C++代码示例,详细介绍分层图的构建方法及其典型应用场景。我们从一道模版题Luogu模版题P4822开始吧。

题目解析

P4822 [BJWC2012] 冻结

题意

一共有 K K K 次机会使时间减半,且满足约束条件:

  1. 在一条道路上最多只能一次时间减半。
  2. 使用一次时间减半只在该道路有效。
  3. 不必使用完 k k k 次机会。

求:使用不超过 K K K 张时间减速,从城市 1 1 1 到城市 N N N 最少需要多长时间。

分析

很明显,把时间看为边权,我们得到一张图,现在要最小化时间,那么就要求最短路。
但是,如果直接跑Dijkstra并在过程中把堆顶的一些边减半行吗?我们可以举些栗子,很容易发现,这种贪心不保证全局最优。
那么现在就是分层图大开杀戒的时候了。

具体实现

我去各方搜刮了一些资料,但是实在太理论了。反正我不想看,那么我们结合模版题和代码来看看吧。
先说一下关于空间的问题,避免RE哦!

空间

因为你要有k个决策,那么你就要开k+1倍的空间,每一层空间维护一个决策。
对于第一层的图,还是正常地直接建图,我使用的是链式前向星,当然使用邻接矩阵也是可以的。对于第二层及以上的图,就去把这一层和下一层之间连一条有向边,表示你这一次的决策。

因此,对于一个含n个点,k个决策的分层图,我们需要开至少 n + k ∗ n n+k * n n+kn 的空间,即 n + k ∗ n n+k * n n+kn 个点

关于例题中的存边过程,也是这样,我们先在第一层存正常的边,在后面的层去连单向边,边权就是原来长度的一半了。

现在放代码,可以看着注释再理解下:

#include<bits/stdc++.h>
#define endl '\n'
using namespace std;

const int N = 1e4+50;
int n, m, k, cnt;
int head[N*55]; // 多层图,存图要开n+n*k,其中k为决策层数,n为原图的点数 

int dis[N*55];
bool vis[N*55];

// 链式前向星存图
struct edge { int to, dis, next; } e[N*55];

// 加边函数
inline void add(int u, int v, int d)
{
    cnt++;
    e[cnt].dis = d;
    e[cnt].to = v;
    e[cnt].next = head[u];
    head[u] = cnt;
}

struct node
{
    int dis;
    int pos;
    bool operator < (const node &x)const { return x.dis < dis; }
};

priority_queue <node> q;

// 单源最短路
void dijkstra(int s)
{
    dis[s] = 0;
    q.push((node){0, s});
    while(!q.empty())
    {
        node tmp = q.top();
        q.pop();
        int x = tmp.pos, d = tmp.dis;
        if(vis[x]) continue;
        vis[x] = 1;
        for (int i = head[x]; i; i = e[i].next)
        {
            int y = e[i].to;
            if (dis[y] > dis[x] + e[i].dis)
            {
                dis[y] = dis[x] + e[i].dis;
                if(!vis[y])
                    q.push(( node ){dis[y], y});
            }
        }
    }
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
	
    cin >> n >> m >> k;

    // 初始化
    for(int i = 1; i <= n*(k+1); ++i)
        dis[i] = INT_MAX;

	for(register int i = 1; i <= m; i++)
    {
		int a, b, xx;
		cin >> a >> b >> xx;
        // 存双向边
		add(a, b, xx);
		add(b, a, xx); 
		for(register int j = 1; j <= k; j++)
        {
            //在每一层中存正常边
			add(a+j*n, b+j*n, xx);
			add(b+j*n, a+j*n, xx); 
            //在下一层连一条单向边,表示决策,在这题中就代表使用时间减缓魔法
			add(a+(j-1)*n, b+j*n, xx/2);
			add(b+(j-1)*n, a+j*n, xx/2);
		}  
	}

    // 终点单独连边,因为k+1层的终点是n+k*n,所以这里要单独加边
	for(register int i = 1; i <= k; i++)
        add(n+(i-1)*n, n+i*n, 0);

    // 跑单源最短路
	dijkstra(1);

	cout << dis[n+k*n] << endl; // 输出第k+1层的dis,这才是最后的答案 
	return 0;
}

至此,第一篇就结束了,不知道我什么时候会更新

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值