控制时空复杂度:和超时爆空间说拜拜
随着题目难度的增大,某 CQ 发现开始有越来越多的同学对空间超限以及时间超限发出了疑问。
某 CQ 十分的奇怪:明明题目的时空限制放的都很宽啊,怎么会出现超时超空间的问题呢?
一问发现有些同学的算法不是很好,导致在一些数据范围比较大的题目当中甚至出现了三重循环,这你不超时谁超时?如果看上去并不会超但是却时间超限了,那往往是死循环导致的。
那么我们到底该怎么样控制我们程序的时空复杂度呢?
其实十分的简单!下面就来大致的讲一讲罢!
时间复杂度:
这个时候你可以盯着题目给定的数据范围看,看看 n 的最大可以到多少,还有一些其它的数据范围,当你想好你将会怎么来做这道题之后,你就可以通过这些东西来计算自己应该用哪种数据类型(int
-> long long
)
然后,最重要的来了!
你盯着数据范围看,看够了再盯着自己循环次数最多的那个地方看(一次递归也算作一次循环),按照最大的数据范围计算出在最坏的情况下,你这里的循环要进行多少次,去掉一些没必要的常数,然后以 1亿 为准,在 1亿 左右或者小于 1亿 的循环次数,那你完全不用担心超时(正常机子 1亿 次循环次数大概刚好不过1s的时间限制,不过oj的机子貌似比较好,稍微超一点点也不大要紧,但不要太多)
另外一种可能导致时间超限的原因可能是无限递归或者死循环,这个时候你需要检查你所有的while循环有没有出现恒为真的情况、递归的终止条件有没有写完整(以及剪枝是否到位,有时剪枝是优化时间的很关键的一步)、还有for循环有没有写成别的奇怪的东西(例如:for (int j = 1; i <= n; ++j)),解决这些问题估计死循环也很难存在了
举个例子:n的数据范围是1到100000,你要对这个数据进行归并排序。
那么你会发现,首先你会需要循环n次来读入,但实际上这个不是最要紧的,最要紧的是排序的过程,你会发现,当你每次将某一个序列分割的时候,实际上你合并的时候需要将这两半序列都过一遍,也就是说你每次将序列分成两半,你都需要进行 n 次循环次数,而将一个长度为n的序列分割至最小需要 次所以这个程序实际上需要约 次的循环次数,这就是归并排序在时间上的复杂度,算算超过了 1亿 吗?并没有,所以这个数据范围,归并排序是可以过的。但是冒泡排序和插入排序它们就不太好过了,你会发现它们在时间上的复杂度都是n²,远远超过了1亿(1e10),这就过不去时间限制了。
空间复杂度:
这东西不要太简单,如果你空间超限,那么恭喜你犯了除编译错误以外第二傻的问题(说的严重点),因为这东西真的太好算辣!
sizeof(x)
可以算出 x 这个变量/数组/指针等任意东西所占用的内存大小,是按照字节来计算的,比如说 int
占 4 个字节,char
占 1 个字节。
仔细观察每道题的标题,你会发现一个叫做空间限制的东西。
你再仔细看你会发现它大概是 256 MB,那我们就以 256 MB 为标准来提一提,比如说你开了一些数组,假设现在有 a、b、c 三个数组,你想知道它们占了多少空间,那你可以尝试输出 (sizeof(a) + sizeof(b) + sizeof(c)) / 1024 / 1024
,如果它大于等于 256,那你肯定就寄了。
当然,你也可以借助空间限制来完成一些本来不可能的操作:
比如,内存分配器这道题,题目要求明显是要你用链表来动态分配内存,因为它并没有明确告诉你到底要用多大的数组才够用,如果你想使用静态数组,你就一定得知道该用多大,而借助空间限制我们可以了解到:题目要求你用这么多内存来解决问题,那它肯定不会让你的空间超过这个限制,也就是说,你只要尽可能顶着空间限制开,那肯定没问题。
观察题目我们知道需要开两个 int
数组,当然还要算上其它的一些变量函数啥的也会占内存,我们可以分出大概 200 多 MB 给这两个数组,那也就是每个数组有 100 MB 可以开,我们知道一个 int
占 4 个字节,1024 个字节是 1 KB,1024 KB 是 1 MB,所以我们的数组长度可以开到 个,经过测试的确是可以过的,所以,当我们的做法受到限制的时候,我们甚至可以利用空间限制来另辟蹊径,找到一种更容易写出的解决方案。
看完这个,再去看两道题实战演练一下,相信你们再也不会在群里问超时怎么办,超空间怎么办这一类的问题了吧?