[KMOI R1] 军事行动

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

先用 BFS 求出任意两座城市之间的最短骑士步数,再把“从已占领城市攻占一座新城市”的代价视作边权,整道题就转化成一棵最小生成树。

OJ: luogu

题目 ID: P9709

难度:提高+/省选-

标签:图论最小生成树bfs

日期: 2026-06-20 01:11

题意

n 座城市,城市 1 一开始就已经被占领。

之后每次只能做一件事:

  • 从某个已经占领的城市派出一支舰队去攻打一座还没占领的城市

舰队在棋盘上按“日”移动,移动方式和国际象棋里的马一样:

  • (+/-1, +/-2)
  • (+/-2, +/-1)

并且坐标必须始终留在 1..m 的方形区域内。

舰队到达目标城市后,要到第二天这座城市才算被攻占。

问最少多少天能攻占所有城市。

样例一过程表

这张表展示样例一中唯一一支舰队的行动过程:

天数结束后 状态
第 1 天 舰队从 (1,1) 走到 (3,2)
第 2 天 舰队从 (3,2) 走到 (1,3)
第 3 天 城市 2 被攻占

所以答案是 3

思路

先看一个只适合很小数据的暴力:

cpp
// brute.cpp:小图状压 DP,直接按“占领集合”转移。
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 10;
const int MAXM = 20;
const int INF = 1e9;

int n, board_size;
int x[MAXN], y[MAXN];
int board_dist[MAXM][MAXM];
int city_dist[MAXN][MAXN];
int dp[1 << MAXN];
int dx[8] = {-1, 1, 2, -2, -1, 1, 2, -2};
int dy[8] = {2, 2, 1, 1, -2, -2, -1, -1};

void bfs_from(int start_id) {
    for (int i = 1; i <= board_size; i++) {
        for (int j = 1; j <= board_size; j++) {
            board_dist[i][j] = -1;
        }
    }

    queue<pair<int, int>> q;
    q.push({x[start_id], y[start_id]});
    board_dist[x[start_id]][y[start_id]] = 0;

    while (!q.empty()) {
        pair<int, int> cur = q.front();
        q.pop();

        int cx = cur.first;
        int cy = cur.second;

        for (int i = 0; i < 8; i++) {
            int nx = cx + dx[i];
            int ny = cy + dy[i];

            if (nx <= 0 || ny <= 0 || nx > board_size || ny > board_size) {
                continue;
            }
            if (board_dist[nx][ny] != -1) {
                continue;
            }

            board_dist[nx][ny] = board_dist[cx][cy] + 1;
            q.push({nx, ny});
        }
    }

    for (int i = 1; i <= n; i++) {
        city_dist[start_id][i] = board_dist[x[i]][y[i]];
    }
}

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

    cin >> n >> board_size;
    for (int i = 1; i <= n; i++) {
        cin >> x[i] >> y[i];
    }

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

    int max_mask = 1 << n;
    for (int i = 0; i < max_mask; i++) {
        dp[i] = INF;
    }
    dp[1 << 0] = 0;

    for (int mask = 0; mask < max_mask; mask++) {
        if (dp[mask] == INF) {
            continue;
        }

        for (int j = 1; j <= n; j++) {
            if ((mask >> (j - 1)) & 1) {
                continue;
            }

            int best = INF;
            for (int i = 1; i <= n; i++) {
                if (((mask >> (i - 1)) & 1) == 0) {
                    continue;
                }
                if (city_dist[i][j] == -1) {
                    continue;
                }
                best = min(best, city_dist[i][j] + 1);
            }

            if (best == INF) {
                continue;
            }

            int next_mask = mask | (1 << (j - 1));
            dp[next_mask] = min(dp[next_mask], dp[mask] + best);
        }
    }

    if (dp[max_mask - 1] == INF) {
        cout << -1 << '\n';
    } else {
        cout << dp[max_mask - 1] << '\n';
    }

    return 0;
}

暴力直接做状压 DP:

  • mask 表示哪些城市已经被占领
  • 下一步可以从 mask 里的任意城市出发,去攻打一座还没占领的城市

这个模型非常贴题,但 n = 2000 时不可能再做状态压缩。

关键观察有两层。

第一层:如果一座新城市 v 是从已占领城市 u 出发攻占的,那么这次操作的代价只取决于两城之间的最短骑士步数:

  • 走到 vdist(u,v)
  • 到达后的第二天才攻占,所以总共还要再 +1

也就是说,u -> v 的代价是:

dist(u,v) + 1

第二层:因为题目明确说,在一支舰队前往攻打或正在攻打某座城市时,不能再派出别的舰队,所以整个攻占过程一定是串行的。

于是,任意一种攻占方案都可以看成一棵以城市 1 为根的树:

  • 每个非根城市只会被第一次攻占一次
  • 它一定有一个“父城市”,表示是从哪座已占领城市出发攻下它的

这棵树上的每条边 (u,v) 都贡献 dist(u,v)+1 的代价,总答案就是所有边权之和。

所以问题已经变成:

n 个城市之间建立一张完全图,边权是 dist(u,v)+1,求从城市 1 出发覆盖所有城市的最小总代价。

由于边权是对称的,这其实就是最小生成树。

于是做法分两步:

  1. 先 BFS 求出城市两两之间的最短骑士步数
  2. 再在这些城市上做一棵 MST

点数是 2000,棋盘边长最多 150,所以主解可以写成:

  • 每次 Prim 选中一个新城市 u
  • 就从 u 做一遍 BFS,更新它到所有未占领城市的边权 dist(u,v)+1

这样既不用预存所有边,也不用预存完整的城市两两距离矩阵。

代码

cpp
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 2005;
const int MAXM = 155;
const int INF = 1e9;

int n, board_size;
int x[MAXN], y[MAXN];
int dist_to_tree[MAXN];
bool vis[MAXN];
int board_dist[MAXM][MAXM];
int dx[8] = {-1, 1, 2, -2, -1, 1, 2, -2};
int dy[8] = {2, 2, 1, 1, -2, -2, -1, -1};

void bfs_from(int start_id) {
    for (int i = 1; i <= board_size; i++) {
        for (int j = 1; j <= board_size; j++) {
            board_dist[i][j] = -1;
        }
    }

    queue<pair<int, int>> q;
    q.push({x[start_id], y[start_id]});
    board_dist[x[start_id]][y[start_id]] = 0;

    while (!q.empty()) {
        pair<int, int> cur = q.front();
        q.pop();

        int cx = cur.first;
        int cy = cur.second;

        for (int i = 0; i < 8; i++) {
            int nx = cx + dx[i];
            int ny = cy + dy[i];

            if (nx <= 0 || ny <= 0 || nx > board_size || ny > board_size) {
                continue;
            }
            if (board_dist[nx][ny] != -1) {
                continue;
            }

            board_dist[nx][ny] = board_dist[cx][cy] + 1;
            q.push({nx, ny});
        }
    }

    for (int i = 1; i <= n; i++) {
        if (vis[i]) {
            continue;
        }
        int step = board_dist[x[i]][y[i]];
        if (step == -1) {
            continue;
        }
        dist_to_tree[i] = min(dist_to_tree[i], step + 1);
    }
}

int prim() {
    for (int i = 1; i <= n; i++) {
        dist_to_tree[i] = INF;
        vis[i] = false;
    }

    dist_to_tree[1] = 0;
    int answer = 0;

    for (int i = 1; i <= n; i++) {
        int u = 0;
        for (int j = 1; j <= n; j++) {
            if (vis[j]) {
                continue;
            }
            if (u == 0 || dist_to_tree[j] < dist_to_tree[u]) {
                u = j;
            }
        }

        if (u == 0 || dist_to_tree[u] == INF) {
            return -1;
        }

        vis[u] = true;
        answer += dist_to_tree[u];
        bfs_from(u);
    }

    return answer;
}

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

    cin >> n >> board_size;
    for (int i = 1; i <= n; i++) {
        cin >> x[i] >> y[i];
    }

    cout << prim() << '\n';

    return 0;
}

复杂度

设城市数为 n,棋盘边长为 m

Prim 一共会选出 n 个城市;每选出一个城市,就做一次棋盘 BFS:

  • 一次 BFS 是 O(m2)O(m^2)
  • 外层 Prim 选点和更新城市代价总共是 O(n2)O(n^2)

所以总复杂度可以写成:

O(nm2+n2)O(n m^2 + n^2)

在本题数据范围内是可以接受的。

总结

这题最关键的一步,是把“攻占顺序”看成一棵树。只要意识到整个过程不能并行,总代价就是若干次“从已占领城市接入一座新城市”的和,于是题目自然转化成了“BFS 求边权 + 最小生成树”。

一图流解析

这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

一图流解析