[JOI 2017 Final] 足球 / Soccer

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

先预处理每个格子最近球员的到达代价,再把空球、控球和四个踢球方向建成 6 层状态图跑最短路。

OJ: luogu

题目 ID: P5100

难度:省选/NOI-

标签:图论最短路网格思维

日期: 2026-06-20 06:02

题意

NN 个球员,球一开始在 11 号球员脚下,目标是把球送到 NN 号球员所在的位置。

操作分三类:

  • 控球时可以运球 1 米,代价 CC
  • 控球时可以朝上下左右某个方向踢 pp 米,代价 A×p+BA \times p + B,踢完后球员失去控球
  • 没控球的球员可以自己走 1 米,代价也是 CC;如果他和球在同一格且当前没人控球,就能接球

要求最小化所有球员疲劳度之和。

思路

先看一个小数据可以直接验证的版本:

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

using i64 = long long;

const i64 INF = (1LL << 62);
const int FREE_BALL = 0;
const int CONTROL = 1;

struct HeapNode {
    i64 dist;
    int state;
    int x, y;

    bool operator < (const HeapNode &other) const {
        return dist > other.dist;
    }
};

int H, W, n;
i64 A, B, C;
vector<int> sx, sy;
vector<i64> player_cost;
vector<i64> dist_arr;

int cell_id(int x, int y) {
    return x * (W + 1) + y;
}

int node_id(int state, int x, int y) {
    return state * (H + 1) * (W + 1) + cell_id(x, y);
}

void try_relax(priority_queue<HeapNode> &pq, int state, int x, int y, i64 nd) {
    int id = node_id(state, x, y);
    if (nd < dist_arr[id]) {
        dist_arr[id] = nd;
        pq.push({nd, state, x, y});
    }
}

void prepare_player_cost() {
    int total = (H + 1) * (W + 1);
    vector<int> step_dist(total, -1);
    queue<int> q;

    for (int i = 1; i <= n; i++) {
        int id = cell_id(sx[i], sy[i]);
        if (step_dist[id] == -1) {
            step_dist[id] = 0;
            q.push(id);
        }
    }

    while (!q.empty()) {
        int cur = q.front();
        q.pop();

        int x = cur / (W + 1);
        int y = cur % (W + 1);

        if (x > 0) {
            int nid = cell_id(x - 1, y);
            if (step_dist[nid] == -1) {
                step_dist[nid] = step_dist[cur] + 1;
                q.push(nid);
            }
        }
        if (x < H) {
            int nid = cell_id(x + 1, y);
            if (step_dist[nid] == -1) {
                step_dist[nid] = step_dist[cur] + 1;
                q.push(nid);
            }
        }
        if (y > 0) {
            int nid = cell_id(x, y - 1);
            if (step_dist[nid] == -1) {
                step_dist[nid] = step_dist[cur] + 1;
                q.push(nid);
            }
        }
        if (y < W) {
            int nid = cell_id(x, y + 1);
            if (step_dist[nid] == -1) {
                step_dist[nid] = step_dist[cur] + 1;
                q.push(nid);
            }
        }
    }

    player_cost.assign(total, 0);
    for (int x = 0; x <= H; x++) {
        for (int y = 0; y <= W; y++) {
            int id = cell_id(x, y);
            player_cost[id] = (i64) step_dist[id] * C;
        }
    }
}

void dijkstra() {
    int total_nodes = 2 * (H + 1) * (W + 1);
    dist_arr.assign(total_nodes, INF);

    priority_queue<HeapNode> pq;
    try_relax(pq, CONTROL, sx[1], sy[1], 0);

    while (!pq.empty()) {
        HeapNode cur = pq.top();
        pq.pop();

        int id = node_id(cur.state, cur.x, cur.y);
        if (cur.dist != dist_arr[id]) {
            continue;
        }

        int x = cur.x;
        int y = cur.y;
        i64 now = cur.dist;

        if (cur.state == FREE_BALL) {
            try_relax(pq, CONTROL, x, y, now + player_cost[cell_id(x, y)]);
            continue;
        }

        if (x > 0) try_relax(pq, CONTROL, x - 1, y, now + C);
        if (x < H) try_relax(pq, CONTROL, x + 1, y, now + C);
        if (y > 0) try_relax(pq, CONTROL, x, y - 1, now + C);
        if (y < W) try_relax(pq, CONTROL, x, y + 1, now + C);

        for (int nx = 0; nx <= H; nx++) {
            if (nx == x) {
                continue;
            }
            i64 cost = now + (i64) abs(nx - x) * A + B;
            try_relax(pq, FREE_BALL, nx, y, cost);
        }
        for (int ny = 0; ny <= W; ny++) {
            if (ny == y) {
                continue;
            }
            i64 cost = now + (i64) abs(ny - y) * A + B;
            try_relax(pq, FREE_BALL, x, ny, cost);
        }
    }
}

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

    cin >> H >> W;
    cin >> A >> B >> C;
    cin >> n;

    sx.assign(n + 1, 0);
    sy.assign(n + 1, 0);
    for (int i = 1; i <= n; i++) {
        cin >> sx[i] >> sy[i];
    }

    prepare_player_cost();
    dijkstra();

    int tx = sx[n];
    int ty = sy[n];
    i64 answer = min(
        dist_arr[node_id(FREE_BALL, tx, ty)],
        dist_arr[node_id(CONTROL, tx, ty)]
    );

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

朴素版把状态写成两层:

  • FREE(x,y)FREE(x, y):球在 (x,y)(x, y),当前没人控球
  • CONTROL(x,y)CONTROL(x, y):球在 (x,y)(x, y),当前有人控球

然后在 CONTROLCONTROL 状态里显式枚举同行同列的所有踢球落点。这个做法很好理解,但一旦格子数到 500×500500 \times 500,每次都枚举整行整列就太慢了。

关键观察 1:不用跟踪"到底是哪位球员"

如果球现在静止在某个格子 (x,y)(x, y),我们只关心:

  • 把"某个球员"调到这里来接球,最少要花多少代价

这个值记成 need[x][y]need[x][y]。因为所有球员走 1 米的代价都是 CC,所以它就是:

  • need[x][y]need[x][y] = 最近球员到 (x,y)(x, y) 的曼哈顿距离 ×C\times C

更重要的是,这个值和历史无关。
原因是:如果某个球员之前已经走到别的地方了,再让他去 (x,y)(x, y),总代价一定不小于"直接从他的初始位置走到 (x,y)(x, y)"。这是曼哈顿距离的三角不等式。

所以一旦球处于"无人控球"的状态,我们完全不必记录哪位球员以前做过什么,只要付出 need[x][y]need[x][y],就能认为"有一位最便宜的球员来到这里接球"。

关键观察 2:把踢球拆成飞行状态

主程序不再从一个控球格子直接连向整行整列,而是把"球正在朝某个方向飞"也看成状态:

flowchart LR
  F["FREE(x,y) 空球"] -->|need[x][y]| C["CONTROL(x,y) 控球"]
  C -->|+C| C2["相邻格子, 继续控球"]
  C -->|+A+B| K["开始朝某方向飞 1 米"]
  K -->|+A| K2["继续朝同方向飞"]
  K -->|0| F2["在当前位置停下, 变成空球"]

这样一来,踢球就从"一个点向整条线连边",变成了"沿一个方向一步一步走"。
总代价完全一样,但每个状态的出边数变成常数了。

最终建图

我们只在球场内部 0H,0W0 \dots H, 0 \dots W 这些格子上建图。最优解没有必要把球和球员带到场外,因为:

  • 两个场内格子之间,走场内的最短路不会比出界更差
  • 同一行或同一列的两个场内格子之间,踢球直线段也一直在场内
  • 先把球带到场外再带回来,只会额外增加距离

于是状态一共 6 层:

  • FREEFREE
  • CONTROLCONTROL
  • KICK_NORTHKICK\_NORTH
  • KICK_SOUTHKICK\_SOUTH
  • KICK_WESTKICK\_WEST
  • KICK_EASTKICK\_EAST

边如下:

  1. FREECONTROLFREE \to CONTROL,代价 need[x][y]need[x][y]
  2. CONTROLCONTROL \to 相邻 CONTROLCONTROL,代价 CC
  3. CONTROLCONTROL \to 相邻 KICK_KICK\_*,代价 A+BA + B,表示踢球至少先飞 1 米
  4. KICK_KICK\_* \to 同方向下一格 KICK_KICK\_*,代价 AA
  5. KICK_FREEKICK\_* \to FREE,代价 00,表示球停在这里

起点是 11 号球员一开始就控球,所以:

  • CONTROL(S1,T1)=0CONTROL(S_1, T_1) = 0

终点是球到达 NN 号球员所在格子即可,所以答案取:

  • min(FREE(SN,TN),CONTROL(SN,TN))\min(FREE(S_N, T_N), CONTROL(S_N, T_N))

代码

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

using i64 = long long;

const i64 INF = (1LL << 62);
const int FREE_BALL = 0;
const int CONTROL = 1;
const int KICK_NORTH = 2;
const int KICK_SOUTH = 3;
const int KICK_WEST = 4;
const int KICK_EAST = 5;
const int STATE_CNT = 6;

struct HeapNode {
    i64 dist;
    int state;
    int x, y;

    bool operator < (const HeapNode &other) const {
        return dist > other.dist;
    }
};

int H, W, n;
i64 A, B, C;
vector<int> sx, sy;
vector<i64> player_cost;
vector<i64> dist_arr;

int cell_id(int x, int y) {
    return x * (W + 1) + y;
}

int node_id(int state, int x, int y) {
    return state * (H + 1) * (W + 1) + cell_id(x, y);
}

void try_relax(priority_queue<HeapNode> &pq, int state, int x, int y, i64 nd) {
    int id = node_id(state, x, y);
    if (nd < dist_arr[id]) {
        dist_arr[id] = nd;
        pq.push({nd, state, x, y});
    }
}

void prepare_player_cost() {
    int total = (H + 1) * (W + 1);
    vector<int> step_dist(total, -1);
    queue<int> q;

    for (int i = 1; i <= n; i++) {
        int id = cell_id(sx[i], sy[i]);
        if (step_dist[id] == -1) {
            step_dist[id] = 0;
            q.push(id);
        }
    }

    while (!q.empty()) {
        int cur = q.front();
        q.pop();

        int x = cur / (W + 1);
        int y = cur % (W + 1);

        if (x > 0) {
            int nid = cell_id(x - 1, y);
            if (step_dist[nid] == -1) {
                step_dist[nid] = step_dist[cur] + 1;
                q.push(nid);
            }
        }
        if (x < H) {
            int nid = cell_id(x + 1, y);
            if (step_dist[nid] == -1) {
                step_dist[nid] = step_dist[cur] + 1;
                q.push(nid);
            }
        }
        if (y > 0) {
            int nid = cell_id(x, y - 1);
            if (step_dist[nid] == -1) {
                step_dist[nid] = step_dist[cur] + 1;
                q.push(nid);
            }
        }
        if (y < W) {
            int nid = cell_id(x, y + 1);
            if (step_dist[nid] == -1) {
                step_dist[nid] = step_dist[cur] + 1;
                q.push(nid);
            }
        }
    }

    player_cost.assign(total, 0);
    for (int x = 0; x <= H; x++) {
        for (int y = 0; y <= W; y++) {
            int id = cell_id(x, y);
            player_cost[id] = (i64) step_dist[id] * C;
        }
    }
}

void dijkstra() {
    int total_nodes = STATE_CNT * (H + 1) * (W + 1);
    dist_arr.assign(total_nodes, INF);

    priority_queue<HeapNode> pq;
    try_relax(pq, CONTROL, sx[1], sy[1], 0);

    while (!pq.empty()) {
        HeapNode cur = pq.top();
        pq.pop();

        int id = node_id(cur.state, cur.x, cur.y);
        if (cur.dist != dist_arr[id]) {
            continue;
        }

        int x = cur.x;
        int y = cur.y;
        i64 now = cur.dist;

        if (cur.state == FREE_BALL) {
            try_relax(pq, CONTROL, x, y, now + player_cost[cell_id(x, y)]);
            continue;
        }

        if (cur.state == CONTROL) {
            if (x > 0) try_relax(pq, CONTROL, x - 1, y, now + C);
            if (x < H) try_relax(pq, CONTROL, x + 1, y, now + C);
            if (y > 0) try_relax(pq, CONTROL, x, y - 1, now + C);
            if (y < W) try_relax(pq, CONTROL, x, y + 1, now + C);

            if (x > 0) try_relax(pq, KICK_NORTH, x - 1, y, now + A + B);
            if (x < H) try_relax(pq, KICK_SOUTH, x + 1, y, now + A + B);
            if (y > 0) try_relax(pq, KICK_WEST, x, y - 1, now + A + B);
            if (y < W) try_relax(pq, KICK_EAST, x, y + 1, now + A + B);
            continue;
        }

        try_relax(pq, FREE_BALL, x, y, now);

        if (cur.state == KICK_NORTH) {
            if (x > 0) try_relax(pq, KICK_NORTH, x - 1, y, now + A);
        }
        else if (cur.state == KICK_SOUTH) {
            if (x < H) try_relax(pq, KICK_SOUTH, x + 1, y, now + A);
        }
        else if (cur.state == KICK_WEST) {
            if (y > 0) try_relax(pq, KICK_WEST, x, y - 1, now + A);
        }
        else {
            if (y < W) try_relax(pq, KICK_EAST, x, y + 1, now + A);
        }
    }
}

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

    cin >> H >> W;
    cin >> A >> B >> C;
    cin >> n;

    sx.assign(n + 1, 0);
    sy.assign(n + 1, 0);
    for (int i = 1; i <= n; i++) {
        cin >> sx[i] >> sy[i];
    }

    prepare_player_cost();
    dijkstra();

    int tx = sx[n];
    int ty = sy[n];
    i64 answer = min(
        dist_arr[node_id(FREE_BALL, tx, ty)],
        dist_arr[node_id(CONTROL, tx, ty)]
    );

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

复杂度

先用多源 BFS 预处理 need[x][y]need[x][y],复杂度 O(HW+N)O(HW + N)

最短路里一共是 6×(H+1)×(W+1)6 \times (H + 1) \times (W + 1) 个状态,每个状态只有常数条出边,所以 Dijkstra 的复杂度是:

  • O(HWlog(HW))O(HW \log(HW))

空间复杂度:

  • O(HW)O(HW)

总结

这题最难的地方不是写 Dijkstra,而是先把题目里的"人、球、控球权"理顺。

一旦想清楚两件事:

  1. 空球状态下,只需要知道最近球员来接球的最小代价
  2. 踢球可以拆成 4 个方向飞行状态

整题就变成一张标准状态图最短路。

一图流解析

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

一图流解析