每次只判断左上角那个格子在被翻折的一半,还是留在原地的一半。
它被翻折:这一小叠上下顺序反转,正反面翻转。它没被翻折:另一半会压到它上面,层号加上一整叠。
这节课不模拟整张纸。我们只维护目标格子的当前位置、层号、每叠层数和朝向,用 5-10 分钟把“翻折反转、保留加压”讲清楚。
每次只判断左上角那个格子在被翻折的一半,还是留在原地的一半。
它被翻折:这一小叠上下顺序反转,正反面翻转。它没被翻折:另一半会压到它上面,层号加上一整叠。
不要开数组模拟全部格子。最大边长是 215,最终层数是 230,完整模拟没有必要。
我们维护 5 个量:h, w, r, c, layer, sz, up。其中 sz 是当前一个位置叠了多少层。
纵向折叠后高度减半,横向折叠后宽度减半。目标格子的 r,c 要映射到新的纸面坐标。
如果目标在折过去的那半边:layer = sz - layer + 1,并且 up = !up。
如果目标留在原地那半边:翻过来的整叠在上方,目标层号变为 layer += sz。
问题 1:当前每格有 sz=8 层,目标层号是 3。如果目标格子被翻折,新层号是多少?
问题 2:做一次 L 折,目标在左半边。它是被翻折,还是被压住?
问题 3:为什么每次处理完都要 sz *= 2?
| 步骤 | 方向 | 目标判断 | 层号与朝向 | 尺寸 / sz |
|---|---|---|---|---|
| 初始 | - | 左上角,位置 (0,0) | layer=1,U | 4x4,sz=1 |
| 1 | D | 在上半,被翻折 | 1 反转仍为 1,朝向变 D | 2x4,sz=2 |
| 2 | R | 在左半,被翻折 | layer = 2 - 1 + 1 = 2,朝向变 U | 2x2,sz=4 |
| 3 | U | 在下半,被翻折 | layer = 4 - 2 + 1 = 3,朝向变 D | 1x2,sz=8 |
| 4 | R | 在右半,被压住 | layer = 3 + 8 = 11,朝向仍 D | 1x1,sz=16 |
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
string s;
cin >> n >> s;
ll h = 1LL << n, w = 1LL << n;
ll r = 0, c = 0; // 目标格子当前坐标,0-based
ll layer = 1; // 从上往下数的层号
ll sz = 1; // 当前每个位置有多少层
bool up = true; // 正面是否朝上
for (char ch : s) {
if (ch == 'D') {
ll half = h / 2;
if (r < half) { // 上半被翻到下半上方
r = half - 1 - r;
layer = sz - layer + 1;
up = !up;
} else {
r -= half;
layer += sz;
}
h = half;
} else if (ch == 'U') {
ll half = h / 2;
if (r >= half) { // 下半被翻到上半上方
r = h - 1 - r;
layer = sz - layer + 1;
up = !up;
} else {
layer += sz;
}
h = half;
} else if (ch == 'R') {
ll half = w / 2;
if (c < half) { // 左半被翻到右半上方
c = half - 1 - c;
layer = sz - layer + 1;
up = !up;
} else {
c -= half;
layer += sz;
}
w = half;
} else {
ll half = w / 2;
if (c >= half) { // 右半被翻到左半上方
c = w - 1 - c;
layer = sz - layer + 1;
up = !up;
} else {
layer += sz;
}
w = half;
}
sz *= 2;
}
cout << layer << ' ' << (up ? 'U' : 'D') << '\n';
return 0;
}
layer = sz - layer + 1 只在“目标所在的小叠本身翻过来”时发生。long long 足够容纳最终层号,因为最大层数是 2^30。对拍就是让两个程序做同一道题,然后比较答案。一个是准备提交的快速程序,一个是只在小数据上运行的暴力程序。暴力慢一点没关系,关键是写法直接、容易相信。
这道折纸题特别适合第一次学对拍,因为快速做法只维护状态,而暴力做法可以在 n <= 4 时真的模拟每一格、每一层。
使用本题正式解法:维护 r,c,layer,sz,up,每次 O(1) 更新。
小数据里把每个位置看成一个栈,真的执行翻折、反转和覆盖。
随机选小 n,生成 n 个纵向折叠和 n 个横向折叠,再打乱。
两个答案不同就打印输入。这个输入就是最有价值的调试样例。
用 vector<Cell> 表示一个位置从上到下的所有层。折过去的一格要先把栈反过来,每层朝向取反,再放到保留半边对应格子的上面。
最后整张纸只剩 1 x 1,在唯一的栈里找到原始坐标 (0,0),它的位置和朝向就是暴力答案。
题目要求长度为 2n,且恰好 n 次纵向折叠、n 次横向折叠。不能完全随机选字符。
做法:先放入 n 个 D/U 随机字符,再放入 n 个 L/R 随机字符,最后用 shuffle 打乱顺序。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
struct Ans {
ll layer;
char face;
bool operator!=(const Ans& other) const {
return layer != other.layer || face != other.face;
}
};
struct Cell {
int r, c;
bool up;
};
vector<Cell> flipped(vector<Cell> v) {
reverse(v.begin(), v.end());
for (auto& x : v) x.up = !x.up;
return v;
}
void putOnTop(vector<Cell>& dst, vector<Cell> top) {
top.insert(top.end(), dst.begin(), dst.end());
dst.swap(top);
}
Ans fastSolve(int n, const string& s) {
ll h = 1LL << n, w = 1LL << n;
ll r = 0, c = 0, layer = 1, sz = 1;
bool up = true;
for (char ch : s) {
if (ch == 'D') {
ll half = h / 2;
if (r < half) r = half - 1 - r, layer = sz - layer + 1, up = !up;
else r -= half, layer += sz;
h = half;
} else if (ch == 'U') {
ll half = h / 2;
if (r >= half) r = h - 1 - r, layer = sz - layer + 1, up = !up;
else layer += sz;
h = half;
} else if (ch == 'R') {
ll half = w / 2;
if (c < half) c = half - 1 - c, layer = sz - layer + 1, up = !up;
else c -= half, layer += sz;
w = half;
} else {
ll half = w / 2;
if (c >= half) c = w - 1 - c, layer = sz - layer + 1, up = !up;
else layer += sz;
w = half;
}
sz *= 2;
}
return {layer, up ? 'U' : 'D'};
}
Ans bruteSolve(int n, const string& s) {
int h = 1 << n, w = 1 << n;
vector<vector<vector<Cell>>> a(h, vector<vector<Cell>>(w));
for (int i = 0; i < h; i++)
for (int j = 0; j < w; j++)
a[i][j].push_back({i, j, true});
for (char ch : s) {
if (ch == 'D') {
int half = h / 2;
vector<vector<vector<Cell>>> b(half, vector<vector<Cell>>(w));
for (int r = 0; r < half; r++)
for (int c = 0; c < w; c++)
b[r][c] = a[half + r][c];
for (int r = 0; r < half; r++)
for (int c = 0; c < w; c++)
putOnTop(b[half - 1 - r][c], flipped(a[r][c]));
a.swap(b); h = half;
} else if (ch == 'U') {
int half = h / 2;
vector<vector<vector<Cell>>> b(half, vector<vector<Cell>>(w));
for (int r = 0; r < half; r++)
for (int c = 0; c < w; c++)
b[r][c] = a[r][c];
for (int r = half; r < h; r++)
for (int c = 0; c < w; c++)
putOnTop(b[h - 1 - r][c], flipped(a[r][c]));
a.swap(b); h = half;
} else if (ch == 'R') {
int half = w / 2;
vector<vector<vector<Cell>>> b(h, vector<vector<Cell>>(half));
for (int r = 0; r < h; r++)
for (int c = 0; c < half; c++)
b[r][c] = a[r][half + c];
for (int r = 0; r < h; r++)
for (int c = 0; c < half; c++)
putOnTop(b[r][half - 1 - c], flipped(a[r][c]));
a.swap(b); w = half;
} else {
int half = w / 2;
vector<vector<vector<Cell>>> b(h, vector<vector<Cell>>(half));
for (int r = 0; r < h; r++)
for (int c = 0; c < half; c++)
b[r][c] = a[r][c];
for (int r = 0; r < h; r++)
for (int c = half; c < w; c++)
putOnTop(b[r][w - 1 - c], flipped(a[r][c]));
a.swap(b); w = half;
}
}
const auto& st = a[0][0];
for (int i = 0; i < (int)st.size(); i++) {
if (st[i].r == 0 && st[i].c == 0) {
return {i + 1, st[i].up ? 'U' : 'D'};
}
}
return {-1, '?'};
}
string randomCase(int n, mt19937& rng) {
string s;
for (int i = 0; i < n; i++) s += (rng() % 2 ? 'D' : 'U');
for (int i = 0; i < n; i++) s += (rng() % 2 ? 'R' : 'L');
shuffle(s.begin(), s.end(), rng);
return s;
}
int main() {
mt19937 rng(20260530);
for (int tc = 1; tc <= 50000; tc++) {
int n = 1 + rng() % 4;
string s = randomCase(n, rng);
Ans a = fastSolve(n, s);
Ans b = bruteSolve(n, s);
if (a != b) {
cout << "Wrong answer found!\n";
cout << n << "\n" << s << "\n";
cout << "fast: " << a.layer << ' ' << a.face << "\n";
cout << "brute: " << b.layer << ' ' << b.face << "\n";
return 0;
}
}
cout << "All tests passed.\n";
return 0;
}
目标没有被翻折不等于层号不变。另一半翻到它上面时,要 layer += sz。
D/R 中目标在保留半边时,坐标要减去 half,映射到新纸面。
只有目标格子所在的小叠被整体翻折时,朝向才会反转。
课堂节奏建议:先推 DR,再推 DRUR,最后让学生口答 L/U 的“被翻折还是被压住”。