一文彻底搞懂【NOIP2017时间复杂度】,看完你就会了

原题链接:牛客2021秋季竞赛1002

一、题目与要求

题目
给出了他自己算出的时间复杂度,可他的编程老师实在不想一个一个检查小明的程序,于是你的机会来啦!下面请你编写程序来判断小明对他的每个程序给出的时间复杂度是否正确。 A++ 语言的循环结构如下:
F i x y
循环体
E
然后判断 i 和 y 的大小关系,若 i 小于等于 y 则进入循环,否则不进入。每次循环结束后i都会被修改成 i +1,一旦 i 大于 y 终止循环。 x 和 y 可以是正整数(x 和 y 的大小关系不定)或变量 n。n 是一个表示数据规模的变量,在时间复杂度计算中需保留该变量而不能将其视为常数,该数远大于 100。E表示循环体结束。循环体结束时,这个循环体新建的变量也被销毁。
注:本题中为了书写方便,在描述复杂度时,使用大写英文字母 O 表示通常意义下 Θ 的概念。
输入描述
输入文件第一行一个正整数 t,表示有 t(t≤ 10) 个程序需要计算时间复杂度。
每个程序我们只需抽取其中 F i x yE即可计算时间复杂度。注意:循环结构允许嵌套。
接下来每个程序的第一行包含一个正整数 L 和一个字符串,L 代表程序行数,字符串表示这个程序的复杂度,O(1)表示常数复杂度,O(n^w) 表示复杂度为 nw,其中 w 是一个小于 100 的正整数(输入中不包含引号),输入保证复杂度只有 O(1)O(n^w) 两种类型。
接下来 L 行代表程序中循环结构中的 F i x y 或者 E。 程序行若以 F 开头,表示进入一个循环,之后有空格分离的三个字符(串)i x y,其中 i 是一个小写字母(保证不为 n ),表示新建的变量名,x 和 y 可能是正整数或 n ,已知若为正整数则一定小于 100。 程序行若以 E开头,则表示循环体结束。
输出描述
输出文件共 t 行,对应输入的 t 个程序,每行输出YesNo或者ERR,若程序实际复杂度与输入给出的复杂度一致则输出 Yes,不一致则输出No,若程序有语法错误(其中语法错误只有: ①F 和 E 不匹配 ②新建的变量与已经存在但未被销毁的变量重复两种情况),则输出ERR
注意:即使在程序不会执行的循环体中出现了语法错误也会编译错误,要输出ERR

二、样例输入与输出

输入

8
2 O(1)
F i 1 1
E
2 O(n^1)
F x 1 n
E
1 O(1)
F x 1 n
4 O(n^2)
F x 5 n
F y 10 n
E
E
4 O(n^2)
F x 9 n
E
F y 2 n
E
4 O(n^1)
F x 9 n
F y n 4
E
E
4 O(1)
F y n 4
F x 9 n
E
E
4 O(n^2)
F x 1 n
F x 1 10
E
E

输出

Yes
Yes
ERR
Yes
No
Yes
Yes
ERR

说明

第一个程序 i 从1 到 1 是常数复杂度。
第二个程序 x 从 1 到 n 是 n 的一次方的复杂度。
第三个程序有一个 F 开启循环却没有E结束,语法错误。
第四个程序二重循环,n 的平方的复杂度。
第五个程序两个一重循环,n 的一次方的复杂度。
第六个程序第一重循环正常,但第二重循环开始即终止(因为 n 远大于 100,100 大于 4)。
第七个程序第一重循环无法进入,故为常数复杂度。
第八个程序第二重循环中的变量 x 与第一重循环中的变量重复,出现语法错误②,输出 ERR

三、深度解析

本题其实是很难的,涉及数据结构,而且C/C++格式化后的代码一般至少五十行,所以本题需要考虑的细节也很多,不能急于求成!想做对本题一定要理解这些关键概念:循环体排列方式,时间复杂度,栈。其中栈可能有一些人没想到,但还不是最关键的,最关键的是前两个概念,这两个概念认识不够深就不可能通过,但即便对这两个概念的掌握不是百分百,也可能通过,因为本人发现平台的测试数据是不全的 。当然这点留到最后再说,因为测试数据已经考察了所有的关键点。接下来按解题所需的顺序讲解这些关键概念,即前一个理解才能写后面的代码。

关键点一:循环体排列方式

众所周知,循环分单重循环和多重循环,但若要计算多重循环的复杂度,仅考虑连续嵌套循环的情况是不够的,还要考虑一个循环内夹杂几个并行的循环,并行的循环可以是单重也可以是多重! 这就是本题的第一个难点。总结一下,循环体分单重循环(单重),连续嵌套的多重循环(连续嵌套),多重循环内夹杂多个并行循环(嵌套夹杂并行),下面逐一讲解。
单重
这个是最简单的,就是FE,连续并行的单重循环就是FEFE…,注意单重循环并行,无需考虑变量创建是否重复,因为F所在的循环只有一个变量。当然写代码时为了方便,最好把单重循环当作多重循环的特例,即单重循环也用多重循环的算法。
连续嵌套
这个也容易想到,就是FFEE的形式,二重循环就是两个连续的F+两个连续的E,其他情况同理。不难发现,只要出现连续的两个F,当前循环就是多重循环。但和单重的情况一样,为了方便,建议把连续嵌套当作嵌套夹杂并行的特例,如果你这两种情况都按我建议的做了,那么将会少写很多代码。

嵌套夹杂并行(难点1)

这点对大部分人来说是很难想到的,这也是全国竞赛的选拔性的体现之一。本题的实用性也极强,因为题目背景就是编程,本题通过了,那么对程序循环的理解一定会更上一层。回到正题,这种情况很复杂,因为前面说了,并行的循环可以是单重也可以是多重,举个简单的例子:
FFEFEE
拆开来写就是:
F FE FE E
显然,这是一个循环内套两个单重循环。如果说第二种情况仅需考虑连续两个F就能确定,那么这种情况就很难判断了。但其实我们是不需要主动判断这三种情况的,因为读取输入数据,即顺序读取FE序列本身就是一个按照循环的定义执行的过程,所以不需要额外判断,我们只需在循环开始和结束时根据这些情况计算好复杂度就行了。

到目前为止,我们已经理解了循环体的三种排列方式,这对后续概念的理解和写代码非常重要。虽然这三种方式不需要你写代码判断,但我们已经能够得出语法正确的条件之一,即每个F必然有个E匹配,所以正确的语法一定是F和E的数量相等的。但请注意,F和E的数量要等输入完后才能确定,为节省代码量,最好是边输入边处理数据。所以我们现在只能等处理完数据再判断E和F的数量,但输入F和E之前我们可以先判断输入程序行数L是否为偶数,这个应该不难想到。

关键点二:栈

栈的概念虽然不难,但对于本题是一定要想到的,因为如果你仔细研究FE序列就不难发现,读取这些序列其实就是出栈和入栈的过程:遇到F,循环开始,就是入栈,重复这个过程直到遇到E,与最近的F抵消,就是出栈。前面说过,我们不需要主动判断循环体的三种排列,因为计算循环体的复杂度需要递归判断,而递归判断就是栈的思想。栈的思想不仅能计算复杂度的问题,还能解决变量名重复和无效循环的问题
变量名重复的检测很简单,维护一个栈+线性搜索就行了,当然这个栈不是严格意义上的,因为查重要搜索栈内元素。注意当E的数量超过F时,会出现栈空但需要出栈的情况,此时可直接判定语法错误。
无效循环,即不进入的循环,这个循环不进入,那就说明当前的F和E是需要忽略的,但由于存在嵌套循环,所以我们需要一个变量来统计无效循环的个数,遇到F就加1,遇到E,说明当前无效循环结束,减1即可。注意这里和检测变量名不同,不需要开辟一个空间来当作栈,只需一个栈顶指针即可。

关键点三:时间复杂度

这个概念是数据结构最初的知识点,就这个知识点本身而言其实是不难理解的,但由于本题涉及嵌套夹杂并行的情况,对复杂度计算的要求其实是很高的。在考虑这个情况之前,首先要明确只含循环的程序的复杂度是如何计算的,即。所以本小节分为两边部分:复杂度计算的一般流程,嵌套夹杂并行的复杂度。

复杂度计算的一般流程(难点2)

众所周知,并行循环的复杂度是相加的,连续嵌套的循环复杂度是相乘的。一个程序的复杂度其实就是多个并行循环的复杂度取最大值,无论并行的循环是否是嵌套的。所以这里要设置两个计算复杂度的变量,一个计算每个循环的复杂度,不妨设为on,简称小O,一个是程序的总复杂度,不妨设为ON,简称大O。显然,小O对大O来说是多个并行的循环,所以小O不仅要计算每个循环的复杂度,而且要在每个循环结束,即每次碰到E的时候,要向大O贡献复杂度,即和大O对比,大O取与小O之间的最大值。至于每次循环的复杂度,这是数据结构的最基础的知识,不做赘述。
理解以上流程,就可以写代码算复杂度了,当然,只要算n的指数就行了,算完一个程序的n的指数,就和题目给定的指数w对比,这个应该也不难想到。注意题目中的x,y可以是整数也可以是字符,所以比较x,y的方式还是很多的,本人用的是字符判断+字符转整数的方式,详见代码这节。

嵌套夹杂并行的复杂度(最大难点)

这个点可以说是本题的大BOSS,终极BOSS了,本人最初通过90%的用例,最后10%死活通不过,其实就是卡死在这个点上了。这个点说白了就是要回退复杂度,即复杂度降级。还是举第一小节的例子:
F FE FE E
首先第一个F的复杂度不用管,只需看后面的F。假设第二个F复杂度是ON,第三个是ON,所以两个并行循环的总复杂度是ON,如果你不做复杂度回退,问题就来了:两个并行循环的复杂度会是ON2。所以复杂度回退思想的动机就是:假设当前循环L1还有与之并行的循环L2,L3,…,Lm,那么L1的上一层循环L0都应该把L2,L3,…,Lm的复杂度都乘一遍。具体来说,假设计算 L i L_i Li的小O是 O i O_i Oi,那么就是 O 0 ∗ O 1 O_0*O_1 O0O1,其他小O应该是 O 0 ∗ O 2 , O 0 ∗ O 3 , . . . , O 0 ∗ O m O_0*O_2,O_0*O_3,...,O_0*O_m O0O2,O0O3,...,O0Om大O的计算应该是 O = m a x ( O 0 ∗ O 1 , O 0 ∗ O 2 , . . . , O 0 ∗ O m ) ( 1 ) O=max(O_0*O_1,O_0*O_2,...,O_0*O_m)\quad (1) O=max(O0O1,O0O2,...,O0Om)(1)显然,当时 m = 1 m=1 m=1时,不需要回退,但 m > 1 m>1 m>1时,只有回退 O i O_i Oi,重新乘以 O i + 1 O_{i+1} Oi+1才符合大O定义。所以 O 0 ∗ O 1 O_0*O_1 O0O1要先除以 O 1 O_1 O1,然后再去乘以其他 O i ( i = 2 , 3 , . . . , m ) O_i(i=2,3,...,m) Oi(i=2,3,...,m)。又因为要取所有 O 0 ∗ O i O_0*O_i O0Oi的最大值,所以除以 O 1 O_1 O1前,大O要先保存一次 O 0 ∗ O 1 O_0*O_1 O0O1,即回退复杂度应该在小O和大O取完最大值之后
注意,网上很多AC代码会考虑常数复杂度的问题,这个其实不需要考虑,因为常数复杂度,你指数不加就是了。认为常数复杂度很重要的其实还是没把嵌套夹杂并行的情况理解彻底,即没想到公式1,可以看出,数学对编程还是有很大帮助的。

直到这里,本题的难点才算彻底解决,如果你还没做过本题或还没通过样例,建议先根据以上解析,自己复现一遍,对编程能力的提高一定会有很大帮助。

四、AC代码与测试数据的不足

以下是本人的代码,供大家参考,其中ON,on,flag,l分别表示程序总复杂度,单个循环复杂度,语法错误标记,无效循环个数。

#include<bits/stdc++.h>
using namespace std;
int main() {
	int L, t, w, ON, on, flag, numE, numF, l;
	char ch, i;
	string s, str_i, x, y;
	cin >> t;
	while (t--) {
		w = ON = on = flag = numE = numF = l = 0;
		str_i = "";
		cin >> L >> s;
		if (L % 2)flag = 1;
		if (s[2] == 'n')w = stoi(s.substr(4, s.size() - 5));
		while (L--) {
			cin >> ch;
			if (ch == 'F') {
				cin >> i >> x >> y;
				if (flag)continue;
				if (str_i.find(i) != string::npos) {
					flag = 1;
					continue;
				}
				numF++;
				str_i += i;
				if (l || (x[0] == 'n' && y[0] != 'n') ||
				        (x[0] != 'n' && y[0] != 'n' && stoi(x) > stoi(y))) {
					l++;
					continue;
				}
				if (x[0] != 'n' && y[0] == 'n')on++;
			} else {
				if (flag)continue;
				if (str_i == "") {
					flag = 1;
					continue;
				}
				numE++;
				str_i.pop_back();
				if (l) {
					l--;
					continue;
				}
				if (on)ON = max(ON, on--);
			}
		}
		if (flag || numE != numF)printf("ERR\n");
		else printf(w == ON ? "Yes\n" : "No\n");
	}
	return 0;
}

测试平台其实缺少以下两种测试数据,但本代码不仅能通过系统的测试数据,还能通过所有情况的数据:
1.x和y都取两位数且大的数的每个位都大于小的数,比如x=33,y=22。有的AC代码对x和y进行逐位比较,但遇到x=33,y=22就不行,可惜测试数据中遗漏了这种情况。
2.多个单重循环并列,且O(1)在O(n)前面,O(n)有多个,比如FE FE FE,第一个FE是O(1),后两个FE是O(N),显然总复杂度应该是O(N)。有的AC代码会做复杂度回退操作,但只遇到一次常数级循环就停止回退,这会导致这种代码在这个例子中,总复杂度为O(N^2)而不是O(N)
以上情况本人已在牛客网反馈,如果你用的是其他平台,也可进行核查和反馈。

以上就是本题的全部解析了,希望对大家有帮助,最后附上AC截图:
牛客竞赛1002

  • 27
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

技术卷

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值