[CSP-S 2022] 假期计划

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

先 BFS 求限制步数内可达,再为每个点保留前 4 个高分候选,枚举中间两个景点凑出四段行程。

OJ: luogu

题目 ID: P8817

难度:提高+/省选-

标签:图论BFS枚举贪心

创建: 2026-07-06 08:46

更新: 2026-10-07 12:15

形式化题目

给定一张 nn 个结点的无向图,结点 11 是家,结点 2∼n2\sim n 是景点,景点 ii 有分数 sis_i。

两个结点 x,yx,y 可转车 kk 次通达,指 xx 到 yy 的简单路径边数不超过 k+1k+1。

要求选四个互不相同的景点 A,B,C,DA,B,C,D,使得

1→A→B→C→D→1 1\to A\to B\to C\to D\to 1

这五段行程都可转车 kk 次通达,并最大化

sA+sB+sC+sD. s_A+s_B+s_C+s_D.

保证至少存在一组符合要求的行程。

样例 11 是一个 88 个结点的环,k=1k=1,即每段行程最多走 22 条边。

flowchart LR
  N1["1 家"] --- N2["2: 9"]
  N2 --- N3["3: 7"]
  N3 --- N4["4: 1"]
  N4 --- N5["5: 8"]
  N5 --- N6["6: 2"]
  N6 --- N7["7: 3"]
  N7 --- N8["8: 6"]
  N8 --- N1

最优行程是 2→3→5→72\to3\to5\to7,对应 9+7+8+3=279+7+8+3=27。

暴力解法

思路

先用 BFS 求出 can_reach[x][y],表示 xx 到 yy 是否可转车 kk 次通达。然后直接枚举四个互不相同的景点 A,B,C,DA,B,C,D,逐段检查

1→A, A→B, B→C, C→D, D→1 1\to A,\ A\to B,\ B\to C,\ C\to D,\ D\to 1

是否都可达,可行就更新分数和的最大值。

代码

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-10-01 22:21
 * update_at: 2026-10-01 22:21
 */
// brute.cpp:小数据暴力解,枚举 4 个不同景点并检查 5 段行程是否都能在限制内通达。
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;

const int MAXN = 25;   // 暴力只服务小数据,n 取到 20 左右

int n, m, k;
ll score[MAXN];                // score[i]:景点 i 的分数
vector<int> graph_edges[MAXN]; // graph_edges[u]:与 u 有直达线路的点
bool can_reach[MAXN][MAXN];    // can_reach[x][y]:x 到 y 是否最多转车 k 次
int dist[MAXN];                // BFS 距离数组

// 从 start 出发做 BFS,标记 k+1 条边以内可达的点。
void bfs(int start) {
    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();

        can_reach[start][u] = true;
        if (dist[u] == k + 1) {
            continue;
        }

        int cnt = graph_edges[u].size();
        for (int i = 0; i < cnt; 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);
    }

    // 依次枚举 4 个互不相同的景点,再逐段检查 1 -> A -> B -> C -> D -> 1。
    ll 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;
}

复杂度

BFS 部分是 O(n(n+m))O(n(n+m)),枚举四个景点是 O(n4)O(n^4),总时间复杂度 O(n4+n(n+m))O(n^4+n(n+m)),空间 O(n2)O(n^2)。

瓶颈

当 n=2500n=2500 时 n4n^4 达到 4×10134\times10^{13},绝对跑不动。暴力里真正有价值的只有一件事:把“最多转车 kk 次”预处理成一个布尔可达矩阵。剩下的四重枚举必须消掉。

正解

思路

拆成“中间一段 + 两侧各选一个”

行程是

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

如果固定中间的 B→CB\to C,那么 AA 只需要满足 1→A→B1\to A\to B 都可达,DD 只需要满足 C→D→1C\to D\to 1 都可达,两边完全独立。

于是问题变成:对每个结点 xx,维护“能排在 xx 前面的高分候选”集合

best[x]={ p:p≠x, can_reach[1][p], can_reach[p][x] }. best[x]=\{\,p : p\neq x,\ can\_reach[1][p],\ can\_reach[p][x]\,\}.

因为图是无向的,can_reachcan\_reach 对称,所以 best[C]best[C] 同时就是“能排在 CC 后面、且能回到家”的候选集合,两份候选列表可以共用同一套数据结构。

为什么只留 4 个候选

枚举一对 B,CB,C 后,AA 要避开 {B,C,D}\{B,C,D\},DD 要避开 {A,B,C}\{A,B,C\}。设最优解是 (A\*,D\*)(A^\*,D^\*):

在 best[B]best[B] 中,凡是排在 A\*A^\* 前面的候选,分数都不低于 A\*A^\*。若它们都不是最优,就说明在配对 (⋅,D\*)(\cdot,D^\*) 下都被排除了,只能是等于 BB、CC 或 D\*D^\*——最多 33 个点。而 BB 本身已经不进候选表,所以真正会挤掉 A\*A^\* 的至多 22 个点,A\*A^\* 最坏排在第 33 位。

同理 D\*D^\* 最坏也排在第 33 位。因此每份候选表保留前 44 个就一定够用,枚举 4×4=164\times4=16 组组合即可得到最优。

样例中的候选表

下面列出样例 11 里每个结点的 best_node(括号内是分数),只保留分数最高的若干个。

xx best[x]best[x](按分数降序)
22 3 (7), 8 (6)3\,(7),\ 8\,(6)
33 2 (9)2\,(9)
44 2 (9), 3 (7)2\,(9),\ 3\,(7)
55 3 (7), 7 (3)3\,(7),\ 7\,(3)
66 8 (6), 7 (3)8\,(6),\ 7\,(3)
77 8 (6)8\,(6)
88 2 (9), 7 (3)2\,(9),\ 7\,(3)

表中可以看出 best[3]best[3] 只有一个候选 22,说明要经过 33 就只能从 22 过来。枚举到 B=3, C=5B=3,\ C=5 时,A=2A=2、DD 在 best[5]={3,7}best[5]=\{3,7\} 里排除掉与 B=3B=3 重合的 33,取 77,得到

s2+s3+s5+s7=9+7+8+3=27, s_2+s_3+s_5+s_7=9+7+8+3=27,

正是样例答案。

代码

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-10-01 22:21
 * update_at: 2026-10-01 22:21
 */
// main.cpp:先从每个点 BFS 求限制步数内的可达性,再为每个点保留高分候选,最后枚举中间两个景点。
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;

const int MAXN = 2505;   // n <= 2500

int n, m, k;
ll score[MAXN];                // score[i]:景点 i 的分数,可达 1e18,必须用 ll
vector<int> graph_edges[MAXN]; // graph_edges[u]:与 u 有直达线路的点
bool can_reach[MAXN][MAXN];    // can_reach[x][y]:x 到 y 是否最多转车 k 次(即不超过 k+1 条边)
int best_node[MAXN][5];        // best_node[x][1..4]:能排在 x 前面的分数最高的 4 个候选景点
int dist[MAXN];                // BFS 距离数组,放全局避免大数组爆栈

// 从 start 出发做 BFS,把 k+1 条边以内可达的点都标记为 can_reach[start][y]。
void bfs(int start) {
    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();

        can_reach[start][u] = true;
        if (dist[u] == k + 1) {
            continue;   // 转车 k 次最多走 k+1 条边,不能再往外扩展
        }

        int cnt = graph_edges[u].size();
        for (int i = 0; i < cnt; i++) {
            int v = graph_edges[u][i];
            if (dist[v] == -1) {
                dist[v] = dist[u] + 1;
                q.push(v);
            }
        }
    }
}

// 把 node 插入 best_node[x],按分数从高到低维护前 4 个,重复的景点不再加入。
void add_candidate(int x, int node) {
    for (int i = 1; i <= 4; i++) {
        if (best_node[x][i] == node) {
            return;
        }
    }

    for (int i = 1; i <= 4; i++) {
        if (best_node[x][i] == 0 || score[node] > score[best_node[x][i]]) {
            for (int j = 4; 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);
    }

    // best_node[x] 收的是“能排在 x 前面”的候选 a:满足 1 能到 a、a 能到 x。
    // 图是无向的,“x 后面”的候选条件形式相同,因此同一份列表可以两边复用。
    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);
            }
        }
    }

    ll answer = 0;
    for (int b = 2; b <= n; b++) {
        for (int c = 2; c <= n; c++) {
            if (b == c || !can_reach[b][c]) {
                continue;
            }

            // b 的候选取 A、c 的候选取 D,凑出四个互不相同的景点。
            // 每份候选最多被 3 个点排除,所以保留前 4 个一定能取到最优。
            for (int i = 1; i <= 4; i++) {
                int a = best_node[b][i];
                if (a == 0 || a == b || a == c) {
                    continue;
                }
                for (int j = 1; 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;
}

复杂度

  • 时间复杂度:O(n(n+m)+n2⋅16)O(n(n+m)+n^2\cdot 16)。每个点做一次 BFS 共 O(n(n+m))O(n(n+m)),候选收集和枚举 B,CB,C 都是 O(n2)O(n^2) 级别,只带一个很小的常数。
  • 空间复杂度:O(n2+n+m)O(n^2+n+m),主要来自可达矩阵。

总结

这题的关键拆法是不要直接枚举 A,B,C,DA,B,C,D,而是先固定中间一段 B→CB\to C:

  • BFS 把“最多转车 kk 次”变成布尔可达矩阵,负责判可行;
  • 每个结点只留前 44 个高分候选,把两侧选择从 O(n)O(n) 降到常数。

“前 4 个够用”这件事来自一个很短的排除论证:一对 B,CB,C 固定后,一个候选最多被 22 个点挤掉。这类“只留常数个最优候选”的剪枝是枚举题里把指数级压成多项式级的常用手法。

本文的 main.cpp 已用四重枚举的 brute.cpp 对拍 600600 组随机数据(含稠密图与稀疏图),两组题面样例均通过。