先求从 1 号景点出发能到达的全部景点;在这些点上按边的较低端高度降序、边长升序做 Kruskal,可以在保证可达景点数最大的前提下,把总滑行距离压到最小。
OJ: luogu
题目 ID: P2573
难度:省选/NOI-
标签:图论最小生成树思维推导
日期: 2026-06-20 01:32
题意
有 n 个景点和 m 条轨道,每个景点有高度。
如果景点 u 和 v 之间有轨道,并且 h[u] >= h[v],那么就能从 u 滑到 v。
你从 1 号景点出发。每次滑到一个新景点后,可以免费吃时间胶囊,瞬间回到任意一个之前经过的景点。
要求:
- 先让经过的景点数尽量多
- 在这个前提下,让总滑行距离最小
输出“最多能经过多少个景点”和“此时最短的总距离”。
思路
先看一个小数据暴力:
// brute.cpp:小数据集合 DP,直接按“已访问景点集合”转移。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 12;
const long long INF = (1LL << 60);
int n, m;
int h[MAXN];
vector<pair<int, int>> g[MAXN];
long long dp[1 << MAXN];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> h[i];
g[i].clear();
}
for (int i = 1; i <= m; i++) {
int u, v, w;
cin >> u >> v >> w;
if (h[u] >= h[v]) {
g[u].push_back({v, w});
}
if (h[v] >= h[u]) {
g[v].push_back({u, w});
}
}
int max_mask = 1 << n;
for (int i = 0; i < max_mask; i++) {
dp[i] = INF;
}
dp[1 << 0] = 0;
int best_cnt = 1;
long long best_sum = 0;
for (int mask = 0; mask < max_mask; mask++) {
if (dp[mask] == INF) {
continue;
}
int cnt = __builtin_popcount((unsigned)mask);
if (cnt > best_cnt || (cnt == best_cnt && dp[mask] < best_sum)) {
best_cnt = cnt;
best_sum = dp[mask];
}
for (int u = 1; u <= n; u++) {
if (((mask >> (u - 1)) & 1) == 0) {
continue;
}
for (pair<int, int> e : g[u]) {
int v = e.first;
int w = e.second;
if ((mask >> (v - 1)) & 1) {
continue;
}
int next_mask = mask | (1 << (v - 1));
dp[next_mask] = min(dp[next_mask], dp[mask] + w);
}
}
}
cout << best_cnt << ' ' << best_sum << '\n';
return 0;
}暴力把“已经访问了哪些景点”做成集合 DP:
- 从集合里的任意一个景点出发
- 沿一条允许方向的边去访问一个新景点
- 代价加上这条边长
这个模型完全贴题,但 n=10^5 时当然不可能做集合状态。
关键观察分两步。
第一步:最多能经过多少个景点,其实就是从 1 号点在有向图里能到达的点数。
因为有时间胶囊存在,一旦某个点被到达,它以后就一直可以作为新的出发点。 所以只要一个点在有向可达范围里,就总能在某个时刻被访问到。
第二步:在这些可达点里,如何让总代价最小?
假设已经知道哪些点可达。把所有这些点单独拿出来看。
如果一条边连接 u,v,那么真正有用的信息是它能把“较高处的一部分点”和“较低处的一部分点”连接起来。
设这条边两端高度较小者为 low = min(h[u], h[v])。
要想访问所有高度不小于 low 的可达点,这条边是否应当被选,只取决于:
- 它能不能把当前两个连通块接起来
- 在同样能接起来的边里,它是不是更短
于是可以把所有可达点之间的边按下面顺序排序:
min(h[u],h[v])从大到小- 边长 从小到大
然后做一遍 Kruskal。
直观理解:
- 我们先处理“较高层”的连通需求
- 再逐层往更低处放开
- 在同一层里,优先选更短的边
这样选出来的树,既不会减少可达景点数量,又能把总代价压到最小。
代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005;
const int MAXM = 1000005;
struct Edge {
int u, v, w;
} edges[MAXM];
struct SortEdge {
int u, v, w, low_h;
bool operator<(const SortEdge &other) const {
if (low_h != other.low_h) {
return low_h > other.low_h;
}
return w < other.w;
}
};
int n, m;
int h[MAXN];
vector<pair<int, int>> g[MAXN];
bool reachable[MAXN];
int fa[MAXN];
void dfs(int start) {
stack<int> st;
st.push(start);
reachable[start] = true;
while (!st.empty()) {
int u = st.top();
st.pop();
for (pair<int, int> e : g[u]) {
int v = e.first;
if (reachable[v]) {
continue;
}
reachable[v] = true;
st.push(v);
}
}
}
void init_dsu() {
for (int i = 1; i <= n; i++) {
fa[i] = i;
}
}
int find_root(int x) {
if (fa[x] == x) {
return x;
}
fa[x] = find_root(fa[x]);
return fa[x];
}
bool unite(int x, int y) {
x = find_root(x);
y = find_root(y);
if (x == y) {
return false;
}
fa[x] = y;
return true;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> h[i];
}
vector<SortEdge> cand;
for (int i = 1; i <= m; i++) {
cin >> edges[i].u >> edges[i].v >> edges[i].w;
int u = edges[i].u;
int v = edges[i].v;
int w = edges[i].w;
if (h[u] >= h[v]) {
g[u].push_back({v, w});
}
if (h[v] >= h[u]) {
g[v].push_back({u, w});
}
}
dfs(1);
int reachable_cnt = 0;
for (int i = 1; i <= n; i++) {
if (reachable[i]) {
reachable_cnt++;
}
}
for (int i = 1; i <= m; i++) {
int u = edges[i].u;
int v = edges[i].v;
int w = edges[i].w;
if (!reachable[u] || !reachable[v]) {
continue;
}
cand.push_back({u, v, w, min(h[u], h[v])});
}
sort(cand.begin(), cand.end());
init_dsu();
long long answer = 0;
int used = 0;
for (SortEdge e : cand) {
if (!unite(e.u, e.v)) {
continue;
}
answer += e.w;
used++;
if (used == reachable_cnt - 1) {
break;
}
}
cout << reachable_cnt << ' ' << answer << '\n';
return 0;
}复杂度
设点数为 n,边数为 m。
- 先做一次从
1出发的可达性搜索: - 再把可达边排序并做 Kruskal:
总时间复杂度
总结
这题表面像“带回溯的搜索”,但时间胶囊其实把问题变成了“访问集合不断扩张”。先求出从 1 可达的最大景点集合,再在这个集合上做一棵特殊顺序的最小生成树,就是整题的核心。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
