, S9 ?: k9 _7 W/ z( {0 W6 ]0 _5 a4 [. R
e、队列 " u* z7 N& n8 D' f" e/ a内存结构:看用数组实现,还是链表实现 - i! `+ {, y, s6 m实现难度:一般 4 j! m2 s o! ?7 g3 f% b8 o a9 `! `% W下标访问:不支持7 Z9 Y" i6 Z$ L8 f" m, n; d
分类:FIFO、单调队列、双端队列 . M& x W! @. L. ]% J( w/ H3 D插入时间复杂度:O ( 1 ) O(1)O(1) % y( ^$ R! ^- B# N9 S% ]查找时间复杂度:理论上不支持$ p* E) ]2 C$ i4 x& i
删除时间复杂度:O ( 1 ) O(1)O(1)4 \3 E8 @; U2 p: H4 C+ J; j
% d/ E: ?9 U/ F
6 a0 B- L0 f7 }- P8 h- R8 Y- N
f、栈/ ^) v& ^) `% z5 w( U! c0 F( t
内存结构:看用数组实现,还是链表实现 $ N. j7 |. z& t( e& J实现难度:一般5 F! c) l4 s. Y0 l
下标访问:不支持+ Z1 p0 V( m2 ?4 z5 J/ e7 W1 V
分类:FILO、单调栈6 I1 Y$ B! N* m s
插入时间复杂度:O ( 1 ) O(1)O(1)2 m+ ]8 e k; l& a
查找时间复杂度:理论上不支持 1 I, @( k f ]2 C删除时间复杂度:O ( 1 ) O(1)O(1) ' Q! ~/ P1 A# g p& E6 {) A ( N/ R9 l4 O. B- d. f9 q/ r& c" }7 F) W! S
g、树, i# v, ]6 ?7 A- ~; c! }
内存结构:内存结构一般不连续,但是有时候实现的时候,为了方便,一般是物理连续,逻辑不连续 # v5 T H5 b6 T: D实现难度:较难 D; ?$ C0 O/ P2 @( X下标访问:不支持 ' y ^- J7 D7 O, G0 g" W0 F分类:二叉树 和 多叉树 ( H, o+ h+ E5 D6 T( F4 e插入时间复杂度:看情况而定8 \% D! h: F$ y1 ~& m
查找时间复杂度:理论上 O ( l o g 2 n ) O(log_2n)O(log : T2 A4 f U1 u8 Z6 J26 N) C( Z. H* g7 L% C% p' ?7 y. a
# w I( k+ p/ @) J1 T$ h T3 w n)* M. n7 A8 [; a( y
删除时间复杂度:看情况而定7 C+ k) V) k9 U! E3 ?
7 B6 n, v, V" e0 C% R
1 t+ N m; g- r. K' u* X! \1、二叉树2 c! |4 q# J* K0 K2 {: k, y3 i$ Q8 y
二叉树的种类较多,比如:二叉搜索树、平衡树。平衡树又可以分为 AVL 树、红黑树、线段树、堆。最平衡的树莫过于满二叉树了。 ' Y0 ]( \8 ^# H, |7 [: k其中,堆也是一种二叉树,也就是我们常说的优先队列。 8 c( m. A# w* F2、多叉树 0 I. s+ z. R% ~) O) b, hB树和B+树是多叉树,当然我们平时学到的并查集其实也是个多叉树,更加严谨一点,应该称之为森林。 t* ?2 \! Y+ }" ^h、图 ! e% f' \3 ?# e! D" y5 E内存结构:不一定1 u: c# k, K d2 S+ x$ s( N7 w
实现难度:难 ; P, q! l- F' z9 B0 I3 ]下标访问:不支持( ]6 P( Q! q" r P
分类:有向图、无向图/ A7 b3 N; u9 E L0 k" i8 `
插入时间复杂度:根据算法而定 " d- e1 e/ d7 I. G查找时间复杂度:根据算法而定9 U& m& ^8 X% W/ I
删除时间复杂度:根据算法而定 % ` J1 ^* s6 q( ^: W0 B4 N & O- [, |3 ^( h - e/ O6 m0 l9 I. z; P1、图的概念' n3 N, N ?+ n" G0 t! x
在讲解最短路问题之前,首先需要介绍一下计算机中图(图论)的概念,如下:+ u1 w( v2 f3 k; Y: |5 f
图 G GG 是一个有序二元组 ( V , E ) (V,E)(V,E),其中 V VV 称为顶点集合,E EE 称为边集合,E EE 与 V VV 不相交。顶点集合的元素被称为顶点,边集合的元素被称为边。9 h1 e( w9 M S$ ^( {
对于无权图,边由二元组 ( u , v ) (u,v)(u,v) 表示,其中 u , v ∈ V u, v \in Vu,v∈V。对于带权图,边由三元组 ( u , v , w ) (u,v, w)(u,v,w) 表示,其中 u , v ∈ V u, v \in Vu,v∈V,w ww 为权值,可以是任意类型。 5 Z6 O! F' k- O8 G- s图分为有向图和无向图,对于有向图, ( u , v ) (u, v)(u,v) 表示的是 从顶点 u uu 到 顶点 v vv 的边,即 u → v u \to vu→v;对于无向图,( u , v ) (u, v)(u,v) 可以理解成两条边,一条是 从顶点 u uu 到 顶点 v vv 的边,即 u → v u \to vu→v,另一条是从顶点 v vv 到 顶点 u uu 的边,即 v → u v \to uv→u;* ?7 H3 [4 _/ b- M( E# _+ V% R( C. s
2、图的存储 8 g6 Q/ |. r- w% E* t& b对于图的存储,程序实现上也有多种方案,根据不同情况采用不同的方案。接下来以图二-3-1所表示的图为例,讲解四种存储图的方案。 ( @+ n: u; B* x( ?) Q; m5 L2 F* l) \# A- S/ C+ z% b* Y" q
0 S# ^+ q. s5 w. _1)邻接矩阵& m- N, A* `2 N2 k
邻接矩阵是直接利用一个二维数组对边的关系进行存储,矩阵的第 i ii 行第 j jj 列的值 表示 i → j i \to ji→j 这条边的权值;特殊的,如果不存在这条边,用一个特殊标记 ∞ \infty∞ 来表示;如果 i = j i = ji=j,则权值为 0 00。 + @0 ]4 n: `/ a+ O; a, J+ K2 v它的优点是:实现非常简单,而且很容易理解;缺点也很明显,如果这个图是一个非常稀疏的图,图中边很少,但是点很多,就会造成非常大的内存浪费,点数过大的时候根本就无法存储。, }/ D1 O/ M/ @0 r& O
[ 0 ∞ 3 ∞ 1 0 2 ∞ ∞ ∞ 0 3 9 8 ∞ 0 ] \left[ ! A) J* l9 M( [" M01∞9∞0∞8320∞∞∞30 0 v" `2 H/ v; S7 s1 r4 m u; X0∞3∞102∞∞∞0398∞0% l8 A* e8 U, d6 j0 u7 ^
\right] & a' O1 e$ X/ ?# n4 v( L1 G1 p% |⎣ 2 a1 d* H) a% Y2 m2 S⎢" [9 v! I0 F% i7 {" y- E9 u. G
⎢' k2 }' I& b- {$ \
⎡ ! W2 n3 J1 A! m ! X, o" }. }& q( @0 y$ D * y$ e; O9 V+ X8 i6 B4 J0 - i. L5 q% C8 n0 N8 p1) G5 E9 U% o* }# }, Z; ?& j
∞+ A% E+ _9 W% f |4 r7 x
9( n$ i' u! ~" g
8 [) ]6 Z' B( M1 Z& a. q+ x
" T3 ^% @; a! ~2 [
∞ % W) w0 X3 z3 h; V/ y1 o$ w+ |0 6 i) h$ ?) y3 G- C+ J# R7 V d∞ ; U# ^* K( \. ~4 g' E( j8 [/ R8) A3 c! V1 T# u0 B4 a L+ f
8 h8 f8 x7 x% m% r8 w , I( r% e: C" @3 a4 f( G1 m! r
3/ z4 d- G& K- B2 @" T+ y. J. t
26 _0 i( j$ W9 f( G0 G
0! d0 X! x# Q1 c9 x0 Y) }% Y" O
∞ 4 Y4 v5 O6 e, J& L3 o& E, y, O 6 y/ E/ z9 A' n9 G4 ^ 6 c& \8 `, i+ z. ^5 {, G
∞ # [5 q1 m) ~. Q/ R. M∞, d, \1 S c I: Y! t
32 C8 F4 Y( K) ~- A9 o
0# M8 r$ t# d0 l* h' Z
3 m+ l7 b2 c7 H' a & @; f/ m9 V. w& H8 _
⎦- l( T6 Y) X1 i
⎥ ! \5 l( T' `0 w: P4 v( b⎥ 7 n0 O, a! t! R' G9 F6 D⎤ " k+ m. f( g0 I& k 6 x" c7 t+ c3 X7 P) b- Y9 g $ a, w/ `( o% ~& s1 d! m" R2)邻接表 ! \, h4 j6 ~ u. b( `+ A7 p邻接表是图中常用的存储结构之一,采用链表来存储,每个顶点都有一个链表,链表的数据表示和当前顶点直接相邻的顶点的数据( v , w ) (v, w)(v,w),即 顶点 和 边权。5 i6 k0 Y. [& Q3 O
它的优点是:对于稀疏图不会有数据浪费;缺点就是实现相对邻接矩阵来说较麻烦,需要自己实现链表,动态分配内存。 # c4 f# T- b& K" w( T如图所示,d a t a datadata 即 ( v , w ) (v, w)(v,w) 二元组,代表和对应顶点 u uu 直接相连的顶点数据,w ww 代表 u → v u \to vu→v 的边权,n e x t nextnext 是一个指针,指向下一个 ( v , w ) (v, w)(v,w) 二元组。 ; ?' j7 v/ d: e4 H/ Y + |6 a' U: P: a3 r7 _! L & F/ N# S7 U4 z0 l! o在 C++ 中,还可以使用 vector 这个容器来代替链表的功能; 8 P$ q3 o+ I% R+ L' n) D vector<Edge> edges[maxn]; " _) R. t3 w% ~' S: t& Y) w1 : B! O a$ N' j$ B: a3)前向星, A9 {$ T2 N$ r) @$ n+ y" d
前向星是以存储边的方式来存储图,先将边读入并存储在连续的数组中,然后按照边的起点进行排序,这样数组中起点相等的边就能够在数组中进行连续访问了。2 ]( u! b, S: N& F1 ]1 Y
它的优点是实现简单,容易理解;缺点是需要在所有边都读入完毕的情况下对所有边进行一次排序,带来了时间开销,实用性也较差,只适合离线算法。 & J t* |' n' T1 v% y& y, x如图所示,表示的是三元组 ( u , v , w ) (u, v, w)(u,v,w) 的数组,i d x idxidx 代表数组下标。 " h0 I1 S; K) \( ~# V: N) Z& Y/ _: F
0 J' C; H& |6 _: n# [9 t5 x5 f
那么用哪种数据结构才能满足所有图的需求呢?7 @ K5 ]# D( I9 q- q
接下来介绍一种新的数据结构 —— 链式前向星。 7 B) U( r1 m* J) L+ Y4)链式前向星 ^ K6 A9 _* ?% w: L链式前向星和邻接表类似,也是链式结构和数组结构的结合,每个结点 i ii 都有一个链表,链表的所有数据是从 i ii 出发的所有边的集合(对比邻接表存的是顶点集合),边的表示为一个四元组 ( u , v , w , n e x t ) (u, v, w, next)(u,v,w,next),其中 ( u , v ) (u, v)(u,v) 代表该条边的有向顶点对 u → v u \to vu→v,w ww 代表边上的权值,n e x t nextnext 指向下一条边。 / R4 ~# F, ^ }+ y具体的,我们需要一个边的结构体数组 edge[maxm],maxm表示边的总数,所有边都存储在这个结构体数组中,并且用head来指向 i ii 结点的第一条边。# U$ B* X* Z6 H- ?6 X! s
边的结构体声明如下:9 v8 F& Z0 K& N X( g; h
struct Edge { 5 N" c) G4 X6 M0 ^# ^9 w int u, v, w, next;' V& V6 S2 I3 t. ~( {
Edge() {} $ L5 K$ L! P* U8 [! E Edge(int _u, int _v, int _w, int _next) :) d) S/ W4 r3 o0 K0 X
u(_u), v(_v), w(_w), next(_next) & @$ Y4 p$ a$ n! X% d" Y, L, u$ o% L {' r n/ [9 j# J$ o3 v
}. ^7 P' y m, \
}edge[maxm]; % d/ }" d0 p5 `4 q6 l. U# T17 D5 j1 G: Y9 T% b& Y. O
2 % d' S% W+ c( b# o( O3 * K. E8 ]7 g& [4 4 w) E j' a/ y+ J57 z! R" b+ F8 \9 x5 M$ r
6# {1 {! y8 o5 T+ i" r/ z. l
7 - H; f+ |# ]" b0 c86 E: v3 ? w# e2 Z
初始化所有的head = -1,当前边总数 edgeCount = 0;1 R5 w1 l, n( s, y1 I
每读入一条 u → v u \to vu→v 的边,调用 addEdge(u, v, w),具体函数的实现如下:% M; A* j) G, B" \: ]/ z, ^8 Y; f
void addEdge(int u, int v, int w) { . r0 x i: Q% _, e* u edge[edgeCount] = Edge(u, v, w, head);/ a# F4 g# X1 I9 H" J
head = edgeCount++;. _+ C( E" ?; E( y( J3 `4 {* D
}6 X+ I# H( ~7 {
11 k2 l' E2 R( @/ a9 r, K
2 7 d% l$ Y: O" g1 d$ W2 S* ]+ }3 6 _; V: |+ ^. H6 G/ E" @) J( y2 l4 / Y- o8 E: ]* y: l9 x8 u这个函数的含义是每加入一条边 ( u , v , w ) (u, v, w)(u,v,w),就在原有的链表结构的首部插入这条边,使得每次插入的时间复杂度为 O ( 1 ) O(1)O(1),所以链表的边的顺序和读入顺序正好是逆序的。这种结构在无论是稠密的还是稀疏的图上都有非常好的表现,空间上没有浪费,时间上也是最小开销。 ( X) P% r/ I6 f* s* m8 h) K调用的时候只要通过head就能访问到由 i ii 出发的第一条边的编号,通过编号到edge数组进行索引可以得到边的具体信息,然后根据这条边的next域可以得到第二条边的编号,以此类推,直到 next域为 -1 为止。 ! c1 \8 R1 a" M& [" C* ~for (int e = head; ~e; e = edges[e].next) { ( r, O6 I) ^8 c: O int v = edges[e].v; / K7 u1 i% B# t ValueType w = edges[e].w;- k$ B+ h+ e3 _7 x
...$ \; t+ w$ j. e% n; x
} / P1 @6 _( A4 F- p1' p- R( K+ z$ Q8 j* P. W, Q) T
2 ( @$ |+ h9 L) q3 . a- m6 l9 D+ d# e' F4 8 S5 O: b8 m3 f1 [8 Q2 ]5 9 F$ ^& X1 I6 I+ M, u! w文中的 ~e等价于 e != -1,是对e进行二进制取反的操作(-1 的的补码二进制全是 1,取反后变成全 0,这样就使得条件不满足跳出循环)。 5 i& L9 N* ~5 {4、算法入门 ! W7 e* @+ c; Q$ O- Z算法入门,其实就是要开始我们的刷题之旅了。先给出思维导图,然后一一介绍入门十大算法。6 N1 L" H; n+ M+ {) p
3 a- m8 I n P. I6 z8 A5 d" f, b$ _
2 Q, n) ~- i. Q# N入门十大算法是 枚举、排序、模拟、二分、双指针、差分法、位运算、贪心、迭代、分治。 6 i& g! |4 d& D; d5 U9 _对于这十大算法,我会逐步更新道这个专栏里面:《LeetCode算法全集》。 ' z8 R+ Q# j( e- V Q1、枚举: p! ^: W; ?8 Z1 I- {: G7 d* @
枚举可以简单理解成for循环,从一个数组中遍历查找一个值,就是枚举;从一个数组中找到一个最大值,就是枚举;求数组所有数的和,也是枚举。2 C0 p7 m, z, e2 r4 W
对于枚举而言,基本就是循环语句的语法学会,这个算法就算学会了。 \9 B, p; ? { g! k2、排序2 C* y. Z3 P2 t6 E2 g
既然是入门,千万不要去看快排、希尔排序这种冷门排序。4 O+ F7 B7 S& A# U/ Q5 I+ i
冒泡排序、选择排序、简单插入排序 原理好懂,先看懂再说,其他不管。因为这三者都是基于枚举的。 " S" c1 K" O O6 t* W' r NC中有现成qsort排序函数,C++中有现成 sort排序函数,直接拿来用,等算法进阶时再回头来看快速排序的算法实现。 ' X0 Y* X" k0 t3、模拟8 d( }3 |' Z G3 ]2 k
模拟就是要求做什么,你就做什么,完全不要去考虑效率问题。. `( [& Y3 S* ^" D. A6 d
不管时间复杂度 和 空间复杂度,放手去做!/ B! I; L+ i. k0 n- H
但是,有时候模拟题需要一些复杂的数据结构,所以模拟题难起来也可以很男,难上加难。8 R$ @( U' N( x* r
4、二分 : i' s' q# S$ C二分一般指二分查找,当然有时候也指代二分枚举。 - r+ s5 N" c3 w1 U! z例如,在一个有序数组中查找值,我们一般这个干: b6 k% @1 P/ B5 w) b9 {1 V
1)令初始情况下,数组下标从 0 开始,且数组长度为 n nn,则定义一个区间,它的左端点是 l = 0 l=0l=0,右端点是 r = n − 1 r = n-1r=n−1; - J. ~5 c) c. k+ K% L0 M& E2)生成一个区间中点 m i d = ( l + r ) / 2 mid = (l + r) / 2mid=(l+r)/2,并且判断 m i d midmid 对应的数组元素和给定的目标值的大小关系,主要有三种: k, j e" O# T
2.a)目标值 等于 数组元素,直接返回 m i d midmid;2 G, ~* g# A1 m, X" {! f0 v# P+ Y
2.b)目标值 大于 数组元素,则代表目标值应该出现在区间 [ m i d + 1 , r ] [mid+1, r][mid+1,r],迭代左区间端点:l = m i d + 1 l = mid + 1l=mid+1;: X7 i! M4 f4 ]/ V- H7 g+ ]. d
2.c)目标值 小于 数组元素,则代表目标值应该出现在区间 [ l , m i d − 1 ] [l, mid-1][l,mid−1],迭代右区间端点:r = m i d − 1 r = mid - 1r=mid−1; . x) n. E+ L( i7 [3)如果这时候 l > r l > rl>r,则说明没有找到目标值,返回 − 1 -1−1;否则,回到 2)继续迭代。( k! j* t. m8 b) V
5、双指针 * ~% I, \" g- A" B4 a* C双指针,主要是利用两个下标在一个数组上,根据问题的单调性,进行指针偏移,由于每个指针只往后偏移,所以时间复杂度可以达到 O ( n ) O(n)O(n),由于思想非常简单,所以出题时,热度不低。 % B' N' R4 a$ q3 t0 F' t 8 ^, `/ N8 S7 M% ? 7 [# g) ]9 C0 ^: [0 V" U* D6、差分法; `. e) t% ^" ]1 @) e+ J x
差分法一般配合前缀和。+ v( l, [' v! f& Y1 K
对于区间 [ l , r ] [l, r][l,r] 内求满足数量的数,可以利用差分法分解问题; . |3 f. q) B, T& w. Y假设 [ 0 , x ] [0, x][0,x] 内的 g o o d n u m b e r good \ numbergood number 数量为 g x g_xg * s' `& {$ s0 t5 {7 z8 L) Mx 5 ~8 f; I6 V# ?4 L- U3 p : ~; b3 T7 h* S. Q! t* z ,那么区间 [ l , r ] [l, r][l,r] 内的数量就是 g r − g l − 1 g_r - g_{l-1}g / \! J( |2 [, H8 l; N+ ~
r $ _! t$ a0 r8 C7 s0 h5 p5 g1 B % u7 T T! h1 a% H$ C- Q# {) d& K
−g 7 V$ w' r! D5 `# V) R4 T4 Y+ bl−13 i1 q* M& F, j
D4 S' h* L+ S8 P* m z
;分别用同样的方法求出 g r g_rg ; {, L( R. H' o( ur. @( M) C6 k7 E' r# i4 V- {. y8 W
, v* S6 s% B% }) i6 g 和 g l − 1 g_{l-1}g . D+ O" Y5 N, j, ?, V6 \
l−1 7 L, |) ?8 K& m5 R' t; M 4 j# z5 e& q7 ?1 H& A ,再相减即可;9 T/ _' w$ u7 r5 ~8 I
) f% W* `- f0 w& l$ H/ ~) F
9 N) N/ @6 ?# y4 f7、位运算& z5 ?2 I" y- e
位运算可以理解成对二进制数字上的每一个位进行操作的运算。 , q$ K4 p/ I3 x2 t位运算分为 布尔位运算符 和 移位位运算符。 . ~+ `5 b' `. P" @7 f布尔位运算符又分为 位与(&)、位或(|)、异或(^)、按位取反(~);移位位运算符分为 左移(<<) 和 右移(>>)。 - x% J3 O8 ]9 i) k1 L* ^如图所示: a& L0 g- ~" b( K" s1 }3 R1 |% K' ~9 k$ [4 w
5 z" c7 a i# {0 M# d. |6 |; _7 u
位运算的特点是语句短,但是可以干大事!, T+ I) I# X; X$ `9 a9 `
比如,请用一句话来判断一个数是否是2的幂,代码如下: 8 @& z8 i- h. k* g. y* Q4 K& ]( x!(x & (x - 1)) " V0 u8 B$ P7 V' |, p: \2 Q4 Y" C2 B1- d. K, n, ?& Q5 h
8、贪心8 T' o( E2 M/ M; q
贪心,一般就是按照当前最优解,去推算全局最优解。' k0 A% ]. I, o2 ]. b
所以,只有当当前最优解和全局最优解一致时才能用贪心算法。贪心算法的证明是比较难的,但是一些简单的贪心问题会比较直观,很容易看出来这个能够这么贪。 ! D. P. z: k% E" z3 T( k e- Y0 g" A9、迭代 4 @7 Y% T& Z3 q5 ~4 a每一次对过程的重复称为一次“迭代”,而每一次迭代得到的结果会作为下一次迭代的初始值,周而复始,直到问题全部解决。( h/ Z6 H; j- M# O6 v, J6 n
10、分治 ! n6 R3 W2 r3 Z. l分治,就是把问题分成若干子问题求解,子问题解决后,问题就解决了。一般利用递归实现。属于初学者比较头疼的内容。递归一开始学习的时候,一定要注意全局变量和局部变量的关系。 " e2 s9 S3 Q: O' k0 y3 Z5、算法进阶3 ]' e" C# W$ m
算法进阶这块是我打算规划自己未来十年去完成的一个项目,囊括了 大学生ACM程序设计竞赛、高中生的OI竞赛、LeetCode 职场面试算法 的算法全集,也就是之前网络上比较有名的 《夜深人静写算法》 系列,这可以说是我自己对自己的一个要求和目标吧。! U* U% D$ W; a/ n! R& e! v
如果只是想进大厂,那么 算法入门 已经足够了,不需要再来看算法进阶了,当然如果对算法有浓厚兴趣,也欢迎和我一起打卡。由于内容较难,工作也比较忙,所以学的也比较慢,一周基本也只能更新一篇。8 \' F0 D/ [3 M
这个系列主要分为以下几个大块内容: 8 }8 l4 L% Y* ~( O 1)图论 % F/ a, T. u0 }: n0 U" G# L 2)动态规划0 n- ]# y1 ` h4 q
3)计算几何 , M) \# ]! [; ?: b3 W: \ 4)数论 : o/ y/ P! Q' O- C 5)字符串匹配: m7 q/ h. n! C7 u1 s
6)高级数据结构(课本上学不到的)" n4 w9 J2 N* ?3 ~
7)杂项算法 " D% n% M O o {* ^1 a8 D4 q3 q3 K& J" }: T9 J
. g0 K# u/ |4 c0 b" A先来看下思维导图,然后我大致讲一下每一类算法各自的特点,以及学习方式:; Q/ O( v; Y" i8 @& D4 ?6 F I3 A
8 J$ J. a9 R6 _7 Z( E
$ B$ x! J+ z; D " [+ k6 C; z# ]8 r3 S1 K如果子问题的数目为 O ( n t ) O(n^t)O(n 2 f' ]0 \ G6 s; L1 B. ?t , t! k' G4 o* C$ i" Y" U7 A. A) ~ ),每个子问题需要用到 O ( n e ) O(n^e)O(n $ W# s* T- t3 H* E
e . v3 ~9 V4 @( P: R1 \ ) 个子问题的结果,那么我们称它为 tD/eD 的问题,于是可以总结出四类常用的动态规划方程:(下面会把opt作为取最优值的函数(一般取 m i n minmin 或 m a x maxmax ), w ( j , i ) w(j, i)w(j,i)为一个实函数,其它变量都可以在常数时间计算出来)。& q6 I! C' {; k$ f; L
1、1D/1D ! s3 e- o |% ^* ^) sd [ i ] = o p t ( d [ j ] + w ( j , i ) ∣ 0 < = i < j ) d = opt( d[j] + w(j, i) | 0 <= i < j )* Y7 O& Y/ y, P0 I" |4 E' _7 u
d=opt(d[j]+w(j,i)∣0<=i<j) 3 h( h, H; F1 y$ k) b% ~状态转移如图四所示(黄色块代表d [ i ] dd,绿色块代表d [ j ] d[j]d[j]):2 L2 p+ u I& n" F; v
- \0 d: U! l, l8 i 9 W" J3 n2 L" U, F- c这类状态转移方程一般出现在线性模型中。 * H6 X3 |' d1 e2、2D/0D- k& V& N2 |4 a A
d [ i ] [ j ] = o p t ( d [ i − 1 ] [ j ] + x i , d [ i ] [ j − 1 ] + y j , d [ i − 1 ] [ j − 1 ] + z i j ) d[j] = opt( d[i-1][j] + x_i, d[j-1] + y_j, d[i-1][j-1] + z_{ij} )/ P7 \ x0 u/ ?" w
d[j]=opt(d[i−1][j]+x * ^* k' c- ?- Pi 4 \3 \* o }/ t8 L 6 @% Y" Q& g/ _ A7 W- m8 w ,d[j−1]+y ; h' h1 _: I! |$ w% e6 Q9 l. b
j . O& j- W+ u% l& k4 O( f ' a. d" z- t! L3 _6 S/ Y, I n5 z ,d[i−1][j−1]+z 6 m& S1 c$ D# L- J
ij$ p; ~( J& x- @. [( X. i" o* C
( \* _2 T0 S4 F8 ~4 E4 I0 P8 c" w ), s' Z. L% g0 S" m, Z' z
状态转移如图四所示: 6 r) j% s6 _: u! J ~4 m, r) G. K6 E. U7 m, X
: z: Q; A8 j% Y5 n' r, `& G$ ^比较经典的问题是最长公共子序列、最小编辑距离。 $ ] Y; d# r. p- e! S! ^6 w2 y有关最长公共子序列的问题,可以参考以下文章:夜深人静写算法(二十一)- 最长公共子序列 3 c4 {0 L' y* l6 X0 f0 n3 q7 A有关最小编辑距离的问题,可以参考以下文章:夜深人静写算法(二十二)- 最小编辑距离 * L+ x+ A$ M* D- ^! _* w/ W9 |% I3、2D/1D + l, ]& S5 S* {. `2 ], J" {' r: n4 zd [ i ] [ j ] = w ( i , j ) + o p t ( d [ i ] [ k − 1 ] + d [ k ] [ j ] ) d[j] = w(i, j) + opt( d[k-1] + d[k][j] ) ) O+ c. n# ^6 D* e) f) A% r/ o. cd[j]=w(i,j)+opt(d[k−1]+d[k][j]). b+ e4 D3 [: r
区间模型常用方程,如图所示: : F* L! Z4 X0 V! c% b7 Q" Z( z' I- a# y* u
) k: K0 |7 Z/ o: G. D! ~
另外一种常用的 2D/1D 的方程为:9 Y7 I3 l. }8 B
d [ i ] [ j ] = o p t ( d [ i − 1 ] [ k ] + w ( i , j , k ) ∣ k < j ) d[j] = opt( d[i-1][k] + w(i, j, k) | k < j )/ g! D! w }1 J5 ?6 r+ n
d[j]=opt(d[i−1][k]+w(i,j,k)∣k<j): [2 U9 M5 B3 y
区间模型的详细内容可以参考以下这篇文章:夜深人静写算法(二十七)- 区间DP# S i: F) \6 X
4、2D/2D9 _4 s5 j, k5 ~9 Z6 o/ M6 ?* Z- V& ^' P
d [ i ] [ j ] = o p t ( d [ i ′ ] [ j ′ ] + w ( i ′ , j ′ , i , j ) ∣ 0 < = i ′ < i , 0 < = j ′ < j ) d[j] = opt( d[i'][j'] + w(i', j', i, j) | 0 <= i' < i, 0 <= j' < j)* U, }; W6 Z- M& Z' N) M" A, i
d[j]=opt(d[i # p, q0 p. g8 s7 p K! x′ 1 Y, B6 S9 s; d4 J ][j 1 u+ d) n: d0 f4 o. `′& ~: G" `0 v% I
]+w(i ; R7 F- m/ O8 V+ \/ {
′6 m: M9 J6 R7 f, C% z" p
,j % k* |6 @* k1 y′ . B. F7 x- z B/ J3 A5 R ,i,j)∣0<=i 2 U7 E; f+ `: O6 u( I′5 V% C0 v: h; a- l6 a' {5 j4 J
<i,0<=j 8 U# O; p/ n9 ?# V' D
′% F6 W: ^* D" Z6 P
<j)7 U8 I4 d- O$ ^; F1 Q
如图所示: 9 ~) G* N+ d/ p; t& E2 @/ N# V R0 b, u
% N, ]0 `* ]. d; C# J
常见于二维的迷宫问题,由于复杂度比较大,所以一般配合数据结构优化,如线段树、树状数组等。 ( f" p) M& N7 E$ ?$ F对于一个tD/eD 的动态规划问题,在不经过任何优化的情况下,可以粗略得到一个时间复杂度是O ( n t + e ) O(n^ {t+e})O(n ! O4 m: |" m7 J) Z5 qt+e: `, x3 N4 e/ S
),空间复杂度是O ( n t ) O(n^t)O(n 4 U0 g! M2 S9 {% S1 A
t# y9 o: }6 V' [$ f: w
) 的算法,大多数情况下空间复杂度是很容易优化的,难点在于时间复杂度,后续章节将详细讲解各种情况下的动态规划优化算法。 S% _& O8 `! P4 E
3)计算几何! c! t2 s2 ?- z/ v0 F
计算几何的问题是代码量最大的。它是计算机科学的一个分支,以往的解析几何,是用代数的方法,建立坐标系去解决问题,但是很多时候需要付出一些代价,比如精度误差,而计算几何更多的是从几何角度,用向量的方法来尽量减少精度误差,例如:将除法转化为乘法、避免三角函数等近似运算 等等。! [" s1 R# N5 H- C: c# J
如果一个比赛中,有一道计算几何的题,那么至少,它不会是一道水题。8 x4 p% ~( ^; h% N1 L
1、double 代替 float " X$ w" L6 z; `c++ 中 double 的精度高于 float,对精度要求较高的问题,务必采用 double; 7 h; ]" y# b0 z* l* Y1 U2、浮点数判定 ' G( a* a( k" w4 z" N( x4 K, u由于浮点数(小数)中是有无理数的,即无限不循环小数,也就是小数点后的位数是无限的,在计算机存储的时候不可能全部存下来,一定是近似的存储的,所以浮点数一定是存在精度误差的(实际上,就算是有理数,也是存在误差的,这和计算机存储机制有关,这里不再展开,有兴趣可以参见我博客的文章:C++ 浮点数精度判定);, m* y8 V5 R5 j
两个浮点数是否相等,可以采用两数相减的绝对值小于某个精度来实现: - c% W8 w1 w+ Aconst double eps = 1e-8; 9 y0 z4 I! t+ Dbool EQ(double a, double b) {5 E# P! g1 s% G: ]9 Y/ }% v
return fabs(a - b) < eps; 2 p# P8 r; D! }} 6 j' Z b+ Q& ]1 M* p1! s+ s$ c" x# J. T. p! n
2 + |* M0 T+ `& t2 R3 7 C; o S- P. d2 @+ Q4 # a( |, V9 [& K3 y+ e+ D并且可以用一个三值函数来确定某个数是零、大于零还是小于零: 4 R7 G2 `( }/ z# Dint threeValue(double d) {" n! h) l4 R3 ]+ F
if (fabs(d) < eps), I8 k Y$ C1 D; G
return 0;+ u" R' ^; c* g8 k: }% M
return d > 0 ? 1 : -1;) L! l4 g, b& b Q! J7 ]# }
} & ?( P" {0 O, o( h- a& d& I! O4 g1 0 y7 |& G W- `3 H+ k- o$ h28 v- B, X1 I: z. m! i0 `% f, q( V4 t0 h
3( h8 e5 w$ w, }/ b
4 : {: V) ]! `+ ]9 D0 q54 j3 l& a U! Q1 g
3、负零判定 5 Y1 T7 d. n2 u因为精度误差的存在,所以在输出的时候一定要注意,避免输出 -0.00: 0 \( }/ r1 J3 L- b4 @" T7 z double v = -0.0000000001; ! v& J1 M! _- ?3 L* k printf("%.2lf\n", v); 0 ~/ h! w, \' Z) X( v! _" X6 B14 P# [6 E) g6 A
2 & s+ s/ L3 l( L* N+ w避免方法是先通过三值函数确定实际值是否为0,如果是0,则需要取完绝对值后再输出: & i& R2 U4 `+ M1 L2 m- U' s( _ double v = -0.0000000001;* J2 o8 |" p) A+ B
if(threeValue(v) == 0) { 1 V3 S" E' N- K7 \) } v = fabs(v); ! Y" ^' H% L; s3 s( x } $ J! |0 U; w3 a. A `9 M6 W' w) X printf("%.2lf\n", v); / X. q% P: m. l1 c9 s* f! W, F13 `4 C; _, Z5 Y* T5 b
2 ! [- D' N( X' t6 f. F4 G- k) n4 C3! O9 Z& t6 Y, z: N0 ]
4: |9 l, S+ O; Y4 g+ B. I. T
56 R5 r, t. \0 j6 Q5 b
4、避免三角函数、对数、开方、除法等 z8 s' _. P) r( i) o9 x9 y
c++ 三角函数运算方法采用的是 CORDIC算法,一种利用迭代的方式进行求解的算法,其中还用到了开方运算,所以实际的算力消耗还是很大的,在实际求解问题的过程中,能够避免不用就尽量不用。 5 X U; G& ^0 n1 D4 v2 ?5 s除法运算会带来精度误差,所以能够转换成乘法的也尽量转换为乘法运算。" N G. o" ~! u, H
5、系统性的学习 & K( X; r) T9 J; d5 \7 ~基础知识:点、向量、叉乘、点乘、旋转、线段、线段判交、三角形面积;- p" N/ C; K) m9 Y" v
进阶知识:多边形面积、凸多边形判定、点在多边形内判定;7 Q( o) a# N5 I3 p/ y# w! q: r
相关算法:二维凸包、三维凸包、旋转卡壳、多边形面积交、多边形面积并、多边形面积异或、多边形和圆的面积交、半平面交、最小覆盖圆、最小包围球、模拟退火。; P9 r: X# E# [2 B& b