题解:P3952 [NOIP 2017 提高组] 时间复杂度

· · 题解

题目简述

A++ 语言的循环语句格式如下:

F i x y
  ...
E

其与 C++ 中的 for (int i = x; i <= y; i++) {...} 等价。

给定 tL 条只有循环语句的程序,判断其中是否出现了语法错误,如果没出现则判断这个程序的时间复杂度是否与给定的时间复杂度相等。

宏定义与变量(具体含义后文给出)

#define endl '\n'
typedef long long ll;
const int L = 100 + 10;
typedef pair<int, int> pii;
#define rep(i, a, b) for (int i = a; i <= b; i++)
#define repr(i, a, b) for (int i = a; i >= b; i--)
// ----------------------------
struct node {
    int depth;
    string start, _end;
    char val_name, type;
};
// ----------------------------
int l, w;
node code[L]; 
char is_use[30];
stack<pii> last_w;
// ----------------------------
int to_int(string s) {
    int ans = 0;
    rep(i, 0, (int)s.length() - 1) ans = ans * 10 + s[i] - '0';
    return ans;
}

题目分析

没有什么算法,但让人一看题目是依托,一写憋半天——

大模拟(打磨你)

输入处理

整个输入中唯一需要稍微处理一下的只有时间复杂度了,string 是个好东西(STL 大法好),可以用 string 读入。时间复杂度只有两种情况:O(1)O(n^w),用 w 来记录 O(n^{w}),第一种情况设为 0;而第二种为了方便,可以先调用 .pop_back() 函数,弹掉右括号,接着调用 .substr(4),表示从字符串的第 4 + 1 个字符开始一直截取到最后,将这个函数返回的字符串转成数字即可。

在输入语句时,由于可能出现语法错误,但后面的输入还要继续,后续的处理比较麻烦,所以这里采用了全部读入再处理的方法,就有了宏定义中的结构体 node。除 depth 以外的变量含义较为清晰,depth 的含义为这个循环在第几层循环内,最外层则为 0

void input() {
    cin >> l;
    w = 0;
    string s;
    int depth = -1;
    cin >> s; s.pop_back();
    if (s != "O(1") w = to_int(s.substr(4));
    rep(i, 1, l) {
        cin >> code[i].type;
        if (code[i].type == 'F') {
            depth++;
            cin >> code[i].val_name >> code[i].start >> code[i]._end;
        }
        else depth--;
        code[i].depth = depth;
    }
}

错误判断

  1. FE 不匹配:这个问题与括号匹配几乎相同,可以用栈或者两个计数变量都可以,此代码中使用栈因为可以同时记录另一种数据。(栈 stack<pii> last_w

  2. 变量名重复:这就更简单了,只需要一个 bool 数组即可。(数组 is_use[30]

时间复杂度计算

void solve() {
  // do!
}

使用的变量及含义:

int now_w = 0;
/*
如果第 i 条语句的类型为 F,那么 now_w 表示只看当前语句所在的一整个大代码块里,运行它前面的未结束循环和自己所用的时间复杂度为 O(n^now_w)。
如果为 E,则 now_w 的值与计算到该语句对应的 F 语句的上一层时的 now_w 的值相同,如果对应的层已是最外层,则为 0。
例如代码为
F i 1 n
F j 1 n
F k 1 n
E
F k 1 n
E
E
E
那么在结束 i=2,4,6 的循环时 now_w=2;在结束 i=3,5 时 now_w=3。
*/
int max_w = 0;
// 最大的记录过的时间复杂度中 n 的指数,也可以说这个程序的时间复杂度就是 O(n^max_w)。
int now_depth = 0;
// 表示当前处于第几层循环,如果有循环不会执行,则计算到此循环内时 now_depth 不会改变。
bool is_use[30];
// 表示现在每个小写字母(变量名)是否存在且未被销毁。(a 的下标为 0)
stack<pii> last_w;
/*
first 表示第 second 条语句的时间复杂度为多少,只计算这一个语句。(第 second 条语句类型必定是 F)
并且这个栈还正好可以判断 F 与 E 不匹配的错误,当这个语句的类型为 E 但栈已空则为错误,代码全部运行完后栈不为空也为错误。
*/

伪代码(主体部分)

$\ \ \ \ \ \ \ \ \scriptsize \square \ $ 输出 `ERR` 并结束函数。 $\ \ \ \ \circ \ $ 判断当前语句类型是否为 `E`。 $\ \ \ \ \ \ \ \ \scriptsize \square \ $ 将 `now_depth` 减少 $1$,将 `now_w` 减少当前栈顶的 `first`,销毁第当前栈顶的 `second` 条语句的循环变量,弹出栈顶,注意还要判断 `now_depth` 是否会小于 $0$,具体原因会在后面给出。最后 `continue` 掉。 $\ \ \ \ \circ \ $ 现在只剩下代码类型为 `F` 的情况,所以先判断一下是否出现错误,即只有循环变量重复。 $\ \ \ \ \ \ \ \ \scriptsize \square \ $ 输出 `ERR` 并结束函数。 $\ \ \ \ \circ \ $ 此时 `now_depth` 和结构体中 `depth` 的作用就显现出来了:由于可能出现某个循环根本进不去的情况,该循环内部的循环也就不会执行,但仍然存在,所以在当前循环根本进不去的时候就不更新 `now_depth`,随后在这里判断如果 `now_depth` 小于 `code[i].depth`,表示程序根本没有走到这里,本条语句也就不用执行。 $\ \ \ \ \ \ \ \ \scriptsize \square \ $ 就算没有运行到这,但题面输出格式的最后一句话:“注意:即使在程序不会执行的循环体中出现了语法错误也会编译错误,要输出 `ERR`。”也就告诉我们即使不执行也会出现一个相对应的 `E`,而出现 `E` 就会弹出栈顶,所以要多压入一个 `make_pair(0, i)` 占位。最后 `continue` 掉。 $\ \ \ \ \circ \ $ 判断这个循环语句的时间复杂度是否为 $O(1)$,即 $x$ 与 $y$ 都为 $n$ 或者 $x$ 与 $y$ 都为数字且 $x \le y$。 $\ \ \ \ \ \ \ \ \scriptsize \square \ $ 任何数乘 $1$ 还是这个数,所以 `now_w` 保持不变,由于进入了这个循环,所以 `now_depth` 增加 $1$,接着往栈内压入 `make_pair(0, i)`。 $\ \ \ \ \circ \ $ 如果不是 $O(1)$,判断这个循环语句的时间复杂度是否为 $O(n)$,即 $x$ 为数字且 $y$ 为 $n$。 $\ \ \ \ \ \ \ \ \scriptsize \square \ $ 此时 `now_w` 和 `now_depth` 就都需要增加 $1$ 了,并且栈内也应该压入的是 `make_pair(1, i)`。 $\ \ \ \ \circ \ $ 题目中已经描述一个语句的时间复杂度只可能是前两种,如果都不符合。 $\ \ \ \ \ \ \ \ \scriptsize \square \ $ 如果都不符合,只能是无法进入,但是还是前面的原因,栈内要压入 `make_pair(0, i)` 占位。这里就涉及到前面要判断 `now_depth` 小于 $0$ 的问题了,还是输出描述最后一句的问题,为了保证这个变量的正确性,所以这里不做改变,但仍然有对应的 `E` 要减去,为了防止 `now_depth` 小于 $0$,所以需要增加这样一个判断。 $\ \ \ \ \circ \ $ 循环内的最后一步,更新 `max_w`。 - `F` 与 `E` 无法匹配前文提到了有两种情况,第二种则是语句全部运行完后栈不为空,在此处判断。 $\ \ \ \ \circ \ $ 如果是这样,输出 `ERR` 并结束函数。 - 函数的最后,所有错误已经判断完毕,判断 `max_w` 是否与在输入部分就计算好的 `w` 相等。 $\ \ \ \ \circ \ $ 如果相等输出 `Yes`。 - 否则 $\ \ \ \ \circ \ $ 否则输出 `No`。 ## AC Code ```cpp #include<stack> #include<cstdio> #include<cctype> #include<cstring> #include<iostream> #include<algorithm> using namespace std; #define endl '\n' typedef long long ll; const int L = 100 + 10; typedef pair<int, int> pii; #define rep(i, a, b) for (int i = a; i <= b; i++) #define repr(i, a, b) for (int i = a; i >= b; i--) // ---------------------------- struct node { int depth; string start, _end; char val_name, type; }; // ---------------------------- int l, w; node code[L]; char is_use[30]; stack<pii> last_w; // ---------------------------- int to_int(string s) { int ans = 0; rep(i, 0, (int)s.length() - 1) ans = ans * 10 + s[i] - '0'; return ans; } void input() { cin >> l; w = 0; string s; int depth = -1; cin >> s; s.pop_back(); if (s != "O(1") w = to_int(s.substr(4)); rep(i, 1, l) { cin >> code[i].type; if (code[i].type == 'F') { depth++; cin >> code[i].val_name >> code[i].start >> code[i]._end; } else depth--; code[i].depth = depth; } } void solve() { input(); memset(is_use, 0, sizeof is_use); while (!last_w.empty()) last_w.pop(); int now_w = 0, max_w = 0, now_depth = 0; rep(i, 1, l) { if (code[i].type == 'E' && last_w.empty()) { cout << "ERR" << endl; return; } if (code[i].type == 'E') { now_depth--; now_depth = max(now_depth, 0); now_w -= last_w.top().first; is_use[code[last_w.top().second].val_name - 'a'] = false; last_w.pop(); continue; } if (is_use[code[i].val_name - 'a']) { cout << "ERR" << endl; return; } is_use[code[i].val_name - 'a'] = true; if (now_depth < code[i].depth) { last_w.push(make_pair(0, i)); continue; } if (code[i].start == "n" && code[i]._end == "n" || isdigit(code[i].start[0]) && isdigit(code[i]._end[0]) && to_int(code[i].start) <= to_int(code[i]._end)) { now_depth++; last_w.push(make_pair(0, i)); } else if (code[i].start != "n" && code[i]._end == "n") { now_w++; now_depth++; last_w.push(make_pair(1, i)); } else last_w.push(make_pair(0, i)); max_w = max(max_w, now_w); } if (!last_w.empty()) { cout << "ERR" << endl; return; } if (max_w == w) cout << "Yes" << endl; else cout << "No" << endl; } int main() { int t; cin >> t; // ---------------------------- while (t--) solve(); return 0; } /* .-~~~~~~~~~-._ _.-~~~~~~~~~-. __.' ~. .~ `.__ .'// A C 之 \./ 之 真 理 \`. .'// | \`. .'// .-~"""""""~~~~-._ | _,-~~~~"""""""~-. \`. .'//.-" `-. | .-' "-.\`. .'//______.============-.. \ | / ..-============.______\`. .'______________________________\|/______________________________`. */ ``` 写这道大模拟算是我运气比较好的一次了,只交了 $4$ 发就过了,甚至分数都是单调上升的( [$0\ pts$](https://www.luogu.com.cn/record/201373807) [$27\ pts$](https://www.luogu.com.cn/record/201374054) [$82\ pts$](https://www.luogu.com.cn/record/201375382) [$100\ pts$](https://www.luogu.com.cn/record/201377737)