前言
題目連結: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\)。吃一塹,長一智,也算是學到了新的套路。