先预处理每个格子最近球员的到达代价,再把空球、控球和四个踢球方向建成 6 层状态图跑最短路。
OJ: luogu
题目 ID: P5100
难度:省选/NOI-
标签:图论最短路网格思维
日期: 2026-06-20 06:02
题意
有
操作分三类:
- 控球时可以运球 1 米,代价
- 控球时可以朝上下左右某个方向踢
米,代价 ,踢完后球员失去控球 - 没控球的球员可以自己走 1 米,代价也是
;如果他和球在同一格且当前没人控球,就能接球
要求最小化所有球员疲劳度之和。
思路
先看一个小数据可以直接验证的版本:
#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;
}朴素版把状态写成两层:
:球在 ,当前没人控球 :球在 ,当前有人控球
然后在
关键观察 1:不用跟踪"到底是哪位球员"
如果球现在静止在某个格子
- 把"某个球员"调到这里来接球,最少要花多少代价
这个值记成
= 最近球员到 的曼哈顿距离
更重要的是,这个值和历史无关。
原因是:如果某个球员之前已经走到别的地方了,再让他去
所以一旦球处于"无人控球"的状态,我们完全不必记录哪位球员以前做过什么,只要付出
关键观察 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["在当前位置停下, 变成空球"]
这样一来,踢球就从"一个点向整条线连边",变成了"沿一个方向一步一步走"。
总代价完全一样,但每个状态的出边数变成常数了。
最终建图
我们只在球场内部
- 两个场内格子之间,走场内的最短路不会比出界更差
- 同一行或同一列的两个场内格子之间,踢球直线段也一直在场内
- 先把球带到场外再带回来,只会额外增加距离
于是状态一共 6 层:
边如下:
,代价 相邻 ,代价 相邻 ,代价 ,表示踢球至少先飞 1 米 同方向下一格 ,代价 ,代价 ,表示球停在这里
起点是
终点是球到达
代码
#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 预处理
最短路里一共是
空间复杂度:
总结
这题最难的地方不是写 Dijkstra,而是先把题目里的"人、球、控球权"理顺。
一旦想清楚两件事:
- 空球状态下,只需要知道最近球员来接球的最小代价
- 踢球可以拆成 4 个方向飞行状态
整题就变成一张标准状态图最短路。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。


