图片来源
1522 字
8 分钟
[noip 2017 普及组] 棋盘
0.题目描述
有一个的棋盘,棋盘上每一个格子可能是红色、黄色或没有任何颜色的。你现在要从棋盘的最左上角走到棋盘的最右下角。
任何一个时刻,你所站在的位置必须是有颜色的(不能是无色的), 你只能向上、 下、左、 右四个方向前进。当你从一个格子走向另一个格子时,如果两个格子的颜色相同,那你不需要花费金币;如果不同,则你需要花费 个金币。
你可以花费 个金币将一个无色格子暂时变成指定的颜色,但是在到达一个原本就有颜色的格子前不能再次使用,离开该格子后颜色恢复。
求最少花费的金币数。
本来的描述就挺简洁的,所以大部分引用的原题的描述
1. 心路历程
本来我看到这个格子,还是从左上角到右下角,直接满脑子都是 dp,然后写了一大堆只有 分,然后才发现他不一定要按拓扑序走才是最短的,所以正解是搜索(最短路),然后搜索写了四个版本,最后发现第一个(最暴力的那个)思路是对的,毕竟数据只有 。
2. 解析
2.1 前言
- 那个 dp 的错误思想就不多说了,这个思路只能应付部分按照拓扑序走的情况。
- 因为正解就是暴力,暴力就是正解,所以这个题的代码量不算太小,而且需要头脑清醒
2.2 思路
-
把能走的路全部建一条边,边权是走过去消耗的金币数,然后对生成的图跑 Dijkstra。
简单的一句话,但是实现起来缺不太容易
2.3 实现
-
点如何编号
如图,把每一行都接上,顺着编号。

从坐标对应到编号的函数:
int tonum(const int &x, const int &y) {return n * (x - 1) + y;}从编号对应到坐标的函数:
pair<int, int> tosite(const int &x) {return {ceil((double)x / n), x % n == 0 ? n : x % n};} -
**建边逻辑 **(强烈建议单独写一个函数)
以样例为例 ,看上面的图
设入点为 ,出点为 。
颜色() : 为无色, 为红色, 为黄色。
- 且 由于不能连续施展魔法所以不能走,不建边 和
- 且 可以消耗两个金币施展魔法将 变成 ,建边,边权为 。 和
- 且 如果能走,则 已经被施展魔法,颜色取决于给 施展魔法的点,建边,边权为 ,在求最短路时特判。 和
- 且
- 可以直接走,建边,边权为 。 和
- 可以消耗一个金币走,建边,边权为 。 和
-
Dijkstra
大部分套模板就行,需要特判一下建边的情况 3,在队列的参数里面加一项,记录父节点,当搜到一个边权为 的边时对比它和它的隔代祖先节点的颜色,决定将 改成是 还是 ,相当于是转化成了建边的情况 4。
3.总结
这个看似简单的暴力题细节特别多,不要太小看他了,一定要保持清醒
Ac Code
#include <cstdio>#include <cstring>#include <cmath>#include <queue>
using namespace std;
typedef pair<int, int> pii;typedef pair<int, pii> piii;
const int N = 110;const int M = 40010;
int c[N][N]; // 记录颜色
int nxt[M], ver[M], w[M], head[M]; // 存图int tot;
int n, m;
void add(int x, int y, int e) { ver[++tot] = y; w[tot] = e; nxt[tot] = head[x]; head[x] = tot;}
int tonum(const int &x, const int &y) { // 坐标转到序号 return n * (x - 1) + y;}
pair<int, int> tosite(const int &x) { // 序号转到坐标 // 这里要注意,稍不留神就错了,主要是取模的问题,本来是5的会被模成0,注意特判 return {ceil((double)x / n), x % n == 0 ? n : x % n};}
int tocol(const int &x) { // 序号转到颜色 return c[tosite(x).first][tosite(x).second];}
bool judge(const int &x, const int &y) { // 判断是否出界 return x > 0 && x <= n && y > 0 && y <= n;}
void build(const int &a, const int &b, const int &x, const int &y) { // 难点:建边 if (c[a][b] == -1 && c[x][y] == -1) return; // 两个没有颜色,只能变一个,所以不能走 else if (c[x][y] == -1) add(tonum(a, b), tonum(x, y), 2); // 有 -> 无 消耗2个金币 else if (c[a][b] == -1) add(tonum(a, b), tonum(x, y), -1); // 无 -> 有 视情况而定,先弄成-1到时候再判断 else if (c[a][b] == c[x][y]) add(tonum(a, b), tonum(x, y), 0); // 颜色相同,消耗0个金币 else add(tonum(a, b), tonum(x, y), 1); // 颜色不同,消耗1个金币}
int dijkstra(const int &s, const int &t) { priority_queue<piii, vector<piii>, greater<piii> > q; // pair<距离, <编号,父节点> > int d[M]; bool v[M]; memset(d, 0x3f, sizeof(d)); memset(v, 0, sizeof(v)); d[s] = 0; q.push({0, {s, 0}}); while (!q.empty()) { int x = q.top().second.first; int fa = q.top().second.second; q.pop(); if (v[x]) continue; v[x] = 1; for (int i = head[x]; i; i = nxt[i]) { int &y = ver[i]; int t = w[i]; // 之前特殊标记的,y与x的父节点(y的隔代祖先)颜色相同则消耗0个,不同则消耗1个 // 默认把没颜色的节点标记成其父节点的颜色,方便统计 if (w[i] == -1) { if (tocol(fa) != tocol(y)) { t = 1; } else t = 0; } // 正常的dijkstra的状态转移 if (d[y] > d[x] + t) { d[y] = d[x] + t; q.push({d[y], {y, x}}); } } } return d[t] == 0x3f3f3f3f ? -1 : d[t]; // 注意特判不能到达的情况}
int main() { memset(c, -1, sizeof(c)); scanf("%d%d", &n, &m); for (int i = 1; i <= m; ++i) { int x, y, z; scanf("%d%d%d", &x, &y, &z); c[x][y] = z; } for (int i = 1; i <= n; ++i) { for (int j = 1; j <= n; ++j) { // 建边,注意判断有没出界 if (judge(i + 1, j)) build(i, j, i + 1, j); if (judge(i, j + 1)) build(i, j, i, j + 1); if (judge(i - 1, j)) build(i, j, i - 1, j); if (judge(i, j - 1)) build(i, j, i, j - 1); } } printf("%d\n", dijkstra(1, tonum(n, n))); return 0;}部分信息可能已经过时







