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\)。吃一堑,长一智,也算是学到了新的套路。

posted @ 2024-08-18 18:46  XuYueming  阅读(9)  评论(0编辑  收藏  举报