Cheap Robot 题解
前言
题目链接:Codeforces;洛谷。
一道初看无从下手的题,转化后成了板子的好题。
题意简述
\(n\) 个结点的无向带权图上,一个机器人在游走,它有一个容量为 \(c\) 的电池,即任何时刻电量 \(x \in [0, c]\)。经过边权为 \(w\) 的边会消耗 \(w\) 的电量。\(1 \ldots k\) 为充电中心,在充电中心机器人能将电充满。
有 \(q\) 次询问,问从 \(u\) 至 \(v\) 电池容量至少为多少。询问独立。\(u\) 和 \(v\) 均为充电中心。
可以强制在线。
\(1 \leq k \leq n \leq 10^5\),\(m, q \leq 3 \times 10^5\)。
题目分析
首先,发现对我们处理问题有关的点均为充电中心。考虑简化图,将充电中心看做关键点。非关键点上的游走的十分冗余的,可以预处理出全源最短路。那么在简化图的关键点之间连一条边,边权就是原图之间的最短路。那么,我们从一个关键点走到另一个关键点消耗的电量就处理出来了。这时,询问时,直接查询简化图中两点之间所有路径最大值的最小值是多少即可。这十分套路,在线 Kruskal 重构树、最小生成树上求树链最值,离线并查集启发式合并。
这么做的时间复杂度差不多是:\(\Theta(km \log m + k^2 \log k + q \log k)\) 的,很劣。暴力我都没想到。
发现“简化图”是一张完全图,并且点数 \(k\) 竟然和 \(n\) 同阶?这简化了根简化了一样。
套路地,宏观上发现有大量重复计算,但是不好优化,那么从微观上来考虑。
假设我们通过某种途径到了点 \(u\),通过一条边权为 \(w\) 的边走到 \(v\),它的电量有什么要求。不妨假设其走到 \(u\) 的剩余电量为 \(x\)。设 \(dis_u\) 表示 \(u\) 离最近的充电站的距离,此时,\(x\) 是无论如何也不会超过 \(c - dis_u\),因为电量最多就是在最近充电站充满再走过来的。并且,我们要求时刻 \(x \geq dis_u\),以保证它能够走到最近的充电站,因为如果走不到的话,那么终点肯定也走不到了。那么我们在 \(u\) 时,电量支持走到最近充电站再走回来,剩余电量是 \(c - dis_u\)。经过这条边后,在 \(v\) 时电量剩余 \(c - dis_u - w\)。不要忘了我们要时刻保证能走到最近的充电站,即 \(c - dis_u - w \geq dis_v\)。所以,\(c \geq dis_u + w + dis_v\)。只要经过这条边,那么电量就必须满足这个下界。并且,不存在我们能用更少的电量通过这条边。
所以,不妨把 \((u, v, w)\) 的边权重新设为 \(w + dis_u + dis_v\),问题是无向图中两点间边权最大值最小,和暴力一样的套路。
至于 \(dis\) 的处理,很 naive,跑多源最短路就行了,不知道别的题解为什么要费口舌解释这个的实现。
使用离线并查集,时间复杂度应该是:\(\Theta((n + m) \log m + (m + q)(\log q + \alpha(n)))\)。
代码
#include <cstdio>
#include <iostream>
#include <vector>
#include <algorithm>
#include <queue>
#include <cstring>
using namespace std;
const int MAX = 1 << 26;
char buf[MAX], *p = buf;
#define getchar() *p++
#define isdigit(x) ('0' <= x && x <= '9')
inline void read(int &x) {
x = 0; char ch = 0;
for (; !isdigit(ch); ch = getchar());
for (; isdigit(ch); x = (x << 3) + (x << 1) + (ch ^ 48), ch = getchar());
}
const int N = 100010, M = 300010;
using ll = long long;
int n, m, k, q;
template <typename T>
using minHeap = priority_queue<T, vector<T>, greater<T>>;
struct Graph {
struct node {
int to, nxt, len;
} edge[M << 1];
int tot = 1, head[N];
void add(int u, int v, int w) {
edge[++tot] = {v, head[u], w};
head[u] = tot;
}
node & operator [] (const int x) {
return edge[x];
}
} xym;
long long dis[N];
struct Edge {
int u, v;
long long w;
bool operator < (const Edge & o) const {
return w < o.w;
}
};
vector<Edge> edge;
struct Question {
int u, v, idx;
};
vector<Question> qry[N];
long long ans[M];
int fa[N];
int get(int x) {
return fa[x] == x ? x : fa[x] = get(fa[x]);
}
signed main() {
fread(buf, 1, MAX, stdin);
read(n), read(m), read(k), read(q);
for (int i = 1, u, v, w; i <= m; ++i) {
read(u), read(v), read(w);
xym.add(u, v, w), xym.add(v, u, w);
edge.push_back({u, v, w});
}
memset(dis, 0x7f, sizeof (long long) * (n + 1));
minHeap<pair<long long, int>> Q;
for (int i = 1; i <= k; ++i) Q.push({dis[i] = 0, i});
while (!Q.empty()) {
long long ndis = Q.top().first;
int now = Q.top().second;
Q.pop();
if (dis[now] < ndis) continue;
for (int i = xym.head[now]; i; i = xym[i].nxt) {
int to = xym[i].to;
if (dis[to] > dis[now] + xym[i].len) {
dis[to] = dis[now] + xym[i].len;
Q.push({dis[to], to});
}
}
}
for (auto &e: edge) e.w += dis[e.u] + dis[e.v];
sort(edge.begin(), edge.end());
for (int i = 1; i <= n; ++i) fa[i] = i;
for (int i = 1, u, v; i <= q; ++i) {
read(u), read(v);
if (u == v) continue;
qry[u].push_back({u, v, i});
qry[v].push_back({u, v, i});
}
for (auto &e: edge) {
int u = e.u, v = e.v;
long long d = e.w;
int fu = get(u), fv = get(v);
if (fu == fv) continue;
if (qry[fu].size() > qry[fv].size())
swap(u, v), swap(fu, fv);
fa[fu] = fv;
for (auto &q: qry[fu]) {
int a = q.u, b = q.v;
if (get(a) == get(b)) {
if (!ans[q.idx]) ans[q.idx] = d;
} else {
qry[fv].emplace_back(move(q));
}
}
qry[fu].clear();
}
for (int i = 1; i <= q; ++i) printf("%lld\n", ans[i]);
return 0;
}
后记
这个套路在这一题是有的,两者的共性为:图上有 \(k\) 个关键点,想要对关键点建完全图,转化为原图上的边权加上 \(dis_u + dis_v\)。吃一堑,长一智,也算是学到了新的套路。
本文作者:XuYueming,转载请注明原文链接:https://www.cnblogs.com/XuYueming/p/18350902。
若未作特殊说明,本作品采用 知识共享署名-非商业性使用 4.0 国际许可协议 进行许可。