0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1228,日期见页头。两边不一致时信原站。
⚠ 这道题的题面带两张图(毯子的四种形状、样例解释)—— 两张都存了本地一份(public/sol/)。
★ 其中那张形状图不是装饰,它是分数:c = 1/2/3/4 的定义只在图上,文字里一个字都没有。
⇒ 所以「转录题面」这件事在这道题上必须连图一起转,第 ③ 步还会把它独立验一遍。
题目描述
相传在一个古老的阿拉伯国家里,有一座宫殿。宫殿里有个四四方方的格子迷宫, 国王选择驸马的方法非常特殊,也非常简单:公主就站在其中一个方格子上, 只要谁能用地毯将除公主站立的地方外的所有地方盖上,美丽漂亮聪慧的公主就是他的人了。
公主这一个方格不能用地毯盖住,毯子的形状有所规定,只能有四种选择(如图); 并且每一方格只能用一层地毯,迷宫的大小为 2ᵏ × 2ᵏ 的方形。

(原题图 1,存档自洛谷。看清楚这四个形状:1 缺左上、2 缺右上、3 缺左下、4 缺右下 —— 换句话说,编号就是「缺口在哪个角」。)
输入格式:第一行一个整数 k,即迷宫大小为 2ᵏ × 2ᵏ(0 < k ≤ 10);
第二行两个整数 x、y,即公主所在方格的坐标(x 为行坐标,y 为列坐标)。
输出格式:将迷宫填补完整的方案。每一补(行)为 x y c
(x、y 为毯子拐角的行坐标和列坐标,c 为使用毯子的形状,具体见原题的图 1,
毯子形状分别用 1、2、3、4 表示,三个数之间用一个空格隔开)。
说明:这道题是 special judge(方案不唯一,只要盖得合法就算对)。 原站列了 spj 的四种报错:① c 越界;② x、y 越界;③ 该位置已被覆盖;④ 该位置从未被覆盖。
输入输出样例
输入
3 3 3
输出
5 5 1 2 2 4 1 1 4 1 4 3 4 1 2 4 4 1 2 7 3 1 5 4 1 8 3 3 6 3 4 8 1 7 2 2 5 1 4 6 3 2 8 1 2 8 4 1 7 7 1 6 6 1 5 8 3 8 5 2 8 8 1
k = 3(8×8),公主在第 3 行第 3 列。上面那段输出是仓库里的 p1228.cpp 真跑出来的,
而且和原站样例逐字节相同 —— 这一点在第 ⑥ 步会变成一条很有用的证据。
样例解释(原题的第二张图,同样存档自洛谷):每个格子上的数字,就是盖住它那块毯子的形状编号; 灰色那格是公主。

1先看清楚:一块毯子长什么样
题面说毯子「只有四种选择」,配了张图 —— 说白了就一句话:
一块毯子 = 2×2 的方块里去掉一格(所以它是个 L 形,占 3 格)。
四种就是「去掉的是哪一格」。下面用 @ 标出题面说的那个拐角(就是输出里的 x y),
+ 是另外两格,. 是缺口:
c = 1 c = 2 c = 3 c = 4
缺左上 缺右上 缺左下 缺右下
. + + . + @ @ +
+ @ @ + . + + .
★ 记法只有一句:拐角永远在缺口的对角,而 c 就是缺口在哪个角(1 左上、2 右上、3 左下、4 右下)。
⚠ 这四个编号只在原题图 1 上(第 ⓪ 步那张),文字题面里一个字都没有 —— 所以第 ③ 步会拿样例把它独立验一遍,而不是「看图看出来的」。
2分治的形状:中心那一块毯子
这道题和 P5461 是同一个套路:切成四块。但这里多一层意思, 而且它就是整道题的题眼:
把 2^k x 2^k 切成四个 2^(k-1) x 2^(k-1):
公主只在其中一块里 -> 这一块可以直接递归(它有一个格子被占住了)
另外三块是完整的正方形 -> 递归不了!因为「铺满一整块」根本做不到
(3 除不尽 4^k,L 形毯子永远铺不满一个完整正方形)
⇒ 在正中心放一块毯子,让它盖住另外三块各自「靠中心的那一格」:
1 2 3 4
+----+----+----+----+
1 | | | | | k = 2(4x4),公主 P 在 (2,2)
+----+----+----+----+
2 | | P | a | | 中心那 2x2 是 (2,2) (2,3) (3,2) (3,3)
+----+----+----+----+ 公主占了左上那格 -> 毯子盖住 a b c 三格
3 | | b | c | | 拐角在 c = (3,3),缺口在左上 -> 输出「3 3 1」
+----+----+----+----+
4 | | | | |
+----+----+----+----+
放完这一块之后,四个子块各有一个被占住的格子 —— 公主那块是公主,另外三块是刚铺的 a、b、c。 于是四块变成了四个一模一样的小问题,各自递归下去,边长为 1 时停。
solve(正方形, 已占住的那一格) —— 你只要保证每次递归下去的都是这个形状,
它自己会把整张图铺满。而「中心那块毯子」的全部作用,就是把三块没有缺口的正方形,
变成三块有缺口的正方形。
★ 顺带算一下总块数:(4^k − 1) / 3。k = 10 时是 349 525 块 —— 第 ⑦ 步会用到。
3★★ 真正卡住人的地方:c 到底怎么编号?
算法到上一步就写完了。可这道题还有一关和算法毫无关系,而且过不去就是零分:编号。
题面把形状定义在图里。图我们存了一份(第 ⓪ 步那张),可图是给人看的,代码要的是另一句话:
c到底对应哪两格 —— 拐角在哪儿,另外两格往哪个方向伸?
从图翻到这句话,中间隔着一次「看图 → 写坐标」,而这一步特别容易译错:
- 图上标着 1 的那块,你读成「缺左上」还是「拐角在左上」?两种读法正好相反;
- 3 和 4 长得最像,顺手记反是一秒钟的事(第 ⑤ 步就是这么错的)。
⚠ 而且译错了自己发现不了:分治全对、格式全对、每行三个合法的数,交上去全错。
⇒ 与其盯着图确认,不如把它解出来:原站那组样例本身就把编号锁死了。
原站给了一组样例(k = 3,公主在 (3,3),21 块毯子)。那 21 行就是一份标准答案,
把它按某种编号方案摆到 8×8 的棋盘上:
- 猜对了 ⇒ 63 个格子恰好各盖一次,公主那格空着;
- 猜错了 ⇒ 立刻出现重叠、出界或者漏格。
⇒ 24 种猜法逐个试一遍,能铺满的只有一种。这不是猜,是解出来的。
// P1228 —— 「毯子形状 1/2/3/4」到底怎么编号?拿样例把它反推出来//// 输入:无(原站那组样例写死在里面)// 输出:24 种猜法逐个验一遍,最后剩下的那一种就是答案//// ★ 这份程序解决的是这道题**唯一真正的门槛**:// 题面说毯子「只能有四种选择(如图)」—— 而那张图我们抄不过来(题面转录只有文字)。// 编号猜错的话,算法一个字都没错,交上去照样全错。//// ★★ 但编号不用猜:**原站给了一组样例,样例本身就把编号锁死了。**// 一块毯子占 2x2 里的三格,(x,y) 是拐角,另外两格必然是「上或下」+「左或右」。// 于是 c=1..4 到底对应哪四种,一共只有 4! = 24 种可能 ——// 把样例那 21 块按每种猜法摆到 8x8 的棋盘上,**能恰好铺满(公主那格除外)的只有一种**。
#include <bits/stdc++.h>using namespace std;
// 原站样例:k = 3(8x8),公主在 (3,3),21 块毯子const int SK = 3, SX = 3, SY = 3;const int SAMPLE[21][3] = { {5,5,1},{2,2,4},{1,1,4},{1,4,3},{4,1,2},{4,4,1},{2,7,3}, {1,5,4},{1,8,3},{3,6,3},{4,8,1},{7,2,2},{5,1,4},{6,3,2}, {8,1,2},{8,4,1},{7,7,1},{6,6,1},{5,8,3},{8,5,2},{8,8,1},};
// 四种「另外两格往哪儿伸」:(dx, dy) —— dx = -1 上 / +1 下,dy = -1 左 / +1 右const int DX[4] = {-1, -1, +1, +1};const int DY[4] = {-1, +1, -1, +1};const char* NAME[4] = {"上 + 左(缺口在左上)", "上 + 右(缺口在右上)", "下 + 左(缺口在左下)", "下 + 右(缺口在右下)"};
int main() { int n = 1 << SK; int order[4] = {0, 1, 2, 3}; int okCount = 0, okOrder[4] = {0, 0, 0, 0}; int tried = 0;
do { tried++; vector<vector<int>> g(n + 2, vector<int>(n + 2, 0)); bool ok = true; string why; for (auto& p : SAMPLE) { int x = p[0], y = p[1], c = p[2]; int d = order[c - 1]; // 这一种猜法里,c 对应哪个方向 int cell[3][2] = {{x, y}, {x + DX[d], y}, {x, y + DY[d]}}; for (auto& q : cell) { int a = q[0], b = q[1]; if (a < 1 || a > n || b < 1 || b > n) { ok = false; why = "有格子出界"; break; } if (a == SX && b == SY) { ok = false; why = "盖住了公主"; break; } if (g[a][b]) { ok = false; why = "两块毯子重叠"; break; } g[a][b] = 1; } if (!ok) break; } int covered = 0; for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) covered += g[i][j]; if (ok && covered != n * n - 1) { ok = false; why = "有格子没盖到"; }
printf("猜法 %2d:c=1->%d c=2->%d c=3->%d c=4->%d %s\n", tried, order[0] + 1, order[1] + 1, order[2] + 1, order[3] + 1, ok ? "★ 铺满了" : ("x " + why).c_str()); if (ok) { okCount++; for (int i = 0; i < 4; i++) okOrder[i] = order[i]; } } while (next_permutation(order, order + 4));
printf("\n24 种猜法里,能把样例铺满的有 %d 种。\n", okCount); if (okCount == 1) { printf("⇒ 编号是唯一确定的,不用猜:\n\n"); for (int c = 1; c <= 4; c++) { int d = okOrder[c - 1]; printf(" c = %d -> 另外两格是 (x%+d,y) 和 (x,y%+d) -> %s\n", c, DX[d], DY[d], NAME[d]); } } return 0;}点「运行 ▶」看结果
跑出来的结论(下面整道题都按它写,而且它和原题图 1 完全一致 —— 区别是:现在这是验过的,不是看图看出来的):
24 种猜法里,能把样例铺满的有 1 种。
⇒ 编号是唯一确定的,不用猜:
c = 1 -> 另外两格是 (x-1,y) 和 (x,y-1) -> 上 + 左(缺口在左上)
c = 2 -> 另外两格是 (x-1,y) 和 (x,y+1) -> 上 + 右(缺口在右上)
c = 3 -> 另外两格是 (x+1,y) 和 (x,y-1) -> 下 + 左(缺口在左下)
c = 4 -> 另外两格是 (x+1,y) 和 (x,y+1) -> 下 + 右(缺口在右下)
★ 而它整理成一句话就是第 ① 步那句:c = 缺口在哪个角(1 左上、2 右上、3 左下、4 右下), 正好等于「公主/已占格落在哪个象限」 —— 于是代码里连转换都不用写。
题面里「见图」的那部分,常常可以从样例反推出来 —— 而且往往是唯一解。
⚠ 反过来也要记住:只有一组样例的时候,这个反推能不能成立要自己验一遍 (这里是 24 选 1,样例那 21 块正好把四种形状全用上了,所以锁得死)。
4先造一把尺子:不交洛谷,也能判自己对不对
这道题是 special judge:合法方案有很多种,「和样例逐字节相同」这条尺子在这里是错的 (换一份合法答案照样满分)。能判的只有一件事:覆盖关系对不对。
⇒ 写一份自检程序:把答案还原成棋盘,逐格数它被盖了几次。 它报的四种错,和原站 spj 的四种报错一一对应:
// P1228 自检:把一份答案还原成棋盘,看它到底合不合法//// 输入:第一行 k;第二行 x y(公主);接下来若干行 "x y c",读到文件尾// 输出:一行结论。合法就报「n x n 的 N 格恰好各盖一次」,不合法就报是哪一种错// (四种错和原站 spj 的报错代码一一对应:c 越界 / 坐标越界 / 重复覆盖 / 有格子没盖到)//// ★ 这份程序的意义:**这道题不用交到洛谷就能自己判对错。**// 它是 special judge 题(方案不唯一),所以「和样例逐字节相同」这条尺子在这儿是错的 ——// 换一份合法答案照样能过。能判的只有「覆盖关系对不对」。
#include <bits/stdc++.h>using namespace std;
const int DX[5] = {0, -1, -1, +1, +1}; // c = 1..4:另外两格往哪儿伸const int DY[5] = {0, -1, +1, -1, +1};
int main() { int k, px, py; if (scanf("%d %d %d", &k, &px, &py) != 3) { printf("读不到 k 和公主坐标\n"); return 0; } int n = 1 << k; vector<vector<int>> g(n + 2, vector<int>(n + 2, 0));
int x, y, c, pieces = 0; while (scanf("%d %d %d", &x, &y, &c) == 3) { pieces++; if (c < 1 || c > 4) { printf("第 %d 块:c = %d 越界(spj 错误 1)\n", pieces, c); return 0; } int cell[3][2] = {{x, y}, {x + DX[c], y}, {x, y + DY[c]}}; for (auto& q : cell) { int a = q[0], b = q[1]; if (a < 1 || a > n || b < 1 || b > n) { printf("第 %d 块:格子 (%d,%d) 越界(spj 错误 2)\n", pieces, a, b); return 0; } if (a == px && b == py) { printf("第 %d 块:盖住了公主 (%d,%d)(spj 错误 3)\n", pieces, px, py); return 0; } if (g[a][b]) { printf("第 %d 块:格子 (%d,%d) 被盖了两次(spj 错误 3)\n", pieces, a, b); return 0; } g[a][b] = 1; } } for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) if (!g[i][j] && !(i == px && j == py)) { printf("格子 (%d,%d) 从没被盖过(spj 错误 4)\n", i, j); return 0; } printf("合法:%d x %d 的 %d 格恰好各盖一次,公主 (%d,%d) 没被盖,一共 %d 块毯子\n", n, n, n * n - 1, px, py, pieces); return 0;}点「运行 ▶」看结果
5第 ① 版:分治一个字没错,编号猜成了「顺时针」
最容易顺手写出来的编号是顺时针:1 左上 → 2 右上 → 3 右下 → 4 左下。 看着比「先上下、再左右」自然多了 —— 它和正确答案只差把 3 和 4 换了个个儿。
// 洛谷 P1228 —— 第 ① 版:分治一个字都没错,**编号是猜的**(所以全错)//// 输入:第一行 k(迷宫是 2^k x 2^k,0 < k <= 10);第二行 x y(公主所在的行、列)// 输出:每行 "x y c" —— 一块毯子的拐角坐标,和它的形状编号//// 分治的形状(和 P5461 同源):// 把正方形切成四块,公主在其中一块里;// 在**正中心**放一块毯子,盖住另外三块各自靠中心的那一格 ——// 于是那三块也「各有一个已经被占住的格子」,和公主那一块变成了同一个问题。// ⇒ 四块各自递归,边长 1 时停。//// ★ 编号 c 不是猜的,是 p1228Derive.cpp 拿原站样例反推出来的(24 选 1,唯一):// c = 1 缺口在左上 c = 2 缺口在右上 c = 3 缺口在左下 c = 4 缺口在右下// 而「缺口」正好就是公主/已占格所在的那个象限 ⇒ **c 就是象限编号**,一行都不用记。//// ⚠ 输出量:k = 10 时是 (1024*1024 - 1) / 3 = 349525 行 ——// 和 P5461 一样,这道题「打得快」和「算得快」都要管,所以关了同步、不用 endl。
#include <bits/stdc++.h>using namespace std;
/** * 铺满左上角为 (r,c)、边长 len 的正方形,其中 (hr,hc) 这一格已经被占住(公主或上一块毯子)。 */void solve(int r, int c, int len, int hr, int hc) { if (len == 1) return; int h = len / 2; int mr = r + h - 1, mc = c + h - 1; // 中心那 2x2 的左上格
bool top = (hr <= mr), left = (hc <= mc); // 已占格落在哪个象限 // ⚠ 这一版把编号猜成了「顺时针」:1 左上、2 右上、3 右下、4 左下。 // 看着比「先上下、再左右」更自然 —— 而它把 3 和 4 换了个个儿。 int quad = top ? (left ? 1 : 2) : (left ? 4 : 3);
// 毯子的拐角 = 中心 2x2 里、和「已占格那个象限」对角的那一格 int cx = top ? mr + 1 : mr; int cy = left ? mc + 1 : mc; printf("%d %d %d\n", cx, cy, quad);
// 四个象限各自递归:已占格那一块用 (hr,hc),另外三块用刚被这块毯子盖住的那一格 solve(r, c, h, top && left ? hr : mr, top && left ? hc : mc); solve(r, c + h, h, top && !left ? hr : mr, top && !left ? hc : mc + 1); solve(r + h, c, h, !top && left ? hr : mr + 1, !top && left ? hc : mc); solve(r + h, c + h, h, !top && !left? hr : mr + 1, !top && !left? hc : mc + 1);}
int main() { int k, x, y; if (scanf("%d %d %d", &k, &x, &y) != 3) return 0; solve(1, 1, 1 << k, x, y); return 0;}点「运行 ▶」看结果
把它的输出喂给上一步那把尺子,第 3 块就被判死:
第 3 块:格子 (2,1) 被盖了两次(spj 错误 3)
它有三个特点,凑在一起就是「最难查」的那一类:
- 算法完全正确 —— 分治、递归、边界一个字都不用改;
- 自己看输出看不出来 —— 每行都是三个合法的数,格式挑不出毛病;
- 样例也未必挡得住 —— 第一行还是对的;而
k = 1且公主在第一行时(1 1/1 2),错版和正解逐字节相同(实测,两条都写成断言了)。
⇒ 唯一能救你的,就是第 ④ 步那把尺子:写一份把答案还原回棋盘的自检程序。 输出是「方案」而不是「一个数」的题,都该这么干。
6第 ② 版:正解(编号照反推来的那份)
// 洛谷 P1228 地毯填补问题 —— 正解:分治,每次在中心放一块毯子//// 输入:第一行 k(迷宫是 2^k x 2^k,0 < k <= 10);第二行 x y(公主所在的行、列)// 输出:每行 "x y c" —— 一块毯子的拐角坐标,和它的形状编号//// 分治的形状(和 P5461 同源):// 把正方形切成四块,公主在其中一块里;// 在**正中心**放一块毯子,盖住另外三块各自靠中心的那一格 ——// 于是那三块也「各有一个已经被占住的格子」,和公主那一块变成了同一个问题。// ⇒ 四块各自递归,边长 1 时停。//// ★ 编号 c 不是猜的,是 p1228Derive.cpp 拿原站样例反推出来的(24 选 1,唯一):// c = 1 缺口在左上 c = 2 缺口在右上 c = 3 缺口在左下 c = 4 缺口在右下// 而「缺口」正好就是公主/已占格所在的那个象限 ⇒ **c 就是象限编号**,一行都不用记。//// ⚠ 输出量:k = 10 时是 (1024*1024 - 1) / 3 = 349525 行 ——// 和 P5461 一样,这道题「打得快」和「算得快」都要管,所以关了同步、不用 endl。
#include <bits/stdc++.h>using namespace std;
/** * 铺满左上角为 (r,c)、边长 len 的正方形,其中 (hr,hc) 这一格已经被占住(公主或上一块毯子)。 */void solve(int r, int c, int len, int hr, int hc) { if (len == 1) return; int h = len / 2; int mr = r + h - 1, mc = c + h - 1; // 中心那 2x2 的左上格
bool top = (hr <= mr), left = (hc <= mc); // 已占格落在哪个象限 int quad = (top ? 0 : 2) + (left ? 0 : 1) + 1; // 1 左上 2 右上 3 左下 4 右下
// 毯子的拐角 = 中心 2x2 里、和「已占格那个象限」对角的那一格 int cx = top ? mr + 1 : mr; int cy = left ? mc + 1 : mc; printf("%d %d %d\n", cx, cy, quad);
// 四个象限各自递归:已占格那一块用 (hr,hc),另外三块用刚被这块毯子盖住的那一格 solve(r, c, h, top && left ? hr : mr, top && left ? hc : mc); solve(r, c + h, h, top && !left ? hr : mr, top && !left ? hc : mc + 1); solve(r + h, c, h, !top && left ? hr : mr + 1, !top && left ? hc : mc); solve(r + h, c + h, h, !top && !left? hr : mr + 1, !top && !left? hc : mc + 1);}
int main() { int k, x, y; if (scanf("%d %d %d", &k, &x, &y) != 3) return 0; solve(1, 1, 1 << k, x, y); return 0;}点「运行 ▶」看结果
三条值得看的证据:
① k = 3、公主 (3,3) -> 和原站样例逐字节相同 (编号反推对了)
② k = 1..10 的每一组 -> 自检程序全部报「合法」 (算法对了)
③ k = 10 -> 349525 行,1048575 格恰好各盖一次
这道题的正解不必和样例一致 —— 我们这份正好一致,是因为递归的顺序碰巧和出题人一样。
⇒ 所以别把它当验收标准:验收标准是第 ④ 步那把尺子(覆盖关系), 而「碰巧和样例一样」只是一条额外的旁证。
7还剩最后一关:349525 行的输出
k = 10 时要打 349 525 行。和 P5461 是同一课:
「算得快」和「打得快」是两件事。
这一版用的是 printf;换成 cout << ... << endl 的话,endl 每行都强制刷一次缓冲,
输出量大的时候能单独把你卡掉。
⇒ 两条写法二选一,别混:全 printf,或者 全 cout + ios::sync_with_stdio(false) 且不用 endl。
8回头看:这道题在教什么
- ★★ 题面里「见图」的部分,常常能从样例反推出来。 这道题的四个编号是 24 选 1, 而样例把它锁死了 —— 不确定的东西,先想想有没有办法把它解出来,再考虑猜。
- ★ 输出是「方案」的题,第一件事是写自检程序。 方案题多半是 spj, 「和样例比」这条尺子失效;能判的只有覆盖 / 合法性,而那个程序通常十几行。
- 分治的关键是「把不能递归的变成能递归的」。 中心那一块毯子什么都没算, 它只干一件事:给另外三块各制造一个缺口。