对当前目录下 1086 份 p4.cpp 提交进行算法归类、原理剖析与最优写法推荐。
一张 2ⁿ × 2ⁿ 的正方形纸(1 ≤ n ≤ 15),初始正面朝上。进行 2n 次对折,每次沿中线把纸对半折,方向有四种:
被翻折的那一半会翻面(正反互换,原来的底层翻成顶层)。折叠串恰好含 n 个竖直折(D/U)与 n 个水平折(R/L),长度 2n。2n 次折后纸变成 1×1 的一叠,共 2²ⁿ 层。问:初始位于左上角 (1,1) 的那个格子,最终 (a) 从顶往下数是第几层(顶层 = 第 1 层),(b) 它是正面朝上还是朝下。
n=1, "DR" → 2 U。样例 2:n=2, "DRUR" → 11 D。n=15 时纸是 32768 × 32768,最终层数高达 2³⁰ ≈ 10⁹。无论是开二维网格、还是用 vector/deque 真的去翻折每一层,时间和空间都是 O(4ⁿ) 级,必然 MLE/TLE。
唯一可行的思路:只追踪 (1,1) 这一个格子。维护它在当前矩形中的位置、它在叠层中上方有多少层、以及它的朝向,每次折叠 O(1) 更新,总复杂度 O(n)。我们不需要知道别的格子去哪了。
绿=单格 O(n) 追踪(正解思路) · 蓝=整张纸模拟(小数据可对,大数据 MLE/TLE) · 黄=打表/公式/抄错题 · 灰=非有效提交 · 红=随机。分类基于代码特征自动统计,个别边界会有出入。
| 类别 | 核心思路 | 复杂度 | 正确性 / 预期得分 |
|---|---|---|---|
| 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 U、12 R 之类的"经验公式" |
O(1) | 错误:无依据,偶中样例,基本 0 分 |
| D. 打表 / 仅判样例 | if(n==1&&s=="DR") ... else ... 只对两个样例 |
O(1) | 仅样例分 |
| E. 抄错题 / 贴错代码 | 粘了 P5(旅行计划 DP)、或输出 ASCII 字符画等无关代码 | — | 0 分 |
| F. 非有效提交 | 空 main、读入即 return 0、srand 乱输出 |
— | 0 分 |
这是"诚实但不够聪明"的一类。它们真的去维护整张纸:要么用二维数组记每格朝向/层数(如某提交开 node s[30004][30004] 直接编译/内存爆掉),要么用 vector<vector<...>> 把每一叠层都存下来,折叠时 reverse + 翻面 + 拼接。
它们在 n 较小时完全正确,因此非常适合用来对拍验证正解。但层数随 n 指数增长:n=15 时总层数 ≈10⁹,光是存下来就远超内存,扫描更是天文数字——本批 26 份模拟里有 23 份开了 1e4²~4e4² 的二维数组,直接 MLE 或 CE。
近半数提交都意识到"只跟一个格子",但真正写对的不到三分之一。常见的三处 bug:
[1..H/2],D 存活下半要 r-=h,被折的格子要先 r=H-r+1 镜像。漏掉重标号会导致下一次折叠判断"在哪一半"出错,整串连锁错。这些提交没有真正处理折叠串:C 凭感觉输出公式(如 n*2 U),D 只 if 两个样例,E 干脆贴了 P5 的 DP 或字符画代码,F 是空壳或 rand()。它们最多骗到样例分。
只维护四个量,每次折叠 O(1) 更新,全程 O(n)、O(1) 空间,n=15 实测 <1ms:
H, W:当前矩形的高与宽(从 2ⁿ 不断减半)。r, c:目标格子在当前矩形内的 1 下标位置。above:目标格子上方的层数(答案 = above + 1)。total:目标格子所在叠的总层数(每折一次翻倍)。faceUp:是否正面朝上。以 D(上半向下盖)为例,设 h = H/2:
r ≤ h(目标在移动的上半):它翻面 → faceUp = !faceUp;它在自己叠中倒序 → above = total-1-above;它落到下半之上,本身上方不再增加;行镜像 r = H-r+1。r > h(目标在静止的下半):对侧整叠(total 层)盖在它上方 → above += total;朝向、叠内顺序不变。total *= 2,再把存活的下半重标号到 [1..h](r -= h 后归一),H = h。U / L / R 完全对称:U 镜像同理但存活上半;R/L 把上述对行的操作换成对列。下面的参考实现用统一的归一化处理了四种方向。
#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。注意 total 在 n=15 可达 2³⁰,above 同量级,建议统一用 long long 防溢出。
结论:第 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) 状态机。算法本身极短,真正的工程难点是把"翻面 + 倒序 + 谁压谁 + 坐标重标号"这四件事在每个方向上都写对。
2ⁿ×2ⁿ 数组或真存所有层,n=15 必崩——只能追踪单格。faceUp 取反,又要 above = total-1-above 倒置叠内顺序,二者必须同时做。above += total,移动半的格子上方不增。判反会让层号严重偏离。[1..H/2],下一次折叠的"在哪一半"判断连锁出错。above+1,别错算成"从底数"。total、above 在 n=15 达 ~10⁹,必须 long long。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;
}
k=当前总层数、c=本格从上往下的层号、isup=正反面、isinup/isinleft=本格当前落在折叠的哪一半。s[i]==(isinup?'D':'U') 一句话判断"本格所在的半边是否正是被翻折的那一半":是→本格被翻到顶部,层号镜像 c=k-c+1 且正反面翻转;否→另一半叠在我们上方,c+=k。逻辑对称、零分支冗余。int 范围内(<2.1×10⁹)但已偏紧,用 long long 更稳妥;再补两行注释说明 c=k-c+1 的镜像含义会更利于阅读。结论:在"只追踪一个格子"这一正确方向上,把状态压到了极致而又不失可读性,是本题的教科书级写法。
#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) 更新层号与正反面,而不是一遇到难点就打表。哪怕只写出"纵向/横向各折一次"的小情形,也比固定输出强。
#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 + 折叠串,输出两个值。养成"先用样例手验输入输出形态"的习惯,能避免整题贴错。