[NOIP 2017 普及组] 棋盘

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

把位置、当前颜色和是否刚施法作为状态,在四维状态图上做 Dijkstra 求最小花费。

OJ: luogu

题目 ID: P3956

难度:普及+/提高

标签:最短路图论模拟noip

日期: 2026-06-20 12:57

题意

给一个 m x m 棋盘,部分格子本来就有颜色,部分格子无色。

你要从 (1,1) 走到 (m,m),每次只能上下左右移动。

如果走向本来就有颜色的格子:

  • 同色花费 0
  • 异色花费 1

如果走向无色格子:

  • 必须先施法把它临时染色,花费 2
  • 并且魔法不能连续使用

求最少花费,无法到达输出 -1

思路

先看一个可以直接验证想法的朴素解:

cpp
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
#include <bits/stdc++.h>
using namespace std;

const int MAXM = 8;
const int INF = 1e9;

struct Node {
    int x, y;
    int color;
    int magic;
    int dist;

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

int board_size, colored_cnt;
int board[MAXM][MAXM];
int dista[MAXM][MAXM][2][2];

int dx[4] = {-1, 1, 0, 0};
int dy[4] = {0, 0, -1, 1};

bool in_board(int x, int y) {
    return x >= 1 && x <= board_size && y >= 1 && y <= board_size;
}

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

    memset(board, -1, sizeof(board));

    cin >> board_size >> colored_cnt;
    for (int i = 1; i <= colored_cnt; i++) {
        int x, y, c;
        cin >> x >> y >> c;
        board[x][y] = c;
    }

    if (board[1][1] == -1) {
        cout << -1 << '\n';
        return 0;
    }

    for (int i = 1; i <= board_size; i++) {
        for (int j = 1; j <= board_size; j++) {
            for (int c = 0; c < 2; c++) {
                for (int s = 0; s < 2; s++) {
                    dista[i][j][c][s] = INF;
                }
            }
        }
    }

    // 小数据暴力版:直接在完整状态图上跑最短路。
    priority_queue<Node> pq;
    int start_color = board[1][1];
    dista[1][1][start_color][0] = 0;
    pq.push({1, 1, start_color, 0, 0});

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

        if (cur.dist != dista[cur.x][cur.y][cur.color][cur.magic]) continue;

        if (cur.x == board_size && cur.y == board_size) {
            cout << cur.dist << '\n';
            return 0;
        }

        for (int dir = 0; dir < 4; dir++) {
            int nx = cur.x + dx[dir];
            int ny = cur.y + dy[dir];
            if (!in_board(nx, ny)) continue;

            if (board[nx][ny] != -1) {
                int next_color = board[nx][ny];
                int cost = (cur.color == next_color ? 0 : 1);
                int new_dist = cur.dist + cost;
                if (new_dist < dista[nx][ny][next_color][0]) {
                    dista[nx][ny][next_color][0] = new_dist;
                    pq.push({nx, ny, next_color, 0, new_dist});
                }
                continue;
            }

            if (cur.magic == 1) continue;

            for (int next_color = 0; next_color <= 1; next_color++) {
                int new_dist = cur.dist + 2 + (cur.color != next_color);
                if (new_dist < dista[nx][ny][next_color][1]) {
                    dista[nx][ny][next_color][1] = new_dist;
                    pq.push({nx, ny, next_color, 1, new_dist});
                }
            }
        }
    }

    cout << -1 << '\n';
    return 0;
}

这题表面上像网格最短路,但一个格子不能只看坐标。

因为到达同一个格子时,后续决策还取决于两件事:

  1. 当前脚下格子的颜色是什么;
  2. 当前脚下格子是不是刚用魔法临时染出来的。

所以状态要写成:

  • (x, y, color, magic)

其中:

  • color 表示当前所在格的颜色;
  • magic = 1 表示当前格子是临时染色格;
  • magic = 0 表示当前格子本来就有颜色。

接下来分两类转移。

第一类,下一格本来就有颜色。

设下一格颜色是 next_color,那么:

  • color == next_color,花费加 0
  • color != next_color,花费加 1

并且走过去后 magic 一定变成 0

第二类,下一格是无色格。

这时只有当前 magic == 0 才允许施法。 你可以把下一格染成红色或黄色中的任意一种:

  • 施法固定花费 2
  • 若染出来的颜色和当前颜色不同,再额外花 1

所以总代价是:

  • 2 + (color != next_color)

走过去后,新的状态 magic = 1

所有边权都非负,因此直接在这张状态图上跑 Dijkstra 即可。

代码

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

const int MAXM = 105;
const int INF = 1e9;

struct Node {
    int x, y;
    int color;   // 当前所站格子的颜色:0 红,1 黄
    int magic;   // 0 表示当前格子本来就有颜色,1 表示当前格子是用魔法临时染色的
    int dist;

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

int board_size, colored_cnt;
int board[MAXM][MAXM];               // -1 表示无色,其它是原始颜色
int dista[MAXM][MAXM][2][2];
bool vis[MAXM][MAXM][2][2];

int dx[4] = {-1, 1, 0, 0};
int dy[4] = {0, 0, -1, 1};

bool in_board(int x, int y) {
    return x >= 1 && x <= board_size && y >= 1 && y <= board_size;
}

void update_state(priority_queue<Node> &pq, int x, int y, int color, int magic, int new_dist) {
    if (new_dist >= dista[x][y][color][magic]) return;
    dista[x][y][color][magic] = new_dist;
    pq.push({x, y, color, magic, new_dist});
}

int dijkstra() {
    for (int i = 1; i <= board_size; i++) {
        for (int j = 1; j <= board_size; j++) {
            for (int c = 0; c < 2; c++) {
                for (int s = 0; s < 2; s++) {
                    dista[i][j][c][s] = INF;
                    vis[i][j][c][s] = false;
                }
            }
        }
    }

    // 起点一开始就必须有颜色,否则无法站在上面。
    if (board[1][1] == -1) return -1;

    priority_queue<Node> pq;
    int start_color = board[1][1];
    dista[1][1][start_color][0] = 0;
    pq.push({1, 1, start_color, 0, 0});

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

        int x = cur.x;
        int y = cur.y;
        int color = cur.color;
        int magic = cur.magic;

        if (vis[x][y][color][magic]) continue;
        vis[x][y][color][magic] = true;

        if (x == board_size && y == board_size) {
            return cur.dist;
        }

        for (int dir = 0; dir < 4; dir++) {
            int nx = x + dx[dir];
            int ny = y + dy[dir];
            if (!in_board(nx, ny)) continue;

            // 下一个格子本来就有颜色,可以直接走过去。
            if (board[nx][ny] != -1) {
                int next_color = board[nx][ny];
                int cost = (color == next_color ? 0 : 1);
                update_state(pq, nx, ny, next_color, 0, cur.dist + cost);
                continue;
            }

            // 下一个格子是无色格,只能在“当前格子不是魔法格”时施法进入。
            if (magic == 1) continue;

            for (int next_color = 0; next_color <= 1; next_color++) {
                int cost = 2; // 施法本身花 2 金币
                if (color != next_color) cost++;
                update_state(pq, nx, ny, next_color, 1, cur.dist + cost);
            }
        }
    }

    return -1;
}

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

    memset(board, -1, sizeof(board));

    cin >> board_size >> colored_cnt;
    for (int i = 1; i <= colored_cnt; i++) {
        int x, y, c;
        cin >> x >> y >> c;
        board[x][y] = c;
    }

    cout << dijkstra() << '\n';
    return 0;
}

复杂度

状态数是 m^2 * 2 * 2, 每个状态最多向四个方向转移一次, 所以总时间复杂度是 O(m2logm2)O(m^2 log m^2),空间复杂度是 O(m2)O(m^2)

总结

这题的关键不是“走棋盘”,而是先把影响后续决策的信息补全成状态:

  • 当前位置
  • 当前颜色
  • 是否刚施法

只要这三个因素记全,它就是一题标准的状态最短路。

一图流解析

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

一图流解析