: H( G9 r3 F1 s2 E8 S! W1、图的概念* U$ ?; }& [2 f6 e: z( b
在讲解最短路问题之前,首先需要介绍一下计算机中图(图论)的概念,如下: T* H! v/ _, a( E
图 G GG 是一个有序二元组 ( V , E ) (V,E)(V,E),其中 V VV 称为顶点集合,E EE 称为边集合,E EE 与 V VV 不相交。顶点集合的元素被称为顶点,边集合的元素被称为边。- A/ c, t% i. e# J
对于无权图,边由二元组 ( 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 为权值,可以是任意类型。 * a) R$ s5 v* M: E7 ~4 }; p图分为有向图和无向图,对于有向图, ( 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; k0 J' y2 H: S' Z& C
2、图的存储 7 l9 Y8 Q) |* u+ V. Y d对于图的存储,程序实现上也有多种方案,根据不同情况采用不同的方案。接下来以图二-3-1所表示的图为例,讲解四种存储图的方案。 & O# L) ]7 j7 N6 G+ j3 F9 U0 E0 n D$ `& h
0 X$ T6 K0 ]: C! N% K1)邻接矩阵 - K, K# \! L4 A邻接矩阵是直接利用一个二维数组对边的关系进行存储,矩阵的第 i ii 行第 j jj 列的值 表示 i → j i \to ji→j 这条边的权值;特殊的,如果不存在这条边,用一个特殊标记 ∞ \infty∞ 来表示;如果 i = j i = ji=j,则权值为 0 00。, b; [7 r, w$ L$ g3 |, F, c
它的优点是:实现非常简单,而且很容易理解;缺点也很明显,如果这个图是一个非常稀疏的图,图中边很少,但是点很多,就会造成非常大的内存浪费,点数过大的时候根本就无法存储。& L( X" Q1 |0 Y: c# q! J7 Q+ E
[ 0 ∞ 3 ∞ 1 0 2 ∞ ∞ ∞ 0 3 9 8 ∞ 0 ] \left[/ H; k( J- O: q# d& l% q; r7 C
01∞9∞0∞8320∞∞∞30( \. V$ x/ B/ o- z- p- v
0∞3∞102∞∞∞0398∞0 % @( S5 i. U6 _# ]$ ]\right] - d$ |6 j8 Q& V% p0 X; E R9 H⎣ ; ~% @9 M9 o( o1 X, L: h2 K! A⎢2 [) [) b, f6 @# Y9 N7 q
⎢ + c- G; ~/ o! W" k5 r⎡, F2 b3 A' h) S; N! ^7 r- h
4 t3 @+ q# r$ R( E, s % ~# ]9 M) E/ b1 Q I
0; c: z) i% z4 E$ g- ^4 h" @
1$ M/ \% d+ i0 ?: D1 U
∞- n5 e" `0 B; k% u0 `1 b
9 ( g( D# a2 M( m) g $ _6 P$ K) m6 J( ]9 v+ Y x- }" }6 b& c- E+ X4 ^
∞2 }3 R# d, w. @; |
0 ) |" p& A) s/ Z' L. f% C5 q1 R∞. \- Q$ U0 F; y2 k
86 |" H8 s" g& G1 d. ?
' v# ~' T+ x& k% [/ g* Q
}! P8 I; S% C6 t D2 J3 b+ u/ @# Z. \ J1 o! F8 _2 n
2 : A' @. I3 g4 ]4 U9 k6 S# N0 0 V4 Z3 @ Q7 @∞ , }8 B( {0 @1 U5 N7 s5 h. I & A8 N( s M; p3 C1 \ ; _9 E' A: s C1 x; \0 r
∞3 y. z, w* y) |7 V% d
∞ 8 X% C: B+ {" M' t3 4 a& y0 ^! _4 X/ n: z0* g) a: d& d: w& p! T) @2 x1 v: ^
4 K/ x) d6 m: f5 g % Y6 h: U% G# c& I8 t
⎦3 P* ^, A$ z. Y
⎥" {0 j# b" `$ j2 I( c) c
⎥* A: [. n, Y/ R% l# [8 Y$ g: l
⎤ " f/ O' Z. ^- v# p) b. u- }/ J $ c: v2 _( l: d6 H4 B8 }
( c' m2 S7 T4 A# P5 }) c2)邻接表$ E% m' @* {9 R( f
邻接表是图中常用的存储结构之一,采用链表来存储,每个顶点都有一个链表,链表的数据表示和当前顶点直接相邻的顶点的数据( v , w ) (v, w)(v,w),即 顶点 和 边权。. @$ A0 J% z+ C5 A
它的优点是:对于稀疏图不会有数据浪费;缺点就是实现相对邻接矩阵来说较麻烦,需要自己实现链表,动态分配内存。 # u2 L0 q; A3 Q- E8 K如图所示,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) 二元组。! w+ ^, U/ f+ Z1 i6 ^; _
: z+ T6 X) R! u! `: `
8 h. c$ N, Z' c8 U/ I5 N在 C++ 中,还可以使用 vector 这个容器来代替链表的功能; 7 ~( y# R. X8 N2 w vector<Edge> edges[maxn]; 7 a( L! R( x, p1; Q2 t7 n7 T5 D
3)前向星 1 C" ~3 ^" K" ]4 H, k& m. p+ [前向星是以存储边的方式来存储图,先将边读入并存储在连续的数组中,然后按照边的起点进行排序,这样数组中起点相等的边就能够在数组中进行连续访问了。3 |: s5 W9 _/ U( T! t% n- l/ E
它的优点是实现简单,容易理解;缺点是需要在所有边都读入完毕的情况下对所有边进行一次排序,带来了时间开销,实用性也较差,只适合离线算法。3 k" w Q% B4 k( W! R. A3 l
如图所示,表示的是三元组 ( u , v , w ) (u, v, w)(u,v,w) 的数组,i d x idxidx 代表数组下标。 4 `( |# i7 y J. L, i0 @9 b % {1 `4 d6 ^! J! h! ?1 p7 Q4 P' L; z5 Q) |4 O
那么用哪种数据结构才能满足所有图的需求呢?% A) I# i2 x0 J0 \+ S
接下来介绍一种新的数据结构 —— 链式前向星。 * s- j; q% z ~5 Q# ~4)链式前向星. O+ H) }9 q3 x( J* C+ h
链式前向星和邻接表类似,也是链式结构和数组结构的结合,每个结点 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 指向下一条边。# q) w2 N9 E: F* P! s
具体的,我们需要一个边的结构体数组 edge[maxm],maxm表示边的总数,所有边都存储在这个结构体数组中,并且用head来指向 i ii 结点的第一条边。! V! ~0 O0 F1 h. b
边的结构体声明如下:) _8 z" q! I$ T7 x3 ~0 n) }
struct Edge { $ @# N3 T- k$ Y- n int u, v, w, next;7 r1 X- u, e$ x% L! [
Edge() {}& D" @) N6 a/ U$ P1 D2 f* K; b# d6 L3 ~
Edge(int _u, int _v, int _w, int _next) : 2 J+ }1 q# e3 H& S- e3 X u(_u), v(_v), w(_w), next(_next) 2 U; I3 @4 K \' x* s {0 W0 d6 O0 I. @( }. X9 A1 c3 d
} . r6 E1 p8 \7 n# y5 \3 H& \}edge[maxm];! p! U, q% ], @2 _/ }: v
15 X7 G6 W7 @; n' V4 Z+ y( C2 ^
2 . @/ \/ A9 N$ {5 U; q( `8 C3 ! L6 R& I& Y: }; c4 ) I* A) w( {1 k0 J4 K% X. X5 5 f& p3 G f& m7 |0 N6& c. t" O( I* T% `
73 I7 a, y. C* `5 B g# ^
8( S$ |3 B F# G) C+ _
初始化所有的head = -1,当前边总数 edgeCount = 0;7 x" L/ f6 C3 y# T: z
每读入一条 u → v u \to vu→v 的边,调用 addEdge(u, v, w),具体函数的实现如下: 6 O% s, D0 }+ ^. qvoid addEdge(int u, int v, int w) {9 S k* \' d# e2 W1 O4 R
edge[edgeCount] = Edge(u, v, w, head);- S2 a4 i5 D. ^: G/ _& J8 w, e
head = edgeCount++;2 c* }) c- h- }; q6 G
}' j: s# E9 o3 `3 Y7 d
1 8 T, Z* W/ }$ w5 E2 g2 4 ^; }) E+ R t+ h+ J3 W0 S' A" o( g7 Y& t2 Y
4$ ]) P" R8 ^' X0 R! a W
这个函数的含义是每加入一条边 ( u , v , w ) (u, v, w)(u,v,w),就在原有的链表结构的首部插入这条边,使得每次插入的时间复杂度为 O ( 1 ) O(1)O(1),所以链表的边的顺序和读入顺序正好是逆序的。这种结构在无论是稠密的还是稀疏的图上都有非常好的表现,空间上没有浪费,时间上也是最小开销。 + K1 M4 u; _! R调用的时候只要通过head就能访问到由 i ii 出发的第一条边的编号,通过编号到edge数组进行索引可以得到边的具体信息,然后根据这条边的next域可以得到第二条边的编号,以此类推,直到 next域为 -1 为止。 & P: c8 n+ b0 Y1 }" X4 `for (int e = head; ~e; e = edges[e].next) { ' L2 L5 Z v8 o$ M+ U/ j" u u int v = edges[e].v; ( p' _& ]6 E% z- S ValueType w = edges[e].w;4 A) a* A6 a( Q9 O# K
...4 w. l8 t3 U" B# Z( _
}, Y& c6 f7 Z2 R7 r
1 0 p; Q( g6 U7 Q( k8 L/ z29 _$ x; R! p0 t
3 $ K, P* K; q3 g6 ]+ _/ ~: w+ q# n% \4* s3 Q9 ]8 M [7 t, O J
5 8 U+ }2 @! _! C& u3 p9 A: P* u9 _- G文中的 ~e等价于 e != -1,是对e进行二进制取反的操作(-1 的的补码二进制全是 1,取反后变成全 0,这样就使得条件不满足跳出循环)。# s5 u! k! S. z$ `1 M+ M( h2 u/ l
4、算法入门 & U; B, j' X% E! o9 a. x8 P算法入门,其实就是要开始我们的刷题之旅了。先给出思维导图,然后一一介绍入门十大算法。: [4 z* O$ m2 C/ n$ r
$ W: M' [( w+ r6 |# w : {, F& G/ u' i入门十大算法是 枚举、排序、模拟、二分、双指针、差分法、位运算、贪心、迭代、分治。5 _$ s* [8 t: C$ q9 ^. `) ~
对于这十大算法,我会逐步更新道这个专栏里面:《LeetCode算法全集》。6 n9 W- ]% g% L- v+ N: F
1、枚举 7 X# n9 z& }( U* V* }枚举可以简单理解成for循环,从一个数组中遍历查找一个值,就是枚举;从一个数组中找到一个最大值,就是枚举;求数组所有数的和,也是枚举。 - _* W+ w. `& F$ @' y7 e; Y对于枚举而言,基本就是循环语句的语法学会,这个算法就算学会了。' ] l- t; H! \9 f4 m
2、排序 5 s' T0 Q/ P% f. E既然是入门,千万不要去看快排、希尔排序这种冷门排序。% b% e" M# w2 g
冒泡排序、选择排序、简单插入排序 原理好懂,先看懂再说,其他不管。因为这三者都是基于枚举的。- _) S5 C0 h* X M# l0 `3 j a
C中有现成qsort排序函数,C++中有现成 sort排序函数,直接拿来用,等算法进阶时再回头来看快速排序的算法实现。 & n1 k5 }4 o* v- d3、模拟 % K4 c7 W0 F! m. e" }+ V; Y模拟就是要求做什么,你就做什么,完全不要去考虑效率问题。) ]2 D8 ~9 Z, C5 b; W
不管时间复杂度 和 空间复杂度,放手去做! " u6 Z) l5 d, \% C" ]$ m但是,有时候模拟题需要一些复杂的数据结构,所以模拟题难起来也可以很男,难上加难。 3 {/ A) g5 D8 y9 j: W" t4、二分 1 e" [. Q+ F7 P6 E二分一般指二分查找,当然有时候也指代二分枚举。 9 v' }8 i" J- G: {4 C3 [( H& k2 q* ?例如,在一个有序数组中查找值,我们一般这个干: : m @, y5 `- J6 B+ ?1)令初始情况下,数组下标从 0 开始,且数组长度为 n nn,则定义一个区间,它的左端点是 l = 0 l=0l=0,右端点是 r = n − 1 r = n-1r=n−1; " l8 B7 i3 H* j b2)生成一个区间中点 m i d = ( l + r ) / 2 mid = (l + r) / 2mid=(l+r)/2,并且判断 m i d midmid 对应的数组元素和给定的目标值的大小关系,主要有三种: 8 a) W. @ Q' f% a 2.a)目标值 等于 数组元素,直接返回 m i d midmid;% L6 t+ A# t0 V# p3 g& e
2.b)目标值 大于 数组元素,则代表目标值应该出现在区间 [ m i d + 1 , r ] [mid+1, r][mid+1,r],迭代左区间端点:l = m i d + 1 l = mid + 1l=mid+1; * E7 H# ~" P7 ~- W 2.c)目标值 小于 数组元素,则代表目标值应该出现在区间 [ l , m i d − 1 ] [l, mid-1][l,mid−1],迭代右区间端点:r = m i d − 1 r = mid - 1r=mid−1; 8 M+ S4 O; O- ?- u- m3)如果这时候 l > r l > rl>r,则说明没有找到目标值,返回 − 1 -1−1;否则,回到 2)继续迭代。! A$ N" S0 K9 R, F' e$ p4 e/ O
5、双指针# R; k- o0 ~- `" p; D
双指针,主要是利用两个下标在一个数组上,根据问题的单调性,进行指针偏移,由于每个指针只往后偏移,所以时间复杂度可以达到 O ( n ) O(n)O(n),由于思想非常简单,所以出题时,热度不低。# \; U8 f+ I- |+ t0 v
' k2 X/ Q+ Z$ q* B
) P2 N6 h+ g4 ]
6、差分法* a1 W; Z8 n+ y7 Z- D! Z
差分法一般配合前缀和。 * N) h0 F: d$ k对于区间 [ l , r ] [l, r][l,r] 内求满足数量的数,可以利用差分法分解问题; , C( i9 o5 b7 @# x" ]& x* x, `假设 [ 0 , x ] [0, x][0,x] 内的 g o o d n u m b e r good \ numbergood number 数量为 g x g_xg ! r- a% o/ n5 w1 P7 b# M6 d8 o* `
x& [3 V& Y! G8 ]3 x7 f, @
9 M$ `& B- ^! | Z
,那么区间 [ l , r ] [l, r][l,r] 内的数量就是 g r − g l − 1 g_r - g_{l-1}g ' _! ]( y. N. q" E5 M
r 9 P2 v: @6 ^' L. } " ]3 a) V1 \6 Y$ h$ W: d7 B' W −g 0 B4 s3 \" N, s7 G+ v0 pl−1 $ i. P2 m! i, y0 R/ p 3 y7 g5 V+ r7 K0 q+ {6 S
;分别用同样的方法求出 g r g_rg 5 b9 p4 \; @0 { Sr0 g+ D4 [$ N. O _( v5 Q! y
: P8 c) c) a' ~: K 和 g l − 1 g_{l-1}g 5 b$ T/ G' C7 @% P9 Bl−1 % N0 Z/ N- j4 X " q2 v6 s$ W$ {8 l/ i ,再相减即可;$ O, v% k- w0 q. H, A U% e: P
f) H0 m s; A! M- W7 v: T r 0 [+ |% z" {" i! \5 M C" r5 @7、位运算7 ]. [+ R- L' z8 |1 J" W' \9 w% M
位运算可以理解成对二进制数字上的每一个位进行操作的运算。$ `- y% k1 I& k; |+ ^
位运算分为 布尔位运算符 和 移位位运算符。 0 A1 u S3 a9 E! U6 d* L. X布尔位运算符又分为 位与(&)、位或(|)、异或(^)、按位取反(~);移位位运算符分为 左移(<<) 和 右移(>>)。 5 x# Q0 L2 E# B% Q如图所示: # s' ~+ b: H* u( K; d: M" O( i3 _" [9 F( V7 J. r+ S: h2 `, z
3 ^5 c3 { n' c4 p) s位运算的特点是语句短,但是可以干大事! 6 U6 E3 E9 n8 i5 D i比如,请用一句话来判断一个数是否是2的幂,代码如下: - m ?# p r) v0 O6 {) ?!(x & (x - 1))$ i, a+ z9 U5 l+ X- _
17 N" u5 t4 e! n4 X0 N
8、贪心 # b: Z9 Z! q. A r/ Y9 m: ~6 X贪心,一般就是按照当前最优解,去推算全局最优解。 , y2 F" X5 C5 }( o! M9 \所以,只有当当前最优解和全局最优解一致时才能用贪心算法。贪心算法的证明是比较难的,但是一些简单的贪心问题会比较直观,很容易看出来这个能够这么贪。 - }& N, @# J* @1 O9、迭代$ I B; n& ]' M7 f; r
每一次对过程的重复称为一次“迭代”,而每一次迭代得到的结果会作为下一次迭代的初始值,周而复始,直到问题全部解决。+ k! s. Y, n7 X" o) h- Z2 G: _
10、分治 8 Q! v4 x7 {* P/ f l& H分治,就是把问题分成若干子问题求解,子问题解决后,问题就解决了。一般利用递归实现。属于初学者比较头疼的内容。递归一开始学习的时候,一定要注意全局变量和局部变量的关系。 6 }& j4 r! |9 U- U5、算法进阶2 R$ r( A# }6 B
算法进阶这块是我打算规划自己未来十年去完成的一个项目,囊括了 大学生ACM程序设计竞赛、高中生的OI竞赛、LeetCode 职场面试算法 的算法全集,也就是之前网络上比较有名的 《夜深人静写算法》 系列,这可以说是我自己对自己的一个要求和目标吧。, G6 M9 Q1 d7 F/ e
如果只是想进大厂,那么 算法入门 已经足够了,不需要再来看算法进阶了,当然如果对算法有浓厚兴趣,也欢迎和我一起打卡。由于内容较难,工作也比较忙,所以学的也比较慢,一周基本也只能更新一篇。 # v) E" j+ G* B M6 c1 T这个系列主要分为以下几个大块内容: 7 P7 x$ t& R# ~; k* J 1)图论 - `7 ]3 ?$ k ^6 E 2)动态规划 ; r4 R. y* E9 p& D; m" w 3)计算几何 5 E2 I" |+ ~3 b% z, E 4)数论 \" x ^3 ~( e, C/ A# n
5)字符串匹配( |5 W! W5 b# u5 q
6)高级数据结构(课本上学不到的)5 a/ A x K$ G/ W
7)杂项算法 + e" Q8 Q* m/ \9 _8 A6 [ G9 G0 `
( A. M2 ?1 V1 g- u
先来看下思维导图,然后我大致讲一下每一类算法各自的特点,以及学习方式:4 H& X" ?" S9 W6 n1 E, r
5 ~; m4 p: A! p1 h: X. p. o, R! i' u$ E1 ]0 G( a
5 P. T \5 H3 `: O- L2 F+ H% s/ g8 q
, s5 a7 U* u3 p) g
从图中可以看出,广搜的本质还是暴力枚举。即对于每个当前位置,枚举四个相邻可以行走的方向进行不断尝试,直到找到目的地。有点像洪水爆发,从一个源头开始逐渐蔓延开来,直到所有可达的区域都被洪水灌溉,所以我们也把这种算法称为 FloodFill。3 P; l5 W" _( E0 h- m8 k
那么,如何把它描述成程序的语言呢?这里需要用到一种数据结构 —— 队列。3 [9 h* S/ y. S: E3 V/ N) i, M. A
这时候,算法和数据结构就完美结合了。 $ ^5 {0 z( C. \; m' }2)动态规划 + n+ b3 ^) [2 [+ v动态规划算法三要素: * [. g; f: U# r! r5 t- C" } ①所有不同的子问题组成的表; . B6 e) P, w' x w1 t, D0 r ②解决问题的依赖关系可以看成是一个图;- N: M$ z! n7 ]8 R, l
③填充子问题的顺序(即对②的图进行拓扑排序,填充的过程称为状态转移); 5 Q. V$ J+ H5 j, F* K: v ) j) J. ~6 [2 C ( R- t6 [4 h7 t2 F. I5 M4 ~" E$ ]如果子问题的数目为 O ( n t ) O(n^t)O(n " `% k: B( l& j& o) Y) Ft& G. d7 U8 E& \+ R
),每个子问题需要用到 O ( n e ) O(n^e)O(n # z Y& n' y. b+ i. R3 Ue, U" h$ X- D9 `6 L) d
) 个子问题的结果,那么我们称它为 tD/eD 的问题,于是可以总结出四类常用的动态规划方程:(下面会把opt作为取最优值的函数(一般取 m i n minmin 或 m a x maxmax ), w ( j , i ) w(j, i)w(j,i)为一个实函数,其它变量都可以在常数时间计算出来)。7 y5 a5 W }: X I
1、1D/1D _7 ?2 r& o& U0 c4 n( w- ]
d [ i ] = o p t ( d [ j ] + w ( j , i ) ∣ 0 < = i < j ) d = opt( d[j] + w(j, i) | 0 <= i < j )- s5 P0 ` s. q+ @3 i4 z
d=opt(d[j]+w(j,i)∣0<=i<j) 1 d$ `% W' |7 z( C4 J状态转移如图四所示(黄色块代表d [ i ] dd,绿色块代表d [ j ] d[j]d[j]):8 i& p+ k* X F
" d) l4 |) ]; c9 Z8 n( V6 r4 q+ S( S( V _$ c) m+ A5 U* m+ I( B
这类状态转移方程一般出现在线性模型中。: T9 w3 |! H" n) e
2、2D/0D / E) ?8 v T% G/ xd [ 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} ) / H6 c r* g1 G. z" n0 M Qd[j]=opt(d[i−1][j]+x 5 `2 s- P$ v8 K$ _9 ~i 7 W+ s% P+ b0 E6 _/ v0 f9 H 0 w0 \; ]& j6 _6 B+ Y) x/ \' g
,d[j−1]+y - E' N( d( J( B: G
j ' _% D7 p/ F: K! T 4 q( I ]9 D2 H1 u: k% v
,d[i−1][j−1]+z , M6 P7 t: C+ B G6 Q: b, X
ij- ]$ K4 s; h8 ~. j1 Y
# a% O8 B) V% d ) 5 D! }4 i) Z" N状态转移如图四所示:# H. d( C1 i2 Y3 b1 H, ^
+ K) M4 q8 z e& g8 S" q5 o/ X
* y7 J ~- j' l; l% I" [比较经典的问题是最长公共子序列、最小编辑距离。 ' m$ f9 i$ t/ e" U1 l1 J4 `, L有关最长公共子序列的问题,可以参考以下文章:夜深人静写算法(二十一)- 最长公共子序列 , D& X4 I1 r2 c5 f有关最小编辑距离的问题,可以参考以下文章:夜深人静写算法(二十二)- 最小编辑距离7 A8 K7 o/ m5 {0 _% q
3、2D/1D: t) e Z4 I; z4 v v- r* S
d [ 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] ) & ]7 F$ P# z# o5 A0 Od[j]=w(i,j)+opt(d[k−1]+d[k][j]). U9 C, |0 r# t4 v
区间模型常用方程,如图所示: , j' D) r1 x; V+ O7 k' u; e6 N [2 W- H1 }% T
$ D+ k" ]* x e
另外一种常用的 2D/1D 的方程为: 7 v, } R1 K9 c) u7 f4 [3 c! {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 ) ; x3 U' |. y, p" o1 fd[j]=opt(d[i−1][k]+w(i,j,k)∣k<j) 8 r9 c/ W# `: |0 A) }1 I8 K0 p区间模型的详细内容可以参考以下这篇文章:夜深人静写算法(二十七)- 区间DP / q& W! i* F( R% ^0 L4、2D/2D + V" }+ y+ ^: w3 O" S. Ed [ 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) $ v) s5 _, l( r; @* M$ Id[j]=opt(d[i - @: {" v5 ]% f M% H6 q& G, s
′ 5 n) q1 Y2 U3 z1 C3 b& g; W# O ][j % D4 T" F! }+ R; A, p
′9 Y6 R$ x9 z& ?4 _
]+w(i 4 @6 H- G% @ A- ~: y4 p0 E% u& \′ 8 d( t6 y$ z1 S ,j ; G) ~) q: n6 \1 \: [$ }
′7 F& i& ]& x, j* _9 z
,i,j)∣0<=i 3 r: W7 O. A' E+ ~" b′1 T2 ?. E8 t& s6 t( e6 `
<i,0<=j + A3 I8 m* A9 n% a′ ) R9 ?+ |4 _2 W; o <j); S I! ^ W4 G7 ?* j4 R
如图所示:8 i0 P' I; r6 ~; v& \
: u: r+ j0 c4 ?. b2 O+ |
4 Z S8 P, K, d3 J% Q8 t
常见于二维的迷宫问题,由于复杂度比较大,所以一般配合数据结构优化,如线段树、树状数组等。 ' T8 ^8 k" f% X% n" c- [对于一个tD/eD 的动态规划问题,在不经过任何优化的情况下,可以粗略得到一个时间复杂度是O ( n t + e ) O(n^ {t+e})O(n ( |/ E/ P1 _- x0 o$ o1 n) a
t+e 3 i+ ?2 {7 N1 |$ q2 M( d) ? ),空间复杂度是O ( n t ) O(n^t)O(n 2 C, \% o3 R1 D+ z
t - y& z" {6 U {$ T/ ~! [- H' t ) 的算法,大多数情况下空间复杂度是很容易优化的,难点在于时间复杂度,后续章节将详细讲解各种情况下的动态规划优化算法。- I; h- K# k9 {7 k7 T
3)计算几何 & K ^# \' t' ?# j; O6 ^5 m1 Y, K4 S计算几何的问题是代码量最大的。它是计算机科学的一个分支,以往的解析几何,是用代数的方法,建立坐标系去解决问题,但是很多时候需要付出一些代价,比如精度误差,而计算几何更多的是从几何角度,用向量的方法来尽量减少精度误差,例如:将除法转化为乘法、避免三角函数等近似运算 等等。 ; }6 [0 k+ c# O) W R如果一个比赛中,有一道计算几何的题,那么至少,它不会是一道水题。3 T9 n: Q5 `" I1 u% D2 b
1、double 代替 float / R" V9 }+ s4 w9 c: f' D# ]c++ 中 double 的精度高于 float,对精度要求较高的问题,务必采用 double; " J# Y% ^# M6 }/ ^& K. i+ }9 W2、浮点数判定 & r) e9 u2 `( I% ~, a由于浮点数(小数)中是有无理数的,即无限不循环小数,也就是小数点后的位数是无限的,在计算机存储的时候不可能全部存下来,一定是近似的存储的,所以浮点数一定是存在精度误差的(实际上,就算是有理数,也是存在误差的,这和计算机存储机制有关,这里不再展开,有兴趣可以参见我博客的文章:C++ 浮点数精度判定);8 T6 l4 o( u5 h) W
两个浮点数是否相等,可以采用两数相减的绝对值小于某个精度来实现:6 U7 d @% q, E2 B o5 r+ `9 n
const double eps = 1e-8;3 X' O# x+ J- Q: ]0 {+ t
bool EQ(double a, double b) {+ A% v7 ?- @2 N, j5 m' J2 W i( i
return fabs(a - b) < eps; - }8 E7 p# C# L; N} W% ~; q8 H6 O( W \' ] ?. @
1 , F, h; `. g X& ]" L2: T5 ]: M7 W/ O* J; q4 S
3 9 U- |& s X+ X$ t; |4 * V& P& X/ H+ d* k并且可以用一个三值函数来确定某个数是零、大于零还是小于零: - ]" K( l% v. A w( D) f+ wint threeValue(double d) {# [% [* |6 q& ~$ S3 F
if (fabs(d) < eps). m6 p; W4 m0 b4 {! z
return 0; 5 _0 Q/ X* u P* W+ C- x7 ` return d > 0 ? 1 : -1;# _6 `6 Y8 C4 a6 _6 M
}% D' g3 i+ X) h' A- L* i- `/ Q
1. ?8 B0 W* z- S _ M! W
2, {" \- o" I7 |& |' v
3 - o1 ^ _+ E5 f6 n$ }. e) u4 p4 3 u5 o/ A g: u9 }1 P5 / H/ G* B0 i$ f* E1 q4 o! B3 A3、负零判定 6 L0 t+ P9 n7 p q因为精度误差的存在,所以在输出的时候一定要注意,避免输出 -0.00: / W% l$ G; e% \2 E3 O$ Y double v = -0.0000000001; + p* Q' B0 }2 L1 J# \ printf("%.2lf\n", v); H3 o8 O# `! t6 r L1# V& \3 \% a9 P, D) N& h8 }" ~/ Q
2, E# C/ F: s! S
避免方法是先通过三值函数确定实际值是否为0,如果是0,则需要取完绝对值后再输出: ) {6 e0 K6 f1 A8 W) I+ g double v = -0.0000000001;. G4 K$ G. S* c3 `
if(threeValue(v) == 0) {( P/ D7 f. g6 F: |& M D3 V' h' L
v = fabs(v);1 A" F3 p9 m2 v+ E" U6 v
} # J" D6 I, p6 Z printf("%.2lf\n", v);, h d7 C& I& q$ B: p
1 / T; d3 r' f, ]6 L7 B G21 b8 \0 ]$ Y0 E) v1 K8 @1 n
3 ( ]/ G) i& H3 Z0 ?4 ^4 ! |" ?4 }" h7 e! _# P/ i) \5& Y: q" H. o+ k* P+ D; x( G0 `
4、避免三角函数、对数、开方、除法等. c9 z) G# H& u5 }
c++ 三角函数运算方法采用的是 CORDIC算法,一种利用迭代的方式进行求解的算法,其中还用到了开方运算,所以实际的算力消耗还是很大的,在实际求解问题的过程中,能够避免不用就尽量不用。 + n/ u! Y0 r, {! e' w) ^2 m1 E除法运算会带来精度误差,所以能够转换成乘法的也尽量转换为乘法运算。# l$ q' s3 Y3 x' C: y
5、系统性的学习! h+ H% T/ x, ~* E; J* U x
基础知识:点、向量、叉乘、点乘、旋转、线段、线段判交、三角形面积; ) ~+ q4 O0 c' I+ h进阶知识:多边形面积、凸多边形判定、点在多边形内判定; 3 R: I! Y: U; S8 X相关算法:二维凸包、三维凸包、旋转卡壳、多边形面积交、多边形面积并、多边形面积异或、多边形和圆的面积交、半平面交、最小覆盖圆、最小包围球、模拟退火。 / E9 P9 y! j6 _' k " t' g- v5 a' Q3 N0 _$ Q . {. i& s- f6 b: B4 N3 X4 C% Y8 q$ `学习计算几何,最好是系统性的,刷题的过程中不断提炼出自己的模板。. w2 A" G- ]( O1 @) x
4)数论6 Q; y0 T4 U4 q1 t9 ~2 L; N
刷题的时候遇到不会的数论题,真的是很揪心,从头学起吧,内容实在是太多了,每个知识点都要证明吃透,不然下次遇到还是不会;不学吧,又不甘心,就是单纯的想把这个题过了,真是进退两难!5 J$ w0 q8 s* o1 |6 N1 ]2 v) C ]
数论对一个人的数学思维要求较高,但是一般也是一些固定的模式,所以把模板整理出来很重要。 & x' {$ e; d; p1 p2 e当然,数论也有简单问题,一般先做一些入门题提升信心。 + j! q$ A6 T* c. a1、数论入门 # l. C! s, p* M2 F6 v主要是一些基本概念,诸如: |5 d0 @4 }+ S0 ?& g
整除性、素数与合数、素数判定、素数筛选法、因数分解、算术基本定理、因子个数、因子和、最大公约数 (GCD) 和 最小公倍数 (LCM)、辗转相除、同余、模运算、快速幂取模、循环节; 0 C' ?$ L% N" a/ Q ^5 v2、数论四大定理 ! c+ R1 L2 t, s, ~; W: m这四个定理学完,可以KO很多题: x) B6 z, y- @) g
欧拉定理、中国剩余定理、费马小定理、威尔逊定理 9 P( [+ p( U; q1 ?$ g: s# @9 s3、数论进阶 4 d8 S' e9 Z2 H5 Z5 q. j系统性的学习,基本也就这些内容了:, [( I$ I8 r: j* X0 P
扩展欧几里得、逆元、欧拉函数、同余方程组、扩展欧拉定理、RSA、卢卡斯定理、整数分块、狄利克雷卷积、莫比乌斯反演、大数判素、大数因子分解、大步小步离散对数等等。! u' e3 j. d! V R6 V
5)字符串匹配- n3 G; L( e) X
字符串匹配学习路线比较明确。 0 W1 Z) v6 A. D) }/ t先学习前缀匹配:字典树。0 ~& }3 w4 c% i# v( S
然后可以简单看一下回文串判定算法:Manacher。4 w- K3 A1 u' V0 [8 u: o! m
以及经典的单字符串匹配算法:KMP。 + }) E3 b5 y- M; \实际上平时最常用的还是 BM 算法,而ACM中基本不考察。 ]+ `+ T; U x. c6 Z然后就是较为高阶的 前缀自动机、后缀数组、后缀树、后缀自动机了。0 O8 Y/ k; d/ | r- \8 ?7 n0 d$ {7 Q
关于 算法学习路线 的内容到这里就结束了。5 x3 ?( y `+ j% W4 B
如果还有不懂的问题,可以 想方设法 找到作者的微信进行在线咨询。; ]' \7 M! }+ d' a# V
参考资料 `5 ]; j$ ^7 t$ b
【阶段一】C语言学习资料:《光天化日学C语言》(日更)5 K% R2 Q$ p) }# ~. F. x
【阶段二】C语言例题:《C语言入门100例》(日更)* p# O! i: f% |! H! g
【阶段三】算法入门题集:《LeetCode算法全集》(日更) E" B, V, c& V' n【阶段四】算法进阶:《夜深人静写算法》(周更)1 H& l& l1 o( S2 ]5 I0 o
————————————————1 G: }% @: P- Z! M; G
版权声明:本文为CSDN博主「英雄哪里出来」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 4 y: j: @7 X) R0 B8 m8 Z原文链接:https://blog.csdn.net/WhereIsHeroFrom/article/details/1183822288 F. ]* B3 ], }
- S9 c# P# R) A! V) d" O: P$ v% E