p.s.又到了一年一度的开学季呀~
时间复杂度
当数据的数量变大的时候,程序运行的时间随之变长,我们用复杂度来描述程序运行时间和数据量的关系
程序1:
int cnt,a[100005];
for(int i=1;i<=n;i++)cnt+=a[i];
这个程序每次把 a [ 1 ] a[1] a[1]到 a [ n ] a[n] a[n]加起来,所以程序运行的时间和 n n n是线性相关的,记做 O ( n ) O(n) O(n)
程序2:
int cnt,a[100005][100005];
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)cnt+=a[i][j];
这个程序每次把 a [ 1 ] [ 1 ] a[1][1] a[1][1]到 a [ n ] [ n ] a[n][n] a[n][n]加起来,所以程序运行的时间和 n 2 n^2 n2是线性相关的,记作 O ( n 2 ) O(n^2) O(n2)
一些常见的时间复杂度
冒泡排序、选择排序
O
(
n
2
)
O(n^2)
O(n2)
归并排序、快速排序
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
埃氏筛法
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
欧拉筛法
O
(
n
)
O(n)
O(n)
计算复杂度的意义
评测机一秒约运行 5 ∗ 1 0 8 5*10^8 5∗108次,即评测机一秒可以进行 5 ∗ 1 0 8 5*10^8 5∗108次简单运算 ( + / − ∗ ) (+/-*) (+/−∗)想要不超时需要把复杂度控制在 5 ∗ 1 0 8 5*10^8 5∗108次简单运算以内。
所以我们也可以可以通过观察数据范围选择算法,如果是 1 0 4 10^4 104可以用复杂度为 O ( n 2 ) O(n^2) O(n2)的算法过, 1 0 6 10^6 106可以 O ( n l o g n ) O(nlogn) O(nlogn)过, 1 0 8 10^8 108可以 O ( n ) O(n) O(n)过,超过 1 0 8 10^8 108就只能 O ( 1 ) O(1) O(1)过,即找出规律直接判断。
差分数组和前缀和数组
例1:
有一个数组长度为n的数组
a
[
i
]
a[i]
a[i],
q
q
q组询问,每次询问
1
1
1到
m
m
m中,
a
[
1
]
+
a
[
2
]
+
a
[
3
]
+
.
.
.
a
[
m
]
a[1]+a[2]+a[3]+...a[m]
a[1]+a[2]+a[3]+...a[m]的和
n
=
1
0
6
,
q
=
1
0
6
,
1
<
=
m
<
=
n
n=10^6,q=10^6, 1<=m<=n
n=106,q=106,1<=m<=n
想法一:每次去把 a [ 1 ] a[1] a[1]到 a [ m ] a[m] a[m]加起来
for(int i=1;i<=m;i++)sum+=a[i];
printf("%d\n",sum);
复杂度是
O
(
n
q
)
O(nq)
O(nq)(因为每次最多把
n
n
n个数都加起来,最多加
q
q
q次),一旦
n
,
q
n,q
n,q都大于
1
0
5
10^5
105,我们的程序就超时了。
由于前
i
i
i个数的和,和前
i
+
1
i+1
i+1个数的和只相差了
a
[
i
+
1
]
a[i+1]
a[i+1],没必要每次都重新加,所以用了如下方法对其进行优化。
int a[100000],presum[100005];
for(int i=1;i<=n;i++)presum[i]=presum[i-1]+a[i];///先加好
printf("%d",presum[m]);
这样每次询问复杂度就变成了 O ( n ) O(n) O(n)(刚开始得到 p r e s u m presum presum数组需要 O ( n ) O(n) O(n)的时间),理论上可以通过 1 0 8 10^8 108的数据了,可是当我们把程序提交的时候却发现还是 T L E TLE TLE了,原因是没关同步流…
刚才处理处的 p r e s u m presum presum数组就是前缀和数组,利用这种方式我们可以把求区间和的复杂度降得很低,要计算 ∑ i = l r a [ i ] \sum^r_{i=l}a[i] ∑i=lra[i]只需要输出 p r e s u m [ r ] − p r e s u m [ l − 1 ] presum[r]-presum[l-1] presum[r]−presum[l−1]就可以了,节约了时间。
例2:
对于一个长度为
n
(
n
<
1
0
6
)
n(n<10^6)
n(n<106)的序列,我们对其进行
m
m
m次操作。每次给你
l
,
r
l,r
l,r,让你把
a
[
l
]
,
a
[
l
+
1
]
.
.
.
,
a
[
r
]
a[l],a[l+1]...,a[r]
a[l],a[l+1]...,a[r]都加一,输出
m
m
m次操作后的序列。
如果我们把
a
[
i
]
a[i]
a[i]到
a
[
j
]
a[j]
a[j]都加1,复杂度是
O
(
n
q
)
O(nq)
O(nq)。
若题目
m
=
1
0
6
m=10^6
m=106,每次
l
=
1
,
r
=
1
0
6
l=1,r=10^6
l=1,r=106,就超时了。
此时我们需要用近似的优化方法:差分数组进行优化
先声明一个数组
b
[
N
]
b[N]
b[N]
当操作使
a
[
l
]
,
a
[
l
+
1
]
.
.
.
,
a
[
r
]
a[l],a[l+1]...,a[r]
a[l],a[l+1]...,a[r]都加一时,我们令
b
[
l
]
+
+
b[l]++
b[l]++,
b
[
r
+
1
]
−
−
b[r+1]--
b[r+1]−−
这时候我们观察b[N]数组和它的前缀和:
b数组
0000000
1
第
l
位
00000
(
−
1
)
第
r
+
1
位
000
0 0 0 0 0 0 0 1_{第l位} 0 0 0 0 0 (-1)_{第r+1位} 0 0 0
00000001第l位00000(−1)第r+1位000
b的前缀和数组preb
0000000
1
第
l
位
111111
0
第
r
+
1
位
00
0 0 0 0 0 0 0 1_{第l位} 11111 1 0_{第r+1位} 0 0
00000001第l位1111110第r+1位00
发现
b
b
b的前缀和数组
p
r
e
b
preb
preb就是我们要得到的
a
a
a数组
这种利用辅助数组对区间加减进行优化的技巧叫做差分数组
即记录 a [ i ] − a [ i − 1 ] a[i]-a[i-1] a[i]−a[i−1]的值,区间修改的复杂度就变成了 O ( 1 ) O(1) O(1)(只需修改 a [ i ] a[i] a[i]和 a [ j + 1 ] a[j+1] a[j+1]),只是查询单个元素的复杂度变成了 O ( n ) O(n) O(n),因为要累加所有它之前的元素。
减少重复操作次数降低时间复杂度:
预处理
前面提到的前缀和是最常见的预处理方法。预处理是一种通过提前处理一些在计算中频繁用到的数据以提高程序效率的技巧。查询结果的复杂度是
O
(
1
)
O(1)
O(1),所以面对多组查询我们只需要付出预处理的时间复杂度。
这边再举素数的一些算法为例子:
一般来讲,判断一个数是否素数的复杂度为
O
(
n
1
/
2
)
O(n^{1/2})
O(n1/2)。判断
m
m
m个素数的复杂度却可以优化至很低,因为在判断之前筛出了某个范围内的素数,这也可以看成一种预处理优化。
一些干货:《调和级数优化复杂度》
在学习素数筛法的时候我们学过一种比较垃圾的筛叫做埃式筛,他的做法是枚举数然后枚举这个数的倍数。总体复杂度是基于调和级数的 O ( n l o g n ) O(nlogn) O(nlogn)
回忆起刚开学的时候做到过这个优化的题目例题:最大gcd,也是使用同样的方式把算
g
c
d
gcd
gcd变成了枚举
g
c
d
gcd
gcd的倍数
另一道用调和级数优化复杂度的题 CF484B
其实现在回想起来思路都是很类似的,
g
c
d
gcd
gcd或是余数都是可以通过枚举
a
[
i
]
a[i]
a[i]的倍数来计算得到的
空间复杂度
每个程序都有允许的内存大小,如果超过了允许的内存就会收到
M
L
E
MLE
MLE的提示。
那么如何计算允许的内存呢?
我们知道一个
i
n
t
int
int数组需要花
32
32
32个二进制位来存储,而一个字节包含
8
8
8个二进制位
那么容易计算,
512
M
B
512MB
512MB内存可以存下
512
∗
1024
∗
1024
∗
8
/
32
≈
5
∗
1
0
7
512*1024*1024*8/32≈5*10^7
512∗1024∗1024∗8/32≈5∗107个
i
n
t
int
int,所以一般用
512
M
B
−
>
6
∗
1
0
7
512MB->6*10^7
512MB−>6∗107去等比例换算就可以知道
a
[
N
]
a[N]
a[N]中的
N
N
N可以开多大了~
滚动数组
当 d p [ i ] [ j ] dp[i][j] dp[i][j]的转移只和 d p [ i − 1 ] [ . . . ] dp[i-1][...] dp[i−1][...]有关时我们可以把空间从 n 2 n^2 n2压缩至 2 ∗ n 2*n 2∗n~