B4532 信息与未来 2026 折纸状态维护

折纸:只盯住一格

这节课不模拟整张纸。我们只维护目标格子的当前位置、层号、每叠层数和朝向,用 5-10 分钟把“翻折反转、保留加压”讲清楚。

(1,1)
目标格子是否被翻折?

一、先回答问题

一句话模型

每次只判断左上角那个格子在被翻折的一半,还是留在原地的一半。

它被翻折:这一小叠上下顺序反转,正反面翻转。它没被翻折:另一半会压到它上面,层号加上一整叠。

不要做什么

不要开数组模拟全部格子。最大边长是 215,最终层数是 230,完整模拟没有必要。

我们维护 5 个量:h, w, r, c, layer, sz, up。其中 sz 是当前一个位置叠了多少层。

二、核心观察

1. 位置会缩小

纵向折叠后高度减半,横向折叠后宽度减半。目标格子的 r,c 要映射到新的纸面坐标。

2. 被翻折

如果目标在折过去的那半边:layer = sz - layer + 1,并且 up = !up

3. 被压住

如果目标留在原地那半边:翻过来的整叠在上方,目标层号变为 layer += sz

四个方向怎么判
  • D:上半向下。目标在上半就是被翻折;在下半就是被压住。
  • U:下半向上。目标在下半就是被翻折;在上半就是被压住。
  • R:左半向右。目标在左半就是被翻折;在右半就是被压住。
  • L:右半向左。目标在右半就是被翻折;在左半就是被压住。

三、互动演示:样例 DR / DRUR

当前纸面 准备开始
目标位置 (0, 0)
当前尺寸 4 x 4
目标层号 1
朝向 U
每格层数 sz 1
已处理 0 / 4
本步解释 点击“下一折”,观察目标格子是否位于被翻折的一半。
课堂读法:先问“目标在哪半边”,再决定是 layer 反转还是 layer 加 sz。不要先想整张纸最终长什么样。

四、自己选答案

问题 1:当前每格有 sz=8 层,目标层号是 3。如果目标格子被翻折,新层号是多少?

问题 2:做一次 L 折,目标在左半边。它是被翻折,还是被压住?

问题 3:为什么每次处理完都要 sz *= 2

五、手推 DRUR

步骤 方向 目标判断 层号与朝向 尺寸 / 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

六、C++14 参考代码

本地 tok-* 高亮,无 CDN
#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 时真的模拟每一格、每一层。

1. 快速程序

使用本题正式解法:维护 r,c,layer,sz,up,每次 O(1) 更新。

2. 暴力程序

小数据里把每个位置看成一个栈,真的执行翻折、反转和覆盖。

3. 随机造数据

随机选小 n,生成 n 个纵向折叠和 n 个横向折叠,再打乱。

4. 比较答案

两个答案不同就打印输入。这个输入就是最有价值的调试样例。

本题暴力怎么想

vector<Cell> 表示一个位置从上到下的所有层。折过去的一格要先把栈反过来,每层朝向取反,再放到保留半边对应格子的上面。

最后整张纸只剩 1 x 1,在唯一的栈里找到原始坐标 (0,0),它的位置和朝向就是暴力答案。

随机数据怎么合法

题目要求长度为 2n,且恰好 n 次纵向折叠、n 次横向折叠。不能完全随机选字符。

做法:先放入 n 个 D/U 随机字符,再放入 n 个 L/R 随机字符,最后用 shuffle 打乱顺序。

C++14 单文件对拍骨架
#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 的“被翻折还是被压住”。