栈式线性扫描:只统计实际进入的『常数→n』循环层数并取最大作指数,死区不贡献但语法照查,输出 Yes/No/ERR。
OJ: roj
题目 ID: 3649
难度:普及+/提高-
标签:模拟栈推导python
创建: 2026-10-02 12:46
更新: 2026-10-10 11:30
形式化题目
每个程序是一棵循环嵌套树,语句按书写顺序执行:
F i x y打开一层循环并新建变量(不可与未销毁的变量重名): 从 起,满足 则执行一轮,随后 加 1,直到 退出; E关闭一层并销毁该层变量。- 边界
各自为小于 100 的正整数或 ,且 远大于 100; 是必须保留的规模变量,不能当常数。 - 每个程序附带一个声称的复杂度:
O(1)或O(n^w)(为小于 100 的正整数)。 - 判定三态:程序含语法错误——①
F与E不匹配、② 新建变量与未销毁变量重名——则输出ERR,即使错误位于不会执行的循环体内也算错;否则计算实际复杂度,与声称一致输出Yes,不一致输出No。
题面 8 个样例的输出为 Yes Yes ERR Yes No Yes Yes ERR:样例 1 的循环只跑 1 次,是常数复杂度;样例 2 单层“常数 F 没有 E,语法错误;样例 4 是二重嵌套的 x。逐句轨迹见下文思路。
正解
思路
朴素做法是把循环真的执行一遍、数出步数再写成
第一步:单层循环只有四种可能。 把
| 是否进入 | 实际次数 | 复杂度贡献 | ||
|---|---|---|---|---|
| 常数 | 常数( |
进入 | 常数次 | |
| 常数 | 进入 | 乘上一个 |
||
| 任意常数 | 不进入(初值已 |
体内语句全不执行 | ||
| 进入 | 恰好 |
四种情况穷尽了
第二步:嵌套加一,顺序取最大。 嵌套循环的次数是内外相乘,对应指数相加,所以维护 depth 记录当前路径上已进入的"常数 best = max(depth),实际复杂度就是
第三步:死区剪枝。 若某层循环一次都不进入(上表第三种),整个循环体都不执行,体内任何"常数 dead 布尔量沿嵌套向下传递:dead = 父层 dead 或 x > y。比较时把
第四步:语法检查与执行无关。 题面明确死代码里的错误也要 ERR,因此查重、配对从不看 dead:F 先查变量是否在存活集合中,在则错误②;然后压栈保存 (depth, 变量名, dead),再更新统计量。E 若遇到空栈则错误①,否则弹栈恢复三样状态、把变量移出存活集合(销毁后名字可复用)。读完后 err 为真或栈非空(F 未闭合)即 ERR;否则拿 best 与声称的指数比较(O(1) 即指数 0)输出 Yes / No。
下面用两个样例展示 depth / best / dead 随语句的演化(dead=是 时该层对 depth 无贡献,但压栈、弹栈照常发生):
| 程序 | 语句 | 关键动作 | depth | best | 说明 |
|---|---|---|---|---|---|
| 样例 6 | F x 9 n |
压栈;常数 depth |
1 | 1 | 正常进入,乘上 |
| 样例 6 | F y n 4 |
压栈;dead,不加 |
1 | 1 | 内层一次不进 |
| 样例 6 | E |
弹栈,恢复 depth=1、dead=否 |
1 | 1 | 死区结束 |
| 样例 6 | E |
弹栈,恢复 depth=0 |
0 | 1 | 栈空,扫描结束 |
| 样例 7 | F y n 4 |
压栈;dead |
0 | 0 | 最外层就进不去 |
| 样例 7 | F x 9 n |
压栈;父层已 dead,不加 |
0 | 0 | 本可 |
| 样例 7 | E |
弹栈,恢复外层状态 | 0 | 0 | 语法照常配对 |
| 样例 7 | E |
弹栈,恢复 depth=0 |
0 | 0 | 栈空,扫描结束 |
两段的 best 分别为 1 和 0,对应实际复杂度 Yes。对比可见 dead 的作用域差别:样例 6 死在内层,外层的一次 ERR。
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-10 11:30
* update_at: 2026-10-10 11:30
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N_SCALE=1000000;
int bound_val(const string &s){return s=="n"?N_SCALE:atoi(s.c_str());}
string run_program(int lines,string claim, vector<string>&tok, int &pos){ set<string> alive; vector<pair<pair<int,string>, bool> > st; int depth=0,best=0; bool parent_dead=false, err=false; for(int line=0;line<lines;line++){string op=tok[pos++]; if(op=="F"){string var=tok[pos++],x=tok[pos++],y=tok[pos++]; if(err) continue; if(alive.count(var)){err=true; continue;} st.push_back({{depth,var},parent_dead}); bool dead=parent_dead || bound_val(x)>bound_val(y); if(!dead && x!="n" && y=="n"){depth++; best=max(best,depth);} parent_dead=dead; alive.insert(var);} else if(!err){ if(st.empty()){err=true; continue;} depth=st.back().first.first; string var=st.back().first.second; parent_dead=st.back().second; st.pop_back(); alive.erase(var);} } if(err||!st.empty()) return "ERR"; int expected=0; if(claim!="O(1)") expected=atoi(claim.substr(4, claim.size()-5).c_str()); return best==expected?"Yes":"No";}
int main(){ios::sync_with_stdio(false);cin.tie(nullptr); vector<string> tok; string s; while(cin>>s) tok.push_back(s); if(tok.empty()) return 0; int pos=0,T=atoi(tok[pos++].c_str()); for(int i=0;i<T;i++){int l=atoi(tok[pos++].c_str()); string claim=tok[pos++]; cout<<run_program(l,claim,tok,pos)<<'\n';} return 0;}#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-02 12:46
# update_at: 2026-10-02 12:46
import sys
from collections.abc import Iterator
N_SCALE = 10**6 # n 的占位值:n 远大于 100,而正整数边界都小于 100
def bound(token: bytes) -> int:
"""把循环边界换成可比较的整数:n 取大占位值,正整数取原值。"""
return N_SCALE if token == b"n" else int(token)
def run_program(lines: int, claim: bytes, it: Iterator[bytes]) -> str:
"""按栈语义执行一个程序,比对实际复杂度与声称复杂度,返回 Yes/No/ERR。"""
alive: set[bytes] = set() # 尚未销毁的循环变量
stack: list[tuple[int, bytes, bool]] = [] # (入栈前层数, 变量名, 入栈前父层是否必不进入)
depth = 0 # 当前已进入的 "常数→n" 循环层数
best = 0 # 历史最大层数,即实际复杂度 O(n^best)
parent_dead = False # 外层必然一次不进:内层一律不贡献复杂度
err = False
for _ in range(lines):
tok = next(it)
if tok == b"F":
var, x, y = next(it), next(it), next(it) # 无论是否已出错都要吃掉整行
if err:
continue
if var in alive: # 语法错误②:与存活变量重名
err = True
continue
stack.append((depth, var, parent_dead))
dead = parent_dead or bound(x) > bound(y) # 初始值已大于上界:一次都不进
if not dead and x != b"n" and y == b"n": # 只有 "常数 → n" 贡献一次幂
depth += 1
best = max(best, depth)
parent_dead = dead
alive.add(var)
elif not err: # E
if not stack: # 语法错误①:E 没有匹配的 F
err = True
continue
depth, var, parent_dead = stack.pop() # 弹栈恢复外层状态
alive.discard(var)
if err or stack:
return "ERR"
expected = 0 if claim == b"O(1)" else int(claim[4:-1]) # 声称的 O(n^w) 指数 w
return "Yes" if best == expected else "No"
def solve() -> None:
data = iter(sys.stdin.buffer.read().split())
T = int(next(data))
print("\n".join(run_program(int(next(data)), next(data), data) for _ in range(T)))
if __name__ == "__main__":
solve()复杂度
- 时间复杂度:
,每个程序逐行扫描一次、每行 ,全程绝不执行循环本身; 、 。 - 空间复杂度:
,状态栈最深 层,存活变量集合至多 个元素。
总结
- 把"执行循环数步数"改写成"分析结构数层数":
无具体值、步数指数级,但复杂度类别是静态可算的。 - 单层只有四种可能,只有"常数
"贡献一次幂; 初值对常数上界一次不进, 对 恰跑一次。 - 嵌套
指数相加用 depth,顺序取最大用 best,死区向内传递用dead——三个量一趟扫描全部维护。 - 语法检查独立于执行:存活集合查重、栈查配对,死代码里的错误同样
ERR;三态判断按ERR→Yes/No的优先级输出。