Cheap Robot 題解

XuYueming發表於2024-08-18

前言

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

相關文章