文章目录
一、算法分析方法
时间复杂度
下界复杂度/最低复杂度:
Ω
(
f
(
x
)
)
\Omega(f(x))
Ω(f(x))
上界复杂度/最高复杂度:
O
(
f
(
x
)
)
O(f(x))
O(f(x))
非紧渐近上界:
o
(
f
(
x
)
)
o(f(x))
o(f(x))
非紧渐近下界:
ω
(
f
(
x
)
)
ω(f(x))
ω(f(x))
上下确界:
Θ
(
f
(
x
)
)
\Theta(f(x))
Θ(f(x))
等价
R
\mathcal{R}
R:
f
R
g
f\mathcal{R}g
fRg等价于
f
(
n
)
=
Θ
(
g
(
n
)
)
f(n)=\Theta(g(n))
f(n)=Θ(g(n))
前序
≺
\prec
≺:
f
≺
g
f\prec g
f≺g等价于
f
(
n
)
=
o
(
g
(
n
)
)
f(n)=o(g(n))
f(n)=o(g(n))
(
1
≺
l
o
g
l
o
g
n
≺
l
o
g
n
≺
n
≺
n
3
4
≺
n
≺
n
l
o
g
n
≺
n
2
≺
2
n
≺
n
!
≺
2
n
2
1\prec log logn \prec logn\prec\sqrt{n}\prec n^{\frac{3}{4}} \prec n \prec nlogn \prec n^2 \prec 2^n \prec n! \prec 2^{n^2}
1≺loglogn≺logn≺n≺n43≺n≺nlogn≺n2≺2n≺n!≺2n2)
空间复杂度(需要排除输入的空间开销!)
事实上,空间复杂度不是考虑的首选,因为空间复杂度的上界不会超过时间复杂度!
最优算法:时间复杂度在
Ω
(
f
(
x
)
)
\Omega(f(x))
Ω(f(x))与
O
(
f
(
x
)
)
O(f(x))
O(f(x))之间
eg1:对于朴素的二分查找,最好情况是
Ω
(
1
)
\Omega(1)
Ω(1),最坏情况是
O
(
l
o
g
n
)
O(logn)
O(logn),平均复杂度是
O
(
l
o
g
n
)
O(logn)
O(logn),空间复杂度是
O
(
1
)
O(1)
O(1)。
eg2:对于朴素的冒泡排序,最好情况,最坏情况,平均情况为
Θ
(
n
2
)
\Theta(n^2)
Θ(n2),空间复杂度为
O
(
1
)
O(1)
O(1)。
eg3:对于朴素的归并排序,最好情况,最坏情况,平均情况为
Θ
(
n
l
o
g
n
)
\Theta(nlogn)
Θ(nlogn),空间复杂度为
O
(
n
)
O(n)
O(n)。
eg4:对于朴素的快速排序,最好情况为
Ω
(
n
l
o
g
n
)
\Omega(nlogn)
Ω(nlogn),最坏情况为
O
(
n
2
)
O(n^2)
O(n2),平均情况为
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn),空间复杂度为
O
(
l
o
g
n
)
O(logn)
O(logn)。
二、分治问题
递归复杂度计算
对于递归复杂度
T
(
n
)
=
a
T
(
⌈
n
/
b
⌉
)
+
O
(
n
d
)
,
a
>
0
,
b
>
1
,
d
≥
0
T(n)=aT(\lceil n/b\rceil)+O(n^d),a>0,b>1,d\geq0
T(n)=aT(⌈n/b⌉)+O(nd),a>0,b>1,d≥0,有
T
(
n
)
=
{
O
(
n
d
)
i
f
d
>
l
o
g
b
a
O
(
n
d
l
o
g
n
)
i
f
d
=
l
o
g
b
a
O
(
n
l
o
g
b
a
)
i
f
d
<
l
o
g
b
a
T(n)=\left\{\begin{array}{ll}O(n^d) & if\ d>log_ba \\ O(n^dlogn) & if\ d=log_ba \\ O(n^{log_ba}) & if\ d<log_ba\end{array}\right.
T(n)=⎩
⎨
⎧O(nd)O(ndlogn)O(nlogba)if d>logbaif d=logbaif d<logba
二分搜索
mid是向下取整!
T
(
n
)
=
T
(
n
/
2
)
+
O
(
1
)
T(n)=T(n/2)+O(1)
T(n)=T(n/2)+O(1),即
a
=
1
,
b
=
2
,
d
=
0
a=1,b=2,d=0
a=1,b=2,d=0,则复杂度为
O
(
l
o
g
n
)
O(logn)
O(logn)
归并排序
T
(
n
)
=
2
T
(
n
/
2
)
+
O
(
n
)
T(n)=2T(n/2)+O(n)
T(n)=2T(n/2)+O(n),即
a
=
2
,
b
=
2
,
d
=
1
a=2,b=2,d=1
a=2,b=2,d=1,复杂度为
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)。
Volker Strassen矩阵乘法
对于矩阵
X
=
[
A
B
C
D
]
,
Y
=
[
E
F
G
H
]
X=\left[\begin{array}{cc}A&B \\ C&D\end{array}\right],Y=\left[\begin{array}{cc}E&F \\ G&H\end{array}\right]
X=[ACBD],Y=[EGFH],则
X
Y
=
[
A
B
C
D
]
[
E
F
G
H
]
=
[
A
E
+
B
G
A
F
+
B
H
C
E
+
D
G
C
F
+
D
H
]
XY=\left[\begin{array}{cc}A&B \\ C&D\end{array}\right]\left[\begin{array}{cc}E&F \\ G&H\end{array}\right]=\left[\begin{array}{cc}AE+BG&AF+BH \\ CE+DG&CF+DH\end{array}\right]
XY=[ACBD][EGFH]=[AE+BGCE+DGAF+BHCF+DH]
因此对于一个size为
n
n
n的
X
Y
XY
XY,分割为了
8
8
8个size为
n
/
2
n/2
n/2的矩阵后做加法(加法的复杂度是
O
(
n
2
)
O(n^2)
O(n2),要矩阵内每个元素两两相加),如此递归。
T
(
n
)
=
8
T
(
n
/
2
)
+
O
(
n
2
)
T(n)=8T(n/2)+O(n^2)
T(n)=8T(n/2)+O(n2),即
a
=
8
,
b
=
2
,
d
=
2
a=8,b=2,d=2
a=8,b=2,d=2,则时间复杂度为
O
(
n
3
)
O(n^3)
O(n3),这也是常规矩阵乘法的复杂度。
而Volker Strassen矩阵乘法,多了一个trick:
这样就只需要计算
7
7
7个子矩阵,即
T
(
n
)
=
7
T
(
n
/
2
)
+
O
(
n
2
)
T(n)=7T(n/2)+O(n^2)
T(n)=7T(n/2)+O(n2),即
a
=
7
,
b
=
2
,
d
=
2
a=7,b=2,d=2
a=7,b=2,d=2,则时间复杂度为
O
(
n
l
o
g
2
7
)
≈
O
(
n
2.81
)
O(n^{log_27})\approx O(n^{2.81})
O(nlog27)≈O(n2.81)。
三、贪心算法
对于一维数轴上若干条直线,最多可以兼容多少条线段互不重合?
先根据长度进行升序排序,然后依次放入不重合的,可以得到答案为
{
B
,
E
,
H
}
\{B,E,H\}
{B,E,H}。
对于若干个进程,有工作时间
t
i
t_i
ti与到达时间
d
i
d_i
di,如何安排顺序让进程的总等待时间最少?(非抢占)
先根据到达时间升序排序,相同到达时间的根据工作时间安排先后。
四、动态规划DP
以01背包为例,给定 n n n个物品与容量为 W W W的背包,每个物品有体积 w i > 0 w_i>0 wi>0和价值 v i > 0 v_i>0 vi>0,求背包能装满的最大的价值。
在01背包问题中贪心算法并非最优!
(如果以
v
i
/
w
i
v_i/w_i
vi/wi的比例为例来降序选择的话,选择
1
,
2
,
5
1,2,5
1,2,5号三件则是
35
35
35的value,但是选择
3
,
4
3,4
3,4号两件则是
40
40
40。)
令
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]表示在
1
∼
i
1\sim i
1∼i的物品中选择,且最大价值不超过
j
j
j的最大价值。
则
d
p
[
i
]
[
j
]
=
m
a
x
{
d
p
[
i
−
1
]
[
j
]
,
v
[
i
]
+
d
p
[
i
−
1
]
[
j
−
w
[
i
]
]
}
dp[i][j]=max\{dp[i-1][j],v[i]+dp[i-1][j-w[i]]\}
dp[i][j]=max{dp[i−1][j],v[i]+dp[i−1][j−w[i]]}(分别表示选第
i
i
i个和不选的情况,如果
w
[
i
]
>
j
w[i]>j
w[i]>j,则不考虑后半个部分,即
d
p
[
i
]
[
j
]
=
d
p
[
i
−
1
]
[
j
]
dp[i][j]=dp[i-1][j]
dp[i][j]=dp[i−1][j])
对于边界情况
j
=
0
j=0
j=0,则
d
p
[
i
]
[
j
]
=
0
dp[i][j]=0
dp[i][j]=0。
时间复杂度为
O
(
n
W
)
O(nW)
O(nW)
五、图论
握手定理:所有端点的入度与出度之和为边数量的两倍(
∑
v
∈
V
d
(
v
)
=
2
∣
E
∣
\sum_{v\in V}d(v)=2|E|
∑v∈Vd(v)=2∣E∣)
完全图:
K
n
K_n
Kn
二分图:
K
m
,
n
K_{m,n}
Km,n
星图:
K
1
,
n
K_{1,n}
K1,n(二分图的特殊情况)
r分割图:
K
r
(
m
)
K_{r(m)}
Kr(m)
最短路径
Dijkstra算法(单源最短路径,不可用于负权边)
d
[
v
]
=
m
i
n
(
d
[
v
]
,
d
[
u
]
+
w
u
,
v
)
d[v]=min(d[v],d[u]+w_{u,v})
d[v]=min(d[v],d[u]+wu,v)(松弛操作)
Bellman-Ford算法(单源最短路径,可用于负权边)
在Dijkstra算法的松弛操作基础上,最多
n
−
1
n-1
n−1次松弛就可以跑完全部的可能,但如果出现了负环的话,则说明
n
−
1
n-1
n−1次松弛之后仍然有可以松弛的边。
(判断是否有负环:建立一个超级源点,与所有点连接一个权值为
0
0
0的边,并以这个源点运行算法。)
floyd算法(全源最短路径,可以有负权边不能有负环)
时间复杂度
O
(
n
3
)
O(n^3)
O(n3)。
Johnson算法(全源最短路径,可以有负权边不能有负环)
时间复杂度
O
(
m
n
l
o
g
m
)
O(mnlogm)
O(mnlogm)
1.建立一个超级源点,与所有点连接一个权值为
0
0
0的边。从超级源点跑一次Bellman-Ford求出到其他点的最短路。得到图中所有点
p
p
p的势能为最短路路径长度
h
p
h_p
hp。
2.对图中任意一条边
u
→
v
u\rightarrow v
u→v且权值为
w
w
w。将该边权值定义为
w
+
h
u
−
h
v
w+h_u-h_v
w+hu−hv(类似重力势能的结构)
3.以每个点为起点,各自进行Dijkstra算法。
矩阵快速幂
网络流(有源点与汇点的图)
容量
f
(
u
,
v
)
f(u,v)
f(u,v):一条边上可供流过的最大流量
流量
c
(
u
,
v
)
c(u,v)
c(u,v):一条边上流过的实际流量(
f
(
u
,
v
)
≤
c
(
u
,
v
)
f(u,v)\leq c(u,v)
f(u,v)≤c(u,v))
残量
w
(
u
,
v
)
w(u,v)
w(u,v):一条边上的容量-流量,剩下可流的最大流(
w
(
u
,
v
)
=
c
(
u
,
v
)
−
f
(
u
,
v
)
w(u,v)=c(u,v)-f(u,v)
w(u,v)=c(u,v)−f(u,v))
除了源点与汇点,每个点的流入总量=流出总量。
f
(
u
,
v
)
=
−
f
(
v
,
u
)
f(u,v)=-f(v,u)
f(u,v)=−f(v,u)(反对称性)
增广路:一条从源点到汇点的路径,路径上每一条边的残量
w
(
u
,
v
)
>
0
w(u,v)>0
w(u,v)>0。
最大流等于最小割!(最小割:为了使源点与汇点不连通**,至少要去掉多少条边)
ford–fulkerson增广(求最大流)
0.令不存在边的权值为
0
0
0。(视为一条流量为
0
0
0的边)
1.找到一条增广路,找到这条路上最小的
w
(
u
,
v
)
w(u,v)
w(u,v)记为
f
l
o
w
flow
flow。
2.在这条增广路上的每一条边
(
u
,
v
)
(u,v)
(u,v)上减去
f
l
o
w
flow
flow。
3.在
(
v
,
u
)
(v,u)
(v,u)上加上
f
l
o
w
flow
flow(建立反向边,之后走反向边就意味着回溯)
4.重复以上,直到找不到增广路为止,此时之前所有操作过的
f
l
o
w
flow
flow之和就是最大流。
时间复杂度为 O ( ∣ E ∣ ∣ f ∣ ) O(|E||f|) O(∣E∣∣f∣)( E E E是边数, f f f是最大流大小。)