洛谷:P5304 [GXOI/GZOI2019]旅行者(二进制暴力枚举 / 建反图染色法)

这篇博客主要探讨了洛谷省选中关于旅行者问题的解决方案,涉及多源汇最短路问题。文章介绍了两种方法:二进制暴力枚举和建反图染色法。对于数据范围较大的情况,作者指出可以使用Spfa或堆优化Dijkstra,并详细解释了如何通过二进制枚举找到两座城市间最短路径的最小值。此外,还提出了染色法,通过在正反图上分别跑Dijkstra来优化计算。最后给出了完整的C++代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

洛谷省选:旅行者

省选大爹%%%
在这里插入图片描述
一看题,数据范围显然限制了只能用 Spfa(祈祷不卡) 或者 堆优化Dijkstra 跑最短路。

但题目要求的却是 k 座城市两两之间最短路的最小值一眼多源汇最短路

怎么翻过这数据的大山呢?

二进制暴力跑最短路法(根据性质):

在这里插入图片描述
可以看到时间限制5s,说明大常数有所允许。

我们每次枚举 n 的二进制位,枚举 logn 次,比如当前枚举到第 i 位,把 k 座城市的编号此处二进制位与 n 相同的放到一个集合(就是同 0 或 同 1 放一起),不同的放到另一个集合。

放到一个集合的操作可以看成:

  • 建立一个起点 连单向边到第一个集合中的任意点,边权为 0;
  • 建立一个终点,第二个集合中任意点连向终点一条单向边,权值也为 0 。
  • 这样每跑完一次 Dijkstra,最短路就等于 dist[终点]

————————————————————————————————————————————

那为什么这样枚举是对的呢?

  • 因为答案中的两两距离最小城市,必定编号不相同,看作 i 与 j**
  • i != j 当且仅当 i 和 j 至少有一位二进制位不同。所以这样枚举必定能让最终答案中,距离最小的两座城市有一次在不同的集合中

枚举不同的二进制位,每次把 k 个点分成两个集合跑 Dijkstra

l o g 2 k log_2k log2k次最短路取min即可

时间复杂度: O ( T n l o g n l o g k ) O(Tnlognlogk) O(Tnlognlogk)

勉勉强强跑 5 秒,可以用快读、O2降一下常数

————————————————————————————————————————

建反图染色法(正解):

普通的最短路维护的是 dist[N] —— 某点到任意点的最短路,这里我们转化一下定义:

  • 维护一个 dis[N] —— 集合到任意点的最短路
  • dis[i] 就代表:从集合走到 i 点的最短路径。维护时相当于从集合中任意点到 i 的距离中维护一个最小值,记录在 dis[i] 里
  • 所以 dis[i] 就舍去了无用的信息,没有记录每座城市离 i 的距离,而是取其中最小的一个

这个集合显然是 K 座城市。

我们先在正向图上跑一遍Djikstra,维护一个 dis[0][N];再在反向图上在跑一次,维护一个 dis[1][N]

一定要来回跑两次是因为:如果原图中有环,dis[i] 有可能信息会被覆盖

最后只需要枚举每一条边,对于边 {a,b,w}

如果 pon[0][a] != pon[1][b]
pon数组记录从集合中的哪座城市走过来(自然要选择最近的),不是简单的两点不同,应该是离他们最近的一座集合中的城市的编号要不同(这个编号可以在求最短路时随时记录)不然就是自环了,题意也要求两两间

维护一个 mi 即可:

 mi = min(mi, dis[0][a] + dis[1][b] + w);

——————————————————————————————————————

可以证明这样跑最短路涵盖最优解,最优解可以被枚举到。

以上就是思路(说的比较通俗,反转了是不会证明

代码加注释如下(染色法):

#include<bits/stdc++.h>
#include<unordered_set>
#include<unordered_map>
#define _CRT_SECURE_NO_WARNING
#define mem(a,b) memset(a,b,sizeof a)
#define cinios (ios::sync_with_stdio(false),cin.tie(0),cout.tie(0))
#define sca scanf
#define pri printf
#define ul u << 1
#define ur u << 1 | 1
//#pragma GCC optimize(2)
//[博客地址](https://blog.youkuaiyun.com/weixin_51797626?t=1) 
using namespace std;

typedef long long ll;
typedef pair<ll, int> PII;

const int N = 100010, M = 1000010, MM = 3000010;
int INF = 0x3f3f3f3f, mod = 100003;
ll LNF = 0x3f3f3f3f3f3f3f3f;
int n, m, k, T, S, D;
int h[N], rh[N], ne[M], e[M], w[M], idx;
ll dist[N], dis[2][N], pon[2][N];
int city[N];
bool st[N];
struct edge
{
    int l, r, w;
}ed[N * 5];

void add(int* h, int a, int b, int x) {
    e[idx] = b, ne[idx] = h[a], w[idx] = x, h[a] = idx++;
}

void dj(int* h, int f) { 
    mem(st, 0);
    for (int i = 1; i <= n; i++)dis[f][i] = LNF, pon[f][i] = 0;
    //初始化极值

    priority_queue<PII, vector<PII>, greater<PII>> q;
    for (int i = 0; i < k; i++) {
        int c = city[i];//起点为集合,所以集合中全部点都放入堆中
        dis[f][c] = 0;//记录距离
        pon[f][c] = c;//记录编号
        q.push({ 0,c });
    }

    while (q.size())
    {
        PII t = q.top();
        q.pop();

        int ver = t.second;
        if (st[ver])continue;
        st[ver] = true;

        for (int i = h[ver]; ~i; i = ne[i]) {
            int j = e[i];

            if (dis[f][j] > dis[f][ver] + w[i]) { //一旦有更小的距离
                dis[f][j] = dis[f][ver] + w[i];
                pon[f][j] = pon[f][ver];//更新距离,同时更新编号
                q.push({ dis[f][j],j });
            }
        }
    }
}

int main() {
    cinios;

    cin >> T;
    while (T--)
    {
        mem(h, -1), mem(rh, -1);
        idx = 0;//多组数据别忘了初始化

        cin >> n >> m >> k;
        for (int i = 0; i < m; i++)
        {
            int a, b, c;
            cin >> a >> b >> c;
            ed[i] = { a,b,c };//把边记录下来
            add(h, a, b, c), add(rh, b, a, c);//正反图
        }

        for (int i = 0; i < k; i++)
            cin >> city[i];//记录集合k

        dj(h, 0);
        dj(rh, 1);//正反跑一遍

        ll mi = LNF;
        for (int i = 0; i < m; i++) {
            int a = ed[i].l, b = ed[i].r, w = ed[i].w;
            if (pon[0][a] == pon[1][b])continue;//自环不考虑
            mi = min(mi, dis[0][a] + dis[1][b] + w);
            //集合跑到 a 的最短路 + 集合跑到 b 的最短路 + 当前边权
        }

        cout << mi << '\n';
    }
    return 0;
}
/*

*/
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值