PrevNext
Resources
IUSACO

本模块以此为基础

CPH

简介和示例

PAPS1

内容更深入;其中第 5.2 节给出了大 O 的形式化定义。

YouTube

如果你更喜欢观看视频


在算法竞赛中,程序必须在规定时间内运行完毕才能得分。USACO 对 C++ 提交的时限为 22 秒,对 Java/Python 提交的时限为 44 秒。保守估计,评测服务器每秒可以处理 10810^8操作; 如果常数因子较小,则可能接近 51085 \cdot 10^8 次。

复杂度计算

我们希望根据输入规模 nn 计算每种算法运行所需的操作次数。幸运的是,利用 大 O 记号可以比较容易地做到这一点。 当 nn 任意增大时,大 O 记号把最坏情况下的时间复杂度表示为 nn 的函数。复杂度 给出了算法所需步骤数关于输入规模的上界。大 O 记号把函数的复杂度写作 O(f(n))\mathcal{O}(f(n)),通常会省略 f(n)f(n) 中的常数因子和低阶项。下面来看一些示例。

下面的代码执行固定次数的操作,因此复杂度为 O(1)\mathcal{O}(1)

a = 5
b = 7
c = 4
d = a + b + c + 153

输入和输出操作也假定为 O(1)\mathcal{O}(1)。在下面的示例中,我们假定循环体内代码 的复杂度为 O(1)\mathcal{O}(1)

循环的时间复杂度取决于循环执行的迭代次数。例如,下面两段代码的复杂度都是 O(n)\mathcal{O}(n)

for i in range(1, n + 1):
pass # constant time code here
i = 0
while i < n:
# constant time code here
i += 1

由于我们忽略常数因子和低阶项,下面的示例同样都是 O(n)\mathcal{O}(n)

for i in range(5 * n + 17):
pass # constant time code here
for i in range(n + 457737):
pass # constant time code here

对于嵌套的多层循环,可以将每层循环的时间复杂度相乘。下面示例的复杂度为 O(nm)\mathcal{O}(nm),因为外层循环执行 O(n)\mathcal{O}(n) 次,内层循环执行 O(m)\mathcal{O}(m) 次。

for i in range(n):
for j in range(m):
pass # constant time code here

在这个示例中,外层循环执行 O(n)\mathcal{O}(n) 次,内层循环执行 11nn 次 (最多为 nn 次)。由于大 O 记号计算最坏情况下的时间复杂度,我们把内层循环 视作因子 nn因此,这段代码 的复杂度为 O(n2)\mathcal{O}(n^2)

for i in range(n):
for j in range(i, n):
pass # constant time code here

如果一个算法包含多个代码块,其时间复杂度取其中最大的复杂度。例如,下面代码的 复杂度为 O(n2)\mathcal{O}(n^2)

for i in range(n):
for j in range(n):
pass # constant time code here
for i in range(n + 58834):
pass # more constant time code here

下面代码的复杂度为 O(n2+m)\mathcal{O}(n^2+m),因为它由复杂度分别为 O(n2)\mathcal{O}(n^2)O(m)\mathcal{O}(m) 的两个代码块组成,且二者相对于对方都不是 低阶函数。

for i in range(n):
for j in range(n):
pass # constant time code here
for i in range(m):
pass # more constant time code here

常见复杂度与限制

一些常见算法和数据结构所产生的复杂度因子如下:

Warning!

即使其中大多数内容你都不认识也不用担心!后面都会介绍。

  • 直接计算答案的数学公式:O(1)\mathcal{O}(1)
  • 二分查找:O(logn)\mathcal{O}(\log n)
  • 有序集合/映射或优先队列:每次操作 O(logn)\mathcal{O}(\log n)
  • 对整数进行质因数分解,或朴素判断一个整数是质数还是合数: O(n)\mathcal{O}(\sqrt{n})
  • 读入 nn 项数据:O(n)\mathcal{O}(n)
  • 遍历含 nn 个元素的数组或列表:O(n)\mathcal{O}(n)
  • 排序:默认排序算法(归并排序、Collections.sortArrays.sort)通常为 O(nlogn)\mathcal{O}(n\log n)
  • Java 对基本类型使用快速排序的 Arrays.sortO(n2)\mathcal{O}(n^2)
  • 遍历输入元素中所有大小为 kk 的子集:O(nk)\mathcal{O}(n^k)。例如,遍历所有三元组 为 O(n3)\mathcal{O}(n^3)
  • 遍历所有子集:O(2n)\mathcal{O}(2^n)
  • 遍历所有排列:O(n!)\mathcal{O}(n!)

下面给出了各种时间复杂度所能处理的 nn 的保守上界。实际可处理的规模可能更大, 但这张表足以帮助你快速判断一种算法是否可行。

nn可行的复杂度
n10n \le 10O(n!)\mathcal{O}(n!), O(n7)\mathcal{O}(n^7), O(n6)\mathcal{O}(n^6)
n20n \le 20O(2nn)\mathcal{O}(2^n \cdot n), O(n5)\mathcal{O}(n^5)
n80n \le 80O(n4)\mathcal{O}(n^4)
n400n \le 400O(n3)\mathcal{O}(n^3)
n7500n \le 7500O(n2)\mathcal{O}(n^2)
n7104n \le 7 \cdot 10^4O(nn)\mathcal{O}(n \sqrt n)
n5105n \le 5 \cdot 10^5O(nlogn)\mathcal{O}(n \log n)
n5106n \le 5 \cdot 10^6O(n)\mathcal{O}(n)
n1018n \le 10^{18}O(log2n)\mathcal{O}(\log^2 n), O(logn)\mathcal{O}(\log n), O(1)\mathcal{O}(1)
Warning!

相当一部分铜组问题都有 n100n\le 100。这并不能充分暗示预期时间复杂度,预期解法仍然 可能是 O(n)\mathcal{O}(n)

常数因子

常数因子是指:复杂度相同的不同操作,实际运行时间会略有不同。例如,三次加法 会比一次加法稍慢。又如,虽然在数组上进行二分查找和向有序集合插入元素的复杂度 都是 O(logn)\mathcal{O}(\log n),但二分查找明显更快。

大 O 记号完全忽略常数因子。大多数情况下这样没有问题,但如果时限特别紧, 即使复杂度符合预期也可能超时(TLE)。此时就必须考虑常数因子。例如,遍历所有 _有序_三元组的代码以 O(n3)\mathcal{O}(n^3) 运行;如果只需遍历所有_无序_三元组, 速度可能提高 66 倍。

目前不必担心如何优化常数因子,只需知道它们的存在。

大 O 记号的形式化定义

ffgg 是从 R0\mathbb{R}_{\ge 0}R0\mathbb{R}_{\ge 0} 的非负函数。如果 存在正常数 n0n_0cc,使得每当 nn0n\ge n_0 时都有 f(n)cg(n)f(n)\le c\cdot g(n),就称 f(n)=O(g(n))f(n)=\mathcal{O}(g(n))

因此,我们可以说复杂度为 O(n)\mathcal{O}(n) 的线性函数同时也是 O(n/2)\mathcal{O}(n/2)O(2n)\mathcal{O}(2n)O(n2)\mathcal{O}(n^2)O(2n)\mathcal{O}(2^n)O(nn)\mathcal{O}(n^n) 等。不过,我们通常会在限制最紧的函数中写出最简单的一个; 对于上面的线性函数,就是 O(n)\mathcal{O}(n)

Optional: P 与 NP

P 指能够在多项式时间内求解的问题类别(O(n2)\mathcal{O}(n^2)O(n3)\mathcal{O}(n^3)O(n100)\mathcal{O}(n^{100})\dots)。NP 是非确定性多项式时间的 缩写,指解可以在多项式时间内验证的问题集合。

NP 中一个常见的例子是广义数独:一个解很容易在多项式时间内验证,但目前并不知道 能否在多项式时间内求出解。“P 与 NP”是一个经典的未解问题,它询问:所有能在 多项式时间内验证的问题,是否也都能在多项式时间内求解?

如果想进一步了解 P 与 NP,请观看这个 YouTube 视频

小测验

什么是时间复杂度?

Question 1 of 4

Module Progress:

PrevNext