1. 项目背景与题目解析
这道P3956 [NOIP 2017 普及组] 棋盘题是信息学奥林匹克竞赛中的经典题目,考察选手对图论算法和动态规划的综合运用能力。题目描述了一个n×n的棋盘,每个格子有颜色(红色或黄色)或者没有颜色(需要魔法临时赋予颜色)。玩家从左上角(1,1)出发,每次可以移动到相邻的格子(上下左右),目标是最小花费到达右下角(n,n)。
我在实际解题过程中发现,这道题看似简单,但暗藏多个考察点:
- 棋盘行走的最短路径问题
- 颜色变化带来的状态转移
- 魔法使用的条件限制
- 花费计算的动态规划思路
2. 核心算法设计思路
2.1 问题建模与状态定义
这道题适合用带状态的广度优先搜索(BFS)来解决。我们需要记录:
- 当前位置(x,y)
- 当前颜色c(红/黄/无色)
- 已花费的金币数g
- 是否使用过魔法m(布尔值)
状态转移需要考虑以下几种情况:
- 移动到有颜色的格子
- 移动到无颜色格子(必须使用魔法)
- 颜色相同和不同时的花费计算
2.2 算法选择依据
为什么选择BFS而不是DFS?
- BFS天然适合寻找最短路径
- 可以方便地记录状态和花费
- 避免DFS可能出现的栈溢出问题
实际编码时可以采用优先队列优化的Dijkstra算法,因为:
- 不同路径的花费不同
- 需要保证每次扩展的都是当前最小花费的路径
3. 详细实现步骤
3.1 数据结构定义
首先定义必要的数据结构和常量:
cpp复制#include <iostream>
#include <queue>
#include <cstring>
using namespace std;
const int MAXN = 105;
const int INF = 0x3f3f3f3f;
struct Node {
int x, y; // 当前位置
int color; // 当前颜色 0:无色 1:红 2:黄
int cost; // 已花费
bool magic; // 是否使用过魔法
// 重载运算符用于优先队列
bool operator>(const Node& other) const {
return cost > other.cost;
}
};
int grid[MAXN][MAXN]; // 棋盘
int dist[MAXN][MAXN][3][2]; // 记忆化数组
int dir[4][2] = {{-1,0},{1,0},{0,-1},{0,1}}; // 方向数组
3.2 核心算法实现
下面是BFS的核心实现:
cpp复制int bfs(int n) {
priority_queue<Node, vector<Node>, greater<Node>> q;
memset(dist, INF, sizeof(dist));
// 初始状态
q.push({1, 1, grid[1][1], 0, false});
dist[1][1][grid[1][1]][0] = 0;
while(!q.empty()) {
Node cur = q.top();
q.pop();
// 到达终点
if(cur.x == n && cur.y == n) {
return cur.cost;
}
// 遍历四个方向
for(int i = 0; i < 4; i++) {
int nx = cur.x + dir[i][0];
int ny = cur.y + dir[i][1];
// 边界检查
if(nx < 1 || nx > n || ny < 1 || ny > n) continue;
int new_color = grid[nx][ny];
int new_cost = cur.cost;
bool new_magic = cur.magic;
// 情况1:目标格子有颜色
if(new_color != 0) {
if(new_color == cur.color) {
new_cost += 0; // 同色不花钱
} else {
new_cost += 1; // 不同色花1金币
}
new_magic = false; // 重置魔法状态
}
// 情况2:目标格子无颜色且未使用魔法
else if(!cur.magic) {
new_color = cur.color; // 使用魔法赋予相同颜色
new_cost += 2; // 花费2金币
new_magic = true; // 标记已使用魔法
}
// 情况3:不能移动(无颜色且已使用魔法)
else {
continue;
}
// 更新状态
if(new_cost < dist[nx][ny][new_color][new_magic]) {
dist[nx][ny][new_color][new_magic] = new_cost;
q.push({nx, ny, new_color, new_cost, new_magic});
}
}
}
return -1; // 无法到达
}
4. 关键问题与优化技巧
4.1 常见错误分析
在实现过程中容易出现的几个问题:
-
魔法使用条件判断错误:
- 必须在移动前检查是否可以使用魔法
- 使用魔法后要正确更新颜色和花费
-
状态记录不完整:
- 必须同时记录位置、颜色、魔法使用状态
- 缺少任一维度都可能导致错误结果
-
优先队列使用不当:
- 必须确保每次取出的是当前最小花费的节点
- 忘记重载比较运算符会导致队列顺序错误
4.2 性能优化建议
-
记忆化剪枝:
cpp复制if(new_cost < dist[nx][ny][new_color][new_magic]) { // 更新并加入队列 }这行代码确保只有当找到更优解时才继续扩展,大幅减少不必要的计算。
-
输入优化:
cpp复制ios::sync_with_stdio(false); cin.tie(0);对于大规模数据输入,使用这2行代码可以显著提高读取速度。
-
空间优化:
对于更大的棋盘,可以考虑使用滚动数组或其他压缩技巧减少内存使用。
5. 完整代码实现
以下是整合后的完整AC代码:
cpp复制#include <iostream>
#include <queue>
#include <cstring>
using namespace std;
const int MAXN = 105;
const int INF = 0x3f3f3f3f;
struct Node {
int x, y;
int color;
int cost;
bool magic;
bool operator>(const Node& other) const {
return cost > other.cost;
}
};
int grid[MAXN][MAXN];
int dist[MAXN][MAXN][3][2];
int dir[4][2] = {{-1,0},{1,0},{0,-1},{0,1}};
int bfs(int n) {
priority_queue<Node, vector<Node>, greater<Node>> q;
memset(dist, INF, sizeof(dist));
q.push({1, 1, grid[1][1], 0, false});
dist[1][1][grid[1][1]][0] = 0;
while(!q.empty()) {
Node cur = q.top();
q.pop();
if(cur.x == n && cur.y == n) {
return cur.cost;
}
for(int i = 0; i < 4; i++) {
int nx = cur.x + dir[i][0];
int ny = cur.y + dir[i][1];
if(nx < 1 || nx > n || ny < 1 || ny > n) continue;
int new_color = grid[nx][ny];
int new_cost = cur.cost;
bool new_magic = cur.magic;
if(new_color != 0) {
if(new_color != cur.color) {
new_cost += 1;
}
new_magic = false;
} else if(!cur.magic) {
new_color = cur.color;
new_cost += 2;
new_magic = true;
} else {
continue;
}
if(new_cost < dist[nx][ny][new_color][new_magic]) {
dist[nx][ny][new_color][new_magic] = new_cost;
q.push({nx, ny, new_color, new_cost, new_magic});
}
}
}
return -1;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int m, n;
cin >> m >> n;
memset(grid, 0, sizeof(grid));
for(int i = 0; i < n; i++) {
int x, y, c;
cin >> x >> y >> c;
grid[x][y] = c + 1; // 1:红 2:黄
}
cout << bfs(m) << endl;
return 0;
}
6. 测试用例与验证
为了确保代码的正确性,建议测试以下几个典型用例:
-
基础测试:
code复制5 3 1 1 0 2 2 1 5 5 0预期输出:8
-
无需魔法的情况:
code复制5 2 1 1 0 5 5 0预期输出:10(必须使用两次魔法)
-
大棋盘测试:
code复制100 1 50 50 1需要验证算法在大规模数据下的表现
7. 算法扩展与变种
这道题可以有多种变种形式,适合进一步练习:
- 多魔法次数:将魔法使用限制从1次改为k次
- 多颜色扩展:增加更多颜色种类和转换规则
- 移动方式变化:允许斜向移动或跳跃移动
- 动态棋盘:棋盘颜色会随时间变化
对于想进一步提高的同学,可以尝试实现这些变种题目,加深对状态搜索类问题的理解。
