[CSP-S 2022] 假期计划

GitHub跳转原题关系图返回列表

先 BFS 求限制步数内可达,再为每个中间点保留高分候选并枚举中间两个景点。

OJ: luogu

题目 ID: P8817

难度:提高+/省选-

标签:图论BFS枚举贪心

日期: 2026-07-06 08:46

题意

从家 1 出发,依次游玩 4 个互不相同的景点 A,B,C,D,最后回到家。每一段行程都要求最多转车 kk 次,也就是最多走 k+1k+1 条边。

每个景点有分数,要求最大化:

text
score[A] + score[B] + score[C] + score[D]

思路

小数据可以直接枚举 4 个景点,再检查 5 段路是否都能在限制内到达:

cpp
// brute.cpp:小数据暴力解,枚举 4 个不同景点并检查 5 段行程是否可达。
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 25;

int n, m, k;
long long score[MAXN];
vector<int> graph_edges[MAXN];
bool can_reach[MAXN][MAXN];

void bfs(int start) {
    int dist[MAXN];
    queue<int> q;
    for (int i = 1; i <= n; i++) {
        dist[i] = -1;
    }
    dist[start] = 0;
    q.push(start);
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        if (dist[u] > k + 1) {
            continue;
        }
        can_reach[start][u] = true;
        if (dist[u] == k + 1) {
            continue;
        }
        for (int i = 0; i < (int)graph_edges[u].size(); i++) {
            int v = graph_edges[u][i];
            if (dist[v] == -1) {
                dist[v] = dist[u] + 1;
                q.push(v);
            }
        }
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m >> k;
    for (int i = 2; i <= n; i++) {
        cin >> score[i];
    }
    for (int i = 1; i <= m; i++) {
        int u, v;
        cin >> u >> v;
        graph_edges[u].push_back(v);
        graph_edges[v].push_back(u);
    }

    for (int i = 1; i <= n; i++) {
        bfs(i);
    }

    long long answer = 0;
    for (int a = 2; a <= n; a++) {
        for (int b = 2; b <= n; b++) {
            for (int c = 2; c <= n; c++) {
                for (int d = 2; d <= n; d++) {
                    if (a == b || a == c || a == d || b == c || b == d || c == d) {
                        continue;
                    }
                    if (can_reach[1][a] && can_reach[a][b] && can_reach[b][c] &&
                        can_reach[c][d] && can_reach[d][1]) {
                        answer = max(answer, score[a] + score[b] + score[c] + score[d]);
                    }
                }
            }
        }
    }

    cout << answer << '\n';
    return 0;
}

暴力枚举是 O(n4)O(n^4),当 n=2500n=2500 时不可接受。但它提醒我们:真正需要判断的只是两点之间能否在 k+1k+1 条边内到达。

先从每个点做一次 BFS,得到 can_reach[x][y],表示 xxyy 是否满足单段行程限制。

路线是:

text
1 -> A -> B -> C -> D -> 1

如果我们枚举中间两个景点 B,C,就只剩下两边要选:

  • A 需要满足 1 -> A 可达,并且 A -> B 可达;
  • D 需要满足 C -> D 可达,并且 D -> 1 可达。

图是无向图,所以“能作为某个点 xx 左侧景点的候选”与“能作为 xx 右侧景点的候选”条件形式相同:候选点 pp 需要满足 can_reach[1][p]can_reach[p][x]

于是可以对每个点 xx 预处理分数最高的前 4 个候选景点。为什么保留 4 个就够?枚举 B,C 时,一个候选最多会因为等于 A/B/C/D 中已有的点而不能用;我们只需要避开常数个点,保留前 4 个足够找到最优可用候选。

最后枚举所有有 can_reach[B][C]B,C,在 best[B] 中选 A,在 best[C] 中选 D,检查四个景点互不相同并更新答案。

代码

cpp
// main.cpp:BFS 预处理可达性,再枚举中间两个景点 B、C。
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 2505;

int n, m, k;
long long score[MAXN];
vector<int> graph_edges[MAXN];
bool can_reach[MAXN][MAXN];
int best_node[MAXN][4]; // best_node[x] 保存能作为 x 前一个景点的高分候选

void bfs(int start) {
    static int dist[MAXN];
    queue<int> q;
    for (int i = 1; i <= n; i++) {
        dist[i] = -1;
    }
    dist[start] = 0;
    q.push(start);

    while (!q.empty()) {
        int u = q.front();
        q.pop();
        if (dist[u] > k + 1) {
            continue;
        }
        can_reach[start][u] = true;
        if (dist[u] == k + 1) {
            continue;
        }
        for (int i = 0; i < (int)graph_edges[u].size(); i++) {
            int v = graph_edges[u][i];
            if (dist[v] == -1) {
                dist[v] = dist[u] + 1;
                q.push(v);
            }
        }
    }
}

void add_candidate(int x, int node) {
    for (int i = 0; i < 4; i++) {
        if (best_node[x][i] == node) {
            return;
        }
    }
    for (int i = 0; i < 4; i++) {
        if (best_node[x][i] == 0 || score[node] > score[best_node[x][i]]) {
            for (int j = 3; j > i; j--) {
                best_node[x][j] = best_node[x][j - 1];
            }
            best_node[x][i] = node;
            return;
        }
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m >> k;
    for (int i = 2; i <= n; i++) {
        cin >> score[i];
    }
    for (int i = 1; i <= m; i++) {
        int u, v;
        cin >> u >> v;
        graph_edges[u].push_back(v);
        graph_edges[v].push_back(u);
    }

    for (int i = 1; i <= n; i++) {
        bfs(i);
    }

    for (int x = 2; x <= n; x++) {
        for (int a = 2; a <= n; a++) {
            if (a != x && can_reach[1][a] && can_reach[a][x]) {
                add_candidate(x, a);
            }
        }
    }

    long long answer = 0;
    for (int b = 2; b <= n; b++) {
        for (int c = 2; c <= n; c++) {
            if (b == c || !can_reach[b][c]) {
                continue;
            }
            for (int i = 0; i < 4; i++) {
                int a = best_node[b][i];
                if (a == 0 || a == b || a == c) {
                    continue;
                }
                for (int j = 0; j < 4; j++) {
                    int d = best_node[c][j];
                    if (d == 0 || d == a || d == b || d == c) {
                        continue;
                    }
                    answer = max(answer, score[a] + score[b] + score[c] + score[d]);
                }
            }
        }
    }

    cout << answer << '\n';
    return 0;
}

复杂度

从每个点 BFS 一次,复杂度为 O(n(n+m))O(n(n+m))。预处理候选和枚举 B,C 都是 O(n2)O(n^2) 级别,只带一个很小的常数。

空间复杂度为 O(n2+n+m)O(n^2 + n + m),主要来自可达矩阵。

总结

本题的关键拆法是:不要直接枚举 A,B,C,D 四个景点,而是先固定中间连接 B -> C,再从两侧各取一个高分候选。

BFS 负责把“最多转车 kk 次”变成布尔可达矩阵;候选数组负责把两侧选择从 O(n)O(n) 降到常数。