数学建模社区-数学中国

标题: ❤️两万字《算法 + 数据结构》全套路线❤️(建议收藏) [打印本页]

作者: 杨利霞    时间: 2021-7-8 15:06
标题: ❤️两万字《算法 + 数据结构》全套路线❤️(建议收藏)

3 u3 k, H0 v5 I7 ~/ G& F  I/ X❤️两万字《算法 + 数据结构》全套路线❤️(建议收藏)
( c# m2 G+ p) s5 v* j" i5 m) Y* z4 @. d3 Z- f( v8 Q+ a
前言
! U5 V' \- Q  l* g* L# y3 M  所谓活到老,学到老,虽然我感觉自己已经学了很多算法了,但是昨天熬夜整理完以后发现,自己还是个弟弟,实在忍不住了,打算把 算法学习路线 发出来,我把整个算法学习的阶段总结成了五个步骤,分别为: 基础语法学习(重要)、语法配套练习、数据结构、算法入门、算法进阶。本文梳理了这五个大项的思维导图,在下文会有详细介绍。/ U4 a% D& n  b& `4 e
  希望各位能够找到自己的定位,通过自己的努力在算法这条路上越走越远。
9 }9 ~: [: j% z. c! {  刚开始切勿心浮气躁,千万不要给自己立 flag,说一定要把这么多东西都学会。就算你的精力旺盛,日夜操劳,时间也是有限的。所以,首先是明确我们要做什么,然后制定好一个合理的 目标 ,再一点一点将要学习的内容逐步付诸实践才是最重要的。
) Z. v- r* _1 T, I; t  每日一篇C语言打卡,目前更新到:光天化日学C语言(20)- 赋值运算符与赋值表达式 | 让代码变得更加简介(建议收藏)。
* a# g+ H! I" F& {
1 ]3 w  L. o' F( U' S' }

+ q+ W3 M8 t9 M" T: O, r$ Y8 P; z2 ~3 F* R8 H
: z8 t4 U; F1 n3 `: j7 {- [7 s
1 J& P6 {% A7 l, }* X' o1 f
% Y4 Z& s3 w. l  L0 a
4 Y$ |5 q5 I, b2 b3 k, [/ J

7 {" ^8 c' J. Q. w5 G8 ^; x图片较大,文章中有拆解,需要原图可以留言找我要哈- S" B( s! H" x* u& Z" w
1、基础语法学习. f8 q3 Z$ l0 x; `. F3 Y( k) n- p
算法是以编程语言为基础的,所以选择一门编程语言来学习是必须的。
  u& S1 T# b; v2 @# p因为作者本身是C/C++技术栈的,所以就拿C语言来举例子吧。如果是 Java、Python 技术栈,可以跳过 C语言相关的内容。这一小节,先给出学习路线图,然后我再来讲,每部分应该如何去学。
# v" I( L* _* f4 r* ~8 f) \, `/ j/ K& i
- R2 V' s: F2 t$ f
5 T8 p2 d& p! n( U* _; B7 U% k, C

$ F1 S3 N' t1 c: J1)HelloWorld4 F2 Z# _) v1 D" k( B; \, T" w
无论是 Java、Python、C/C++,想要上手一门语言,第一步一定是 HelloWorld,先不要急着去配环境。如果环境配了几个小时,可能一开始的雄心壮志就被配环境的过程消磨殆尽,更加不要谈日后的丰功伟业了。' G# W5 ~- b4 _, z
2)让自己产生兴趣
: Y% e8 v  U0 I所以,我们需要让这件事情从一开始就变得 有趣,这样才能坚持下去。比如找一个相对较为有趣的教程,这里我会推荐这个:《光天化日学C语言》。听名字就比较搞笑,可能作者本身也不是什么正经人,哈哈哈!虽然不能作为一个严谨的教程去学,起码可以对搞笑的内容先产生兴趣。从而对于语言本身有学习下去的动力。) v0 u3 C8 w  u" |! K4 x8 G, |3 B( H
刚才提到的这个系列,可以先收藏起来。回头再去看,它讲述的是 对白式 的 C语言教学,从最简单的输出 HelloWorld 这个字符串开始讲起,逐渐让读者产生对C语言的兴趣。这个系列的作者是前 WorldFinal 退役选手,一直致力于 将困难的问题讲明白 。我看了他的大部分教程,基本都能一遍看懂。算了,不装了,摊牌了,因为我就是这个作者。# B, G& o+ I) P  W' U+ }! m1 m' F( {
3)目录是精髓
* W9 s4 r8 h, Y  `- e% \然后,我们大致看下你选择的教程的前几个章节,那些标题是否有你认知以外的名词出现,比如以这个思维导图为例,前几个章节为:/ A4 ^3 n" l6 Q8 [, m2 c
1、第一个C语言程序3 g6 i( R% M; b. Y
2、搭建本地环境7 j$ u: [5 \$ g' g1 z
3、变量
) f  @% i+ K4 y: W/ }% U4、标准输出
( |  j$ t. G  d, g( v$ L) \5、标准输入0 q/ w8 a2 \! E: d9 Z
6、进制转换入门% N0 P, A8 _, _; ~1 m& [
7、ASCII字符4 s$ @, F( \3 U% M6 f3 ~1 l& G
8、常量
8 x  S' d, ^; s$ ~  j6 M* J
5 }) F7 S( s4 \. \7 t) O7 C
+ b) z6 V6 s  E. R) k9 J
如果你觉得这些名词中有 3 / 4 以上是没有什么概念的。那么,可能需要补齐一些数学、计算机方面的基础知识。反之,我们就可以继续下一步了。' V2 q6 G4 u2 i* H
4)习惯思考并爱上它
3 W8 u1 r* `% ~7 I+ j  d1 Y! J只要对一件事情养成习惯以后,你就会发现,再难的事情,都只是一点一点积累的过程。重要的是,每天学习的过程一定要吃透,养成主动思考的好习惯。因为,越到后面肯定是越难的,如果前期不养成习惯,后面很可能心有余而力不足。
5 f; V2 x0 H" T$ M7 n' ^5 e就像刷题,一旦不会做就去找解题报告,最后就养成了看解题报告才会做题的习惯。当然这也是一种习惯,只不过不是一种好习惯罢了。
, p6 E6 ]8 E6 j) t) b. j5)实践是检验真理的唯一标准
& G! E& c1 |) T; w6 R光看教程肯定是不行的,写代码肯定还是要动手的,因为有些语法你看一遍,必定忘记。但是写了几遍,永世难忘。这或许就是写代码的魅力所在吧。
4 P; f; v; @6 m0 d1 u& D所以,记得多写代码实践哟 (^U^)ノ~YO# r/ q5 b8 m: e
6)坚持其实并没有那么难
$ M) h8 y& }9 ]( t8 L* L2 f7 w; m1 m每天把教程上的内容,自己在键盘上敲一遍,坚持一天,两天,三天。你会发现,第四天就变成了习惯。所以坚持就是今天做了这件事情,明天继续做。
  x8 z" U5 e$ k% Z' [0 r( q7)适当给予正反馈
0 x6 Q; o6 M- z0 @) M/ _- F. P7 L然而,就算再有趣的教程,看多了都会乏味,这是人性决定的,你我都逃不了。能够让你坚持下去的只有你自己,这时候,适当给予自己一些正反馈就显得尤为重要。比如,可以用一张表格将自己的学习计划记录下来,然后每天都去分析一下自己的数据。/ }# T1 c+ ]3 O% Y3 T9 i
当然,你也可以和我一样,创建一个博客,然后每天更新博文,就算没有内容,也坚持日更,久而久之,你会发现,下笔如有神,键盘任我行!更新的内容,可以是自己的学习笔记,心路历程 等等。
& H4 e3 q% q& d: @6 m- p看着每天的粉丝量呈指数级增长,这是全网对你的认可,应该没有什么会是比这个更好的正反馈了。
% j3 c# h3 L5 P: M9 C+ Y7 B8)学习需要有仪式感
* ?" J4 h& _2 I9 J' ]1 X  |那么,至此,不知道屏幕前的你感想如何,反正正在打字的我已经激情澎湃了。已经全然忘记这一章是要讲C语言基础的了!* H$ o. i$ M/ J3 t3 e( W' A
介于篇幅,我会把C语言基础的内容,放在这个专栏 《光天化日学C语言》 里面去讲,一天更新一篇,对啊,既然说了要坚持,要养成习惯,我当然也要做到啦~如果你学到了哪一章,可以在评论区评论 “打卡” ,也算是一种全网见证嘛!) e3 E3 ~* I0 l$ L+ |4 I
我也很希望大家的学习速度能够超越我的更新速度。# l& V$ e* r  S# S( P3 ^9 ~
2、语法配套练习
2 g4 i  J* I# }9 s; J9 K( t学习的过程中,做题当然也是免不了的,还是应征那句话:实践是检验真理的唯一标准。
5 y+ D% g% A/ z; m而这里的题库,是我花了大量时间,搜罗了网上各大C语言教程里的例题,总结出来的思维导图,可以先大致看一眼:& @+ D1 ^8 w4 \: i  I8 C6 R0 w
- \7 y0 w& L" n( ]2 Y) Z8 s
0 h8 X+ a3 Z8 ]. U# z
) h4 P  N& }; O8 H+ M; e9 [0 c0 f" a& S
7 `: z) W3 Z) U7 B! C
从数学基础、输入输出、数据类型、循环、数组、指针、函数、位运算、结构体、排序 等几个方面,总结出的具有概括性的例题 100 道 《C语言入门100例》,目前还在更新中。$ _: S) V9 R) D: T
这里可以列举几个例子:# I3 {$ V0 a8 P" Z3 v( A  {+ J5 I
1、例题1:交换变量的值
: b, Z5 e, K/ A1 L" i( M' C一、题目描述
( F; @& u/ r; g; W: N# b+ b  循环输入,每输入两个数 a aa 和 b bb,交换两者的值后输出 a aa 和 b bb。当没有任何输入时,结束程序。+ q; d/ [: P( Z* s( `6 J: L6 q' z

0 V3 x9 L+ q2 m6 R4 e
: Y, z3 `, I2 d$ u

$ v& P9 t! o. v

8 y$ E5 U/ Z9 U# H二、解题思路
( Q! g' i) I0 w难度:🔴⚪⚪⚪⚪% I; Z1 v6 \" d4 ]

# R. K1 r- U# p! E7 ?
- q# H* i9 ^. R" r' a( S
这个题的核心是考察如何交换两个变量的值,不像 python,我们可以直接写出下面这样的代码就实现了变量的交换。: ?5 R! g/ H, v# {- V! R( V6 p
a, b = b, a! }7 H  q+ Q) o
12 T& R9 T( t2 F' m, v8 [: h/ T
在C语言里,这个语法是错误的。, `% w( f3 X" z9 P
我们可以这么理解,你有两个杯子 a aa 和 b bb,两个杯子里都盛满了水,现在想把两个杯子里的水交换一下,那么第一个想到的方法是什么?- ^1 U2 }4 T& M3 i
当然是再找来一个临时杯子:" ]& Z- o& `8 Q$ N; C) E, A$ `( E
  1)先把 a aa 杯子的水倒进这个临时的杯子里;
% w+ E7 R/ o% u5 r  [% l  2)再把 b bb 杯子的水倒进 a aa 杯子里;3 u/ J3 O- p8 Z5 `& f" ^
  3)最后把临时杯子里的水倒进 b bb 杯子;3 C' T8 q6 T0 y% t
; @% O3 ~, a7 l; W7 _1 E- Q2 ^
, z3 F1 T. ~: h
这种就是临时变量法,那么当然,还有很多很多的方法,接下来就让我们来见识一下吧。
. l8 E5 Z) h( H6 R$ `, K+ M  E# O: X+ ~9 C" u; J
0 e. Y6 x$ Q3 i+ l" p  K) X
三、代码详解
; U" ~1 y1 D) v+ @, Y6 O& j( y) ?! J1、正确解法1:引入临时变量& N# m5 d% [; K/ B. g7 o
#include <stdio.h>7 F/ M; m, K' h# a$ X1 O2 H
int main() {7 X) N  I2 g0 E* T; M6 N8 w
    int a, b, tmp;
1 N# _9 Z, ~- d6 i- h  K  n        while (scanf("%d %d", &a, &b) != EOF) {9 X0 `4 {& e1 ^9 L" y+ s
            tmp = a;   // (1)
5 k4 w$ m3 T7 P3 D! W            a = b;     // (2)
: @- O9 M5 C$ a            b = tmp;   // (3)& a' m! N' c3 ?/ X2 \6 u* s
            printf("%d %d\n", a, b);) w  p/ c$ l) \. }
        }
" i: S" X' o  h8 P! _3 l        return 0;
; f( U3 Q7 z. o5 H( m3 A}
/ r7 O1 }, X: O! t4 y& W1( l; z8 p0 m! _; Z; ]& Z
2
% i* C/ i; M' k, L' Y. _1 I& Z3
2 h, b3 {' A4 I" i5 m% `. ]4+ X% A# c! n# m  M5 R
5
$ t7 @' t, r; b) Q6
) ~9 H) D; y: W( y% z7
$ X8 S$ T, C* L: K8
* H" p$ {2 C+ J% n2 w4 x6 X/ {2 J91 p4 H' d3 Z$ K3 y# r
10. Z! Z% y* X( o5 y9 M- ~& D
11& L. ]& K! D- w, D8 \
( 1 ) (1)(1) tmp = a;表示把 a aa 杯子的水倒进这个临时的杯子里;
9 f8 F' V7 o. s  g# U7 R% L( 2 ) (2)(2) a = b;表示把 b bb 杯子的水倒进 a aa 杯子里;' ~  L/ Y8 }0 |
( 3 ) (3)(3) b = tmp;表示把临时杯子里的水倒进 b bb 杯子里;
! y% }6 O. u% [, j3 {. I这三步,就实现了变量 a aa 和 b bb 的交换。1 N; f0 Q- k( Z0 r! V+ c
2、正确解法2:引入算术运算) Y  n  s4 a. r& i
#include <stdio.h>
' p- W  Y# {2 ]4 @int main() {
4 z6 A. {; I$ y& ^) u    int a, b;
6 k3 g# U! }+ ?9 O, t# P% a. T        while (scanf("%d %d", &a, &b) != EOF) {
( T, k( I/ o  O' @" c+ o2 z6 j0 k            a = a + b;   // (1)  [, M1 _+ M; R" M$ I9 M3 l; }
            b = a - b;   // (2)7 \! X- K% k9 |7 q" E1 S
            a = a - b;   // (3)  o/ K# g, m# b8 g2 d; S* A
            printf("%d %d\n", a, b);
1 d* a* \) x9 O. k  O" c! k        }
  i3 A8 @/ ~& M: [% w        return 0;6 i7 }% i7 A+ h1 O, q5 D
}
/ h' ^( Q! ?( {7 S% s1  H# U. |) u: z0 y& p6 o% {( x
2" t! Z) d5 T$ j+ W/ P% ]  b6 G2 y# G
35 s5 Z1 d% T# D8 o! w* B
43 Z7 ]" j% ?. Y- B5 s. C0 D
5
" F! A% m* @. y, W" R: e6
; v' K/ t  Y: f2 Q7  b% d8 G3 I) m( k' i3 e. u
8
8 h' q6 g' t0 r2 w, N+ j6 |9; }3 a$ S7 b+ c4 h4 G% R
10
' C4 x7 z0 w2 M5 H: Y* U11
6 r5 k! v4 L& {# I( 1 ) (1)(1) a = a + b;执行完毕后,现在最新的a的值变成原先的a + b的值;% T8 k/ E9 t0 \4 ^/ F$ M8 A' J
( 2 ) (2)(2) b = a - b;执行完毕后,相当于b的值变成了a + b - b,即原先a的值;2 W/ U" b5 l' I9 ^
( 3 ) (3)(3) a = a - b;执行完毕后,相当于a的值变成了a + b - a,即原先b的值;
; I" B! i% Q$ h! ]9 d从而实现了变量a和b的交换。  m" z; g* y' v: {1 V, k
3、正确解法3:引入异或运算
2 W9 B/ @. o4 Z1 }7 R1 _7 x首先,介绍一下C语言中的^符号,代表的是异或。9 K/ r0 b# O; p1 L2 E
二进制的异或,就是两个数转换成二进制表示后,按照位进行以下运算:
) o- C7 F; O( b& n5 b* _左操作数        右操作数        异或结果# l8 R' L: c( q; t9 I8 F, C- ~0 d
0        0        03 M( o. k$ m: j+ P
1        1        0
1 N% e' _: V) g0        1        18 V1 V+ N8 z3 Q( t4 v7 F5 P  F
1        0        1/ R2 j- z& X4 s# J9 l% H+ ]* L
也就是对于 0 和 1,相同的数异或为 0,不同的数异或为 1。
1 }6 l$ m/ X2 F# g/ R这样就有了三个比较清晰的性质:2 f+ r$ a6 c$ R8 d& o
1)两个相同的十进制数异或的结果一定位零。
3 H6 ^: A* T: I  I* ]2)任何一个数和 0 的异或结果一定是它本身。
) g$ z4 i  H4 S5 O+ u- U3)异或运算满足结合律和交换律。
8 {2 K% l. h3 }6 I% J#include <stdio.h>% n7 i9 H3 y1 d0 k$ j
int main() {
$ `: Q. d0 w' j  h) R    int a, b;
. b( ?3 J. H+ m8 \2 T        while (scanf("%d %d", &a, &b) != EOF) {$ i7 }1 S7 `7 D, \; I
            a = a ^ b;   // (1)7 D0 v, b& y$ `3 a, o
            b = a ^ b;   // (2)- _% J/ [7 j( r4 d) X* G9 _
            a = a ^ b;   // (3)! k* ~9 o* I. p  y7 n; \
            printf("%d %d\n", a, b);  }! M$ d/ [4 E" I: \4 o
        }
$ T  z, H. L2 i        return 0;
8 X: i5 J' S# Z: {7 C, k( e) q}
& Q7 e4 Y, R' t- R/ p1. k6 Z5 j; r1 Z- t; V( i
2$ c8 ~3 Q: i" c' N
3
- H+ o. g  U7 ~41 n- f6 X* m" K# q
5
- ^* M8 b# ~/ B  @4 r+ [6 }6
7 d8 ?9 N+ j% c5 T2 ?1 y7+ O$ L8 z+ p6 ~/ ^6 O
82 O7 ?; N. a' s( l- J6 O
9: I4 p/ a  x8 R7 l% Q$ H+ H
108 R8 s1 |6 Y; u$ ?
11
; p$ ]& @. S) K' }我们直接来看 ( 1 ) (1)(1) 和 ( 2 ) (2)(2) 这两句话,相当于b等于a ^ b ^ b,根据异或的几个性质,我们知道,这时候的b的值已经变成原先a的值了。" X, {, \) x6 t' n0 z
而再来看最后一句话,相当于a等于a ^ b ^ a,还是根据异或的几个性质,这时候,a的值已经变成了原先b的值。
/ g, M9 t  \( l9 R% m$ g  P7 F从而实现了变量a和b的交换。0 q) B6 n5 U$ P1 r

, b  L/ h' X! k2 C9 h9 n
7 e$ ~& T/ v. d" ?$ h5 X3 A
4、正确解法4:奇淫技巧" i4 |, v# r% n% g
当然,由于这个题目问的是交换变量后的输出,所以它是没办法知道我程序中是否真的进行了交换,所以可以干一些神奇的事情。比如这么写:
2 c! e1 `$ T; k  Y1 G6 |#include <stdio.h>
! h0 H0 J% l& C+ p0 O: {5 }7 Hint main() {
1 b$ x; L! S) }    int a, b;/ f6 t4 h8 ]. Z. x, q9 S$ O, h
        while (scanf("%d %d", &a, &b) != EOF) {. p# h. e' Z2 ]# n3 j7 D8 f
            printf("%d %d\n", b, a);
$ Q2 Q+ {/ P6 _* i        }+ ?! Q3 |( T0 |) s7 Q8 ^
        return 0;
, X( q# h8 C9 }, W5 c}) c; |% ?! q8 t/ Y( r% k6 r3 _
1
4 H8 i. t7 r: F: h! u& f1 G, L# T2! n, X  M( A; E$ P' S
3
: A( `4 w# `5 C" P( g4
* v* ^' H6 m3 K2 M" ]8 ^: B5; ?$ m- d+ H! B; b% \: r
6! O5 l1 y) ^6 j7 o4 V
7( Y5 b5 c: L! o0 L% A& F/ i% Z
8
- U9 W- V2 \4 q: {, ?: A, T1 }你学废了吗 &#129315;?
& S- W5 O$ U6 e2、例题2:整数溢出, ?0 s" q, M+ W4 F, f" v: U
一、题目描述/ }: T0 y# J( q6 r1 L
  先输入一个 t ( t ≤ 100 ) t (t \le 100)t(t≤100),然后输入 t tt 组数据。每组输入为 4 个正整数 a , b , c , d ( 0 ≤ a , b , c , d ≤ 2 62 ) a,b,c,d(0 \le a,b,c,d \le 2^{62})a,b,c,d(0≤a,b,c,d≤2 % [1 T" x8 q+ ^2 O9 `
62/ e; l0 m- c7 Z1 {' B! \! c7 ?. e
),输出 a + b + c + d a+b+c+da+b+c+d 的值。0 I6 p5 _, ?' D. V' D, n7 y
. ^" F9 H; a+ ]7 T' A& q

+ t! b9 D4 ~/ M+ [8 r2 h二、解题思路
, n) [* \4 ]& O# `, }% ]$ G$ [难度:&#128308;&#128308;⚪⚪⚪, Q( K6 E' {! A3 j4 }
9 o% ?/ `; V- k1 n" S
* U! L' W& K( o+ e
这个问题考察的是对补码的理解。
* P9 w( ]# l9 ?. H, j3 P仔细观察题目给出的四个数的范围:[ 0 , 2 62 ] [0, 2^{62}][0,2
# ?2 z" Q1 m1 p5 M0 q# \62$ u- h6 D; o' H5 Z: p/ l
],这四个数加起来的和最大值为 2 64 2^{64}2
# {) K: z6 }7 f/ e2 T! I6 @64
2 B3 t3 U  ]. m& m, P7 p7 {7 Y+ y 。而C语言中,long long的最大值为:2 63 − 1 2^{63}-12
! G' W) G& c& H* u6 v63/ d3 R) t) h6 L* o
−1,就算是unsigned long long,最大值也只有2 64 − 1 2^{64}-12
+ J9 I4 N' o2 r4 G64
6 P5 z* {3 ]+ o* C −1。
1 \) @+ ?; D9 {% Z" R5 q* x但是我们发现,只有当四个数都取得最大值 2 62 2^{62}2 3 f0 Z7 a* U+ l6 ^' m: c. O1 B& v5 e
62
4 @/ Q- t# S/ _) G, Y  时,结果才为 2 64 2^{64}2 : l4 b8 ]2 Z& u. M5 c2 U9 T; t
64+ B- V7 g: B$ ^; ?! i
,所以可以对这一种情况进行特殊判断,具体参考代码详解。( l- Q/ F; P' [. o# R! p. K; _
三、代码详解
; ]$ s! n- ]1 R7 v' e, Y  i( j#include <stdio.h>
. v: k* C+ a1 P& ]$ \5 L, _typedef unsigned long long ull;                           // (1)
6 w7 p  }0 n4 N, q7 o8 l3 R) f! iconst ull MAX = (((ull)1)<<62);                           // (2)
6 C6 H, H5 H! ^0 h
& Z$ J& @& W5 D2 u
, w* p3 F# U( a  s& ]+ ?% U% x
int main() {
( x3 l- k6 R2 U        int t;
3 e- o3 s  P+ b2 C' Y1 P) u  v  o        ull a, b, c, d;
0 g5 |- O: b7 u/ _* U        scanf("%d", &t);
8 a- ?* q. \( z. X" X        while (t--) {+ p6 u. Y$ w4 B+ L; x& q
                scanf("%llu %llu %llu %llu", &a, &b, &c, &d);     // (3)5 {- \& m6 R7 y8 S
                if (a == MAX && b == MAX && c == MAX && d == MAX) // (4): E' z2 @' r8 K5 r8 L  w0 V
                        printf("18446744073709551616\n");             // (5)
/ f8 J) o4 E& r                else! m' z9 ?' T* t7 u
                        printf("%llu\n", a + b + c + d);              // (6)
; v9 ~9 y9 o  g5 n! \( b        }
- ^) k& E$ G4 ^1 o: H6 |# Y4 E        return 0;
; I8 m+ s' c* x}
4 D3 B  @3 T* g( y+ f9 v: A1
9 C8 [9 s2 L! J. O8 v( F" J2" e2 n5 f/ I" o5 H% N
3
. E, Y0 S/ t. m: E9 [! i4# L  l, f( `& ]6 m  `, F0 O* J
50 i! K! K: R4 O8 b
6
# u7 o, ]) e) b  |1 K& w72 w1 {! Q+ T: l/ w
8) m* X; {" L& f4 p1 U! i
9
9 K, o; B* l# v% U; a10$ n: g7 r+ a+ d; D, X
11
; q/ r. b2 k. ]# ^# h4 r12. L9 G+ Q' y  J1 F6 |2 J
13
7 Y9 `; ?: e$ u143 y1 ^6 T) P' G
15
" ^: k* a& i" E# F+ m. F* x5 a16- U' L1 z; X  f- `( w6 c6 ^! R
17! }& \0 q. O7 U% F
( 1 ) (1)(1) 由于这题数据量较大,所有数据都需要用64位无符号整型。ull作为unsigned long long的别名;" a5 Y: D& Y2 g2 ]0 J6 S
( 2 ) (2)(2) 用常量MAX表示 2 62 2^{62}2
3 }0 C1 @! T  m& N' z7 h/ B( }1 e4 ]62* B$ S5 u; j4 M, I
,这里采用左移运算符直接实现 2 22 是幂运算;/ Q; W! h6 ]7 r' K2 N( v' X! \3 h
数学        C语言
- V; v7 n) z; G/ \4 n  F' t% c4 g& `2 n 2^n2
# c# H" O3 T/ M9 o. Z7 rn
1 g1 _3 G! w& j% f, U/ h  j         1<<n1 w$ H- \# R6 z* w. T. T. K3 r: [
需要注意的是,由于 1 是int类型,所以需要对 1 进行强制转换。(ull)1等价于(unsigned long long)1;
. K8 v; }# O# T6 o$ \. A( 3 ) (3)(3) %llu是无符号64位整型的输入方式;7 k/ z- E6 w8 B0 ^8 Y) }
( 4 ) (4)(4) 这里是对所有数都等于最大值的特殊判断,&&运算符的优先级低于==,所以这里不加括号也没事;$ D  K8 u/ Y' _  ?* l
( 5 ) (5)(5) 由于 2 64 2^{64}2 7 l3 d6 I3 `( }5 P8 x5 ?! m# ^3 b
647 j% |+ z2 h7 C, G* y
  是无法用数字的形式输出的,所以我们提前计算机算好以后,用字符串的形式进行输出;  d' ?5 E2 `& W
( 6 ) (6)(6) 其它情况都在 [ 0 , 2 64 − 1 ] [0, 2^{64}-1][0,2
; f/ y( l; f+ I: r% L/ N649 ~) y+ x  {- ]: A
−1] 范围内,直接相加输出即可。- @8 r9 z7 M  s; B5 x8 B  ^
由于这个专栏是付费专栏,可能对学生党不是很友好,所以作者经过再三思考,打算放出 300 张 一折优惠券, 先到先得。只要拿这个图片来找作者即可享受,仅限前 300 名。: J7 E' ?& j7 w0 t% S3 i
为了适当提高一定门槛,你至少需要学会如何下载图片或者截图并且发送到微信里 &#129315;。+ m1 r3 y1 Q6 P6 E
6 X( r& N7 f$ w: E7 Y# w' |
( H0 G0 ?1 i5 I; E& i0 h
3、数据结构
8 ]0 X% t9 Q( R. i/ K3 @《C语言入门100例》上的例题,如果能理解前面 25 道,那基本C语言的学习就可以告一段落了,接下来就要开始我们的数据结构的学习了。% \& M1 r9 i0 X9 _- z  f, b
1、什么是数据结构$ h0 K$ u! r; p% ?$ ^8 s' M( w
你可能听说过 数组、链表、队列、栈、堆、二叉树、图,没错,这些都是数据结构,但是你要问我什么是数据结构,我突然就一脸懵逼了。' \3 t5 A) ]; n0 ?
如果一定要给出一个官方的解释,那么它就是:
( f4 [& V, E; g+ x/ o5 D; }计算机存储、组织数据的方式。相互之间存在一种或多种特定关系的数据元素的集合。通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。往往同高效的检索算法和索引技术有关。8 L2 |# P3 y( m2 v

; }. D: F( Y4 ~

: l: y* N) C0 _/ N8 |' C. I是不是还不如说它是堆,是栈,是队列呢?' {0 ^* n  ?) m
是这样的,我们学习的过程中,跳过一些不必要的概念,能够节省我们更多的时间,从而达到更好的效果,当你还在理解数据结构是什么的时候,可能人家已经知道了栈有哪些操作了。; \1 e  Z4 {5 r: e8 {2 w, K" e% E
2、数据结构和算法的关系
2 `. f0 D7 w: h$ L. q3 L1 X很多同学搞不明白,数据结构与算法有哪些千丝万缕的关系?甚至有些同学以为算法里本身就包含了数据结构。9 W, @0 s- u) Z+ k6 W- X
数据结构主要讲解数据的组织形式,比如链表,堆,栈,队列。
; y: z9 s- Q9 F  _1 o而算法,则注重的是思想,比如链表的元素怎么插入、删除、查找?堆的元素怎么弹出来的?栈为什么是先进后出?队列又为什么是先进先出?8 w, G; {3 @9 Y6 v0 F1 h
讲得直白一点,数据结构是有实体的,算法是虚拟的;数据结构是物质上的,算法是精神上的。当然,物质和精神 缺一不可。* F( \% r. q; d7 r6 u
3、数据结构概览/ P3 C6 I( S9 Q% z, G+ H* `
周末花了一个下午整理的思维导图,数据结构:
( S. h' I* \. O1 O
  I2 h/ g$ v& G1 G; \

: t. [% |; n. m' R( m1 U常用的一些数据结构,各自有各自的优缺点,总结如下:/ ?% f. j8 R4 {. ]2 u
a、数组
. j9 t# q  m6 Z' z' M0 J+ K1 n, Y! d: c内存结构:内存空间连续
, L" V4 J! E7 T  u( r$ q实现难度:简单
3 b; J8 j# b: C: l8 y9 A3 @: J4 @下标访问:支持7 Q* D+ \9 t) x$ u( P$ G7 l
分类:静态数组、动态数组) x& I8 Z5 T& X5 S  v
插入时间复杂度:O ( n ) O(n)O(n)
6 b0 t! U  z+ P9 `* g) I查找时间复杂度:O ( n ) O(n)O(n)
& h6 l3 d5 s9 b# q, T6 S删除时间复杂度:O ( n ) O(n)O(n)/ m, N' \9 f; ], s6 A! r0 g
. ]( {: f9 l9 O' D0 g. X. ]7 w' n
6 K& }( B- q7 t/ R( v, {* k$ R
b、字符串+ P/ [1 [* x8 n+ Y; ~
内存结构:内存空间连续,类似字符数组$ e* Z! r: F7 M+ [
实现难度:简单,一般系统会提供一些方便的字符串操作函数/ n1 u- |1 @, ?  m* m
下标访问:支持6 g% q' r3 y. z: q* u, K2 v
插入时间复杂度:O ( n ) O(n)O(n); m  T8 A/ b3 B, }1 b. L+ ?+ H9 G
查找时间复杂度:O ( n ) O(n)O(n)* |! E4 c& c1 F8 j
删除时间复杂度:O ( n ) O(n)O(n)2 x3 ]; r0 s- l, C. b9 ]$ C
8 r. m# l# z% y
6 Z$ o. f  F1 d1 B1 T) E3 P
c、链表
( F" y2 a1 s1 e4 Y内存结构:内存空间连续不连续,看具体实现
, J% l2 U) z/ h& e( ]: ~- J) ^实现难度:一般
) c( N* G* W3 |) M下标访问:不支持" g4 N, P, V' v  A! C( H6 i. D8 [( j
分类:单向链表、双向链表、循环链表、DancingLinks
$ s+ K6 \: V$ m2 c插入时间复杂度:O ( 1 ) O(1)O(1)  |* J7 ], e# A4 e- Z" I
查找时间复杂度:O ( n ) O(n)O(n)
0 p+ E2 a, I9 M: A删除时间复杂度:O ( 1 ) O(1)O(1)/ K  x) c7 D/ Q7 t' s* H5 B& W

1 z; F, ^( O+ s2 x, E7 P

; s% Z$ \7 r- ~d、哈希表/ a4 M+ _2 R% H
内存结构:哈希表本身连续,但是衍生出来的结点逻辑上不连续7 e: T# u! N4 Q
实现难度:一般
9 v( l7 [. f0 m  d# n: {3 w下标访问:不支持& `) N7 ]9 u0 N3 L/ \
分类:正数哈希、字符串哈希、滚动哈希
6 t% q3 W! B/ ~7 s& j* y8 _; A/ i, i插入时间复杂度:O ( 1 ) O(1)O(1)
: t% a: S4 z' A; v6 B查找时间复杂度:O ( 1 ) O(1)O(1)
3 X1 E' z, m, j* r+ @删除时间复杂度:O ( 1 ) O(1)O(1), @6 A. ]; n7 ~5 t8 }0 }9 E: }5 v

; v) o, k) b7 u
8 q4 J: r8 E. D+ o, b
e、队列  S3 D$ A9 u5 H) q9 m0 g9 ^
内存结构:看用数组实现,还是链表实现; u' m& ^- f$ w1 m% t
实现难度:一般1 X: S1 A  X3 r: T' E' I5 l
下标访问:不支持
0 ]# D! u/ P0 y0 B' \6 G$ j9 g" V分类:FIFO、单调队列、双端队列+ o; z- y' ?0 b9 _- D# ^
插入时间复杂度:O ( 1 ) O(1)O(1)  v: v2 x* s# V  w
查找时间复杂度:理论上不支持1 U& F, g4 J8 h$ f
删除时间复杂度:O ( 1 ) O(1)O(1)
9 X& f( z( h9 n9 b' x# O! o1 W3 o' y( L2 j+ z" c

0 r: Y0 W9 _6 {$ Z  Ef、栈2 u7 o4 T) {& \' T$ d, C
内存结构:看用数组实现,还是链表实现/ t& c& {( `4 V0 X
实现难度:一般, t" m, ~( Q1 b" C8 a
下标访问:不支持" p! g" m) l& g" p/ A3 s5 H
分类:FILO、单调栈! }* d7 _1 w3 ^9 y$ n; L
插入时间复杂度:O ( 1 ) O(1)O(1)! A% |; C0 S+ R" V' Z3 b
查找时间复杂度:理论上不支持( O" w8 u3 G0 W+ G- ~+ M
删除时间复杂度:O ( 1 ) O(1)O(1)
# [6 D8 [# D9 Z+ X' A0 Z: N& O) h* I, F1 z, }

. T6 Q" F) K2 o1 G; }g、树
  }: @# o1 |) Y" Y内存结构:内存结构一般不连续,但是有时候实现的时候,为了方便,一般是物理连续,逻辑不连续
- V8 W& y# {; z/ \% N0 ]9 y2 O实现难度:较难$ t; ]* Y# _  q4 d. G
下标访问:不支持# q! m9 x+ v$ f/ s
分类:二叉树 和 多叉树
* a* n( U# [1 T& e插入时间复杂度:看情况而定
5 q0 M4 n9 O2 D- N查找时间复杂度:理论上 O ( l o g 2 n ) O(log_2n)O(log
" Y9 `3 `) }+ G9 J$ v% D2- Y1 S8 x+ s  u3 ]/ [
​       
% v8 }  z% B8 |9 a n)
$ b. Z( l9 B3 s" J/ }删除时间复杂度:看情况而定7 B3 j& M8 v5 W' o

' l* ]1 O0 S: O
' X. `7 `) a' x
1、二叉树9 t( o& C! N7 R; Y6 C9 \; U
二叉树的种类较多,比如:二叉搜索树、平衡树。平衡树又可以分为 AVL 树、红黑树、线段树、堆。最平衡的树莫过于满二叉树了。/ v  ?( L, a8 K& z% x8 I  G
其中,堆也是一种二叉树,也就是我们常说的优先队列。7 H; y6 f( x! E9 ]2 M
2、多叉树" m; Y/ N. Q) G% Z2 |6 ]% }+ i
B树和B+树是多叉树,当然我们平时学到的并查集其实也是个多叉树,更加严谨一点,应该称之为森林。4 r4 w  h. d* N' B
h、图
, ]. X2 H2 E2 I. n! R3 ]8 i内存结构:不一定
) q( u7 y- f' ~实现难度:难
$ J0 E* O, {- V/ K) P下标访问:不支持% n5 |# O0 i4 a& K+ c1 g) |& V
分类:有向图、无向图
- j% p( K2 I$ I& v. s/ p插入时间复杂度:根据算法而定
- q( k) N$ |' G. p$ @2 u5 D' P) z5 m$ y4 o查找时间复杂度:根据算法而定
+ q; p3 w; |# f* V+ ?. @删除时间复杂度:根据算法而定) a7 v) H% @6 F( Y% S
( u: `8 Y/ F. A5 ]$ ^
# O8 x* o" N5 v: c( |
1、图的概念
% m4 D+ {& Y8 r4 g在讲解最短路问题之前,首先需要介绍一下计算机中图(图论)的概念,如下:
5 l# I6 H; d, q( }9 [图 G GG 是一个有序二元组 ( V , E ) (V,E)(V,E),其中 V VV 称为顶点集合,E EE 称为边集合,E EE 与 V VV 不相交。顶点集合的元素被称为顶点,边集合的元素被称为边。0 M' Q- p, p( M
对于无权图,边由二元组 ( 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 为权值,可以是任意类型。
9 F1 B1 T( A7 z3 A# b$ U图分为有向图和无向图,对于有向图, ( 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;5 K- m( X  H- z3 e
2、图的存储- j; b9 N8 G/ |+ ^& r) H/ y- }2 L
对于图的存储,程序实现上也有多种方案,根据不同情况采用不同的方案。接下来以图二-3-1所表示的图为例,讲解四种存储图的方案。
& v+ k5 T8 L- E2 \# a7 R  R7 r# J9 f: o6 y& X4 [

' \0 F" P; {; {" t: j7 ?- b2 I* k: ^1)邻接矩阵
% b/ b8 P" R- X9 D( I邻接矩阵是直接利用一个二维数组对边的关系进行存储,矩阵的第 i ii 行第 j jj 列的值 表示 i → j i \to ji→j 这条边的权值;特殊的,如果不存在这条边,用一个特殊标记 ∞ \infty∞ 来表示;如果 i = j i = ji=j,则权值为 0 00。
' {& f4 t& F: A! l它的优点是:实现非常简单,而且很容易理解;缺点也很明显,如果这个图是一个非常稀疏的图,图中边很少,但是点很多,就会造成非常大的内存浪费,点数过大的时候根本就无法存储。+ k5 k6 ^! c! Q( S, a7 G3 @
[ 0 ∞ 3 ∞ 1 0 2 ∞ ∞ ∞ 0 3 9 8 ∞ 0 ] \left[
3 e5 v9 j* K' _8 o. P+ x/ G! a) i01∞9∞0∞8320∞∞∞30
. P  F, V: ~$ G/ W0∞3∞102∞∞∞0398∞0
: r! E. ]( E2 P1 F  Y7 S% e\right]
1 q$ X- N5 W& n/ k. ?  c4 h4 y' ?/ F0 _+ |- [; a
( ?, N& I# I( z  Y( M* x

/ j4 d0 r0 A3 v" W' `% J  ]9 [1 @+ Q( T9 d+ k  t
​        ' F1 m2 H9 D" y: ~
  
7 ^" V3 A6 g# n5 s0 C  G+ t02 P( p' y# B+ K3 Y7 ]( s8 \
1
7 X4 S* a2 p/ h
& W3 i( a1 p7 S4 T/ x$ H9* D+ d1 [( ?- o) s7 [
​       
4 w% n: v  v; ^) q  6 s8 \+ h/ _; e" W9 ?

1 @& L0 p) d6 P- w8 @( E6 K0# p2 B7 ?, V, t  _
/ N* ?* ]) Y. H& |( h1 @6 ?: J/ D
86 ?1 e; T. K$ w" q0 c& r
​       
  F/ N* H' Y; J7 s/ k1 A3 ]! d0 |  
& D+ P& e' k' e8 x# O3
) x' N4 C4 s5 W2) J; n& B. K6 o8 m& _* F
0
+ E( P* q$ Y/ }" P
' v5 H& B: u# ]- P# U* r​       
5 J0 \) n0 p. `. i  
3 ?% I; x# m3 U4 [0 \3 N
: y9 O6 _" X) N3 E( K
4 E. l5 A4 H% c: G, ?3: y3 C: s( B2 `" @7 ^
0) S$ a# y6 \1 L1 }
​        ' x+ b6 N) }; J4 x
  
; f. f& }  s, ]) e) ^- a, B  A. N1 W( V8 l

1 i/ A) q, c; \) v
( X( I( z5 i) O2 V* _; @6 [& Y( I4 m) Y- L
​        : D5 `7 C' ~& `$ O
! G8 t0 A$ P7 `4 U0 ?- B8 W' Q! E
2)邻接表; c' m  D( N" U- y6 |+ `: E6 u
邻接表是图中常用的存储结构之一,采用链表来存储,每个顶点都有一个链表,链表的数据表示和当前顶点直接相邻的顶点的数据( v , w ) (v, w)(v,w),即 顶点 和 边权。
! {' }1 T) H6 W# E它的优点是:对于稀疏图不会有数据浪费;缺点就是实现相对邻接矩阵来说较麻烦,需要自己实现链表,动态分配内存。
; b5 B& j+ z  R& ?! B如图所示,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) 二元组。
" T3 v: ^3 I* N+ o; H$ t
  s) s3 ]2 u0 t9 v( l

9 x! {: \: v  Q5 Z7 x在 C++ 中,还可以使用 vector 这个容器来代替链表的功能;
/ B  W# O4 y6 @5 C  p: M    vector<Edge> edges[maxn];) ?- i- W  M5 w" r/ h+ u% N/ g9 i1 v
1- q6 b$ k. u* a$ F; ^$ Y/ y
3)前向星
2 ?5 z6 S7 x6 p' E6 E/ v前向星是以存储边的方式来存储图,先将边读入并存储在连续的数组中,然后按照边的起点进行排序,这样数组中起点相等的边就能够在数组中进行连续访问了。
1 n4 U, }3 H/ Q: _它的优点是实现简单,容易理解;缺点是需要在所有边都读入完毕的情况下对所有边进行一次排序,带来了时间开销,实用性也较差,只适合离线算法。% r9 U* \% x' Y+ M5 ~9 X
如图所示,表示的是三元组 ( u , v , w ) (u, v, w)(u,v,w) 的数组,i d x idxidx 代表数组下标。, J1 \% K3 G6 @0 F7 c

+ t3 ^& Q: Y- {7 s; O

& _. F7 Q! F9 r/ q$ v1 m1 a: t那么用哪种数据结构才能满足所有图的需求呢?9 |) h( w% F; n
接下来介绍一种新的数据结构 —— 链式前向星。
& j4 t# F$ D" h# R' m2 X; G4)链式前向星
% G& R1 Y! B! S7 w8 F* w1 P5 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 指向下一条边。" x' l% l9 _, C
具体的,我们需要一个边的结构体数组 edge[maxm],maxm表示边的总数,所有边都存储在这个结构体数组中,并且用head来指向 i ii 结点的第一条边。8 `  f5 a  F& e" s
边的结构体声明如下:
* {2 Q: l7 @  f7 g2 u8 P9 Pstruct Edge {" b$ a! W& B4 j6 z+ I
    int u, v, w, next;
$ H  y1 j  B$ d* t' m1 B    Edge() {}1 K' }7 c, H& U% S& N
    Edge(int _u, int _v, int _w, int _next) :1 _0 [0 _& b0 m* Q; ~0 Q
        u(_u), v(_v), w(_w), next(_next) & y  s: Q9 i6 U+ K
    {
. H3 T8 B" f, {$ H    }- [  u$ f+ w2 A' a7 ^6 _( k
}edge[maxm];
+ Q0 E3 ]6 n7 R* b5 F# H6 E1' N* G7 `; y( \' O1 A& R
2- a" k% g% _9 d. W0 @) }% I' m
3
: N) {" Q: t; ]7 ]1 k45 O2 b! V" U9 o- S% U3 Q
5: ~6 Q( ?4 @! A3 X
60 e1 W- a8 l( I, Z
72 l! W4 D* d- b* T
87 O4 e7 @$ f2 e$ a  R1 K+ A
初始化所有的head = -1,当前边总数 edgeCount = 0;; E0 P, s( x4 z0 S) p0 _- L" W
每读入一条 u → v u \to vu→v 的边,调用 addEdge(u, v, w),具体函数的实现如下:/ R$ A& j$ d! n4 J7 F9 b6 A; Y
void addEdge(int u, int v, int w) {
; ^) N3 T; ]" S; M9 D    edge[edgeCount] = Edge(u, v, w, head);
( ?+ s( x& {+ z! C# X  ]8 Q    head = edgeCount++;" M$ c  y  D: R/ h, [" j
}
/ |1 ^* |5 m. t* X6 p1
1 }! U# q& u! W: D( p/ x2
' ]- O  {0 _) e; Z; q3
- i+ K% a  O7 ?4 {4
: m  B# d" O' o; I这个函数的含义是每加入一条边 ( u , v , w ) (u, v, w)(u,v,w),就在原有的链表结构的首部插入这条边,使得每次插入的时间复杂度为 O ( 1 ) O(1)O(1),所以链表的边的顺序和读入顺序正好是逆序的。这种结构在无论是稠密的还是稀疏的图上都有非常好的表现,空间上没有浪费,时间上也是最小开销。
7 L2 N& G. ~3 Q! L3 d* }调用的时候只要通过head就能访问到由 i ii 出发的第一条边的编号,通过编号到edge数组进行索引可以得到边的具体信息,然后根据这条边的next域可以得到第二条边的编号,以此类推,直到 next域为 -1 为止。
$ O% k1 q7 E6 |3 wfor (int e = head; ~e; e = edges[e].next) {
# \4 w( e  B, O0 j( K9 ^* {' d6 t    int v = edges[e].v;
" |. t, s7 T7 Z& t    ValueType w = edges[e].w;
; e& E9 b! S2 |6 m' ]& P$ S    ...
& J5 i4 `( Q7 F( j& ~}
; e" n5 x5 m* i0 R: n! |5 E- ]1
- [# S! N6 c. r0 b8 f3 g1 {2
; T+ R) S+ ~) c* I; \2 A3' }* E% R6 d, y
43 H6 e) |1 _- z. J9 }6 `2 A( P. s
5
3 @8 D) Z$ R! C* l5 Q文中的 ~e等价于 e != -1,是对e进行二进制取反的操作(-1 的的补码二进制全是 1,取反后变成全 0,这样就使得条件不满足跳出循环)。
/ i& M! K  H) c  C. G$ K$ S4、算法入门) r- [/ p$ g7 s
算法入门,其实就是要开始我们的刷题之旅了。先给出思维导图,然后一一介绍入门十大算法。( [/ I- [5 f0 d. c

; y2 d; z# K4 o4 H

& Q# Y. b$ h( I& q3 b入门十大算法是 枚举、排序、模拟、二分、双指针、差分法、位运算、贪心、迭代、分治。
( [4 Y4 }" s3 L* }对于这十大算法,我会逐步更新道这个专栏里面:《LeetCode算法全集》。
+ N, {! t9 o/ @& @  [+ a1、枚举! a/ M% Y  m1 N0 ]. S1 I
枚举可以简单理解成for循环,从一个数组中遍历查找一个值,就是枚举;从一个数组中找到一个最大值,就是枚举;求数组所有数的和,也是枚举。
+ ?/ F; w0 F( P对于枚举而言,基本就是循环语句的语法学会,这个算法就算学会了。
# U. M1 H! L0 E0 v* K2、排序( y% Y$ R5 `$ E
既然是入门,千万不要去看快排、希尔排序这种冷门排序。
) q2 |+ _3 ?$ K, d冒泡排序、选择排序、简单插入排序 原理好懂,先看懂再说,其他不管。因为这三者都是基于枚举的。. d! Q. n% C4 N; w, P, M& B! f
C中有现成qsort排序函数,C++中有现成 sort排序函数,直接拿来用,等算法进阶时再回头来看快速排序的算法实现。
# M3 a3 ~" o$ r% h( o  @3、模拟
% [: ^& B7 \: R模拟就是要求做什么,你就做什么,完全不要去考虑效率问题。
! ?4 v8 L0 L! {/ F  ]不管时间复杂度 和 空间复杂度,放手去做!# p  B4 `% y; R0 C
但是,有时候模拟题需要一些复杂的数据结构,所以模拟题难起来也可以很男,难上加难。6 g. u: _2 c& z; ?
4、二分) r' V. y. _2 [- Q' M/ f
二分一般指二分查找,当然有时候也指代二分枚举。9 t: R8 a" O# {2 T% I
例如,在一个有序数组中查找值,我们一般这个干:8 I! T0 C) j& u
1)令初始情况下,数组下标从 0 开始,且数组长度为 n nn,则定义一个区间,它的左端点是 l = 0 l=0l=0,右端点是 r = n − 1 r = n-1r=n−1;
5 ^4 a7 K& N! Z. r+ r2)生成一个区间中点 m i d = ( l + r ) / 2 mid = (l + r) / 2mid=(l+r)/2,并且判断 m i d midmid 对应的数组元素和给定的目标值的大小关系,主要有三种:
: J& X! p7 t8 ]1 O8 R  2.a)目标值 等于 数组元素,直接返回 m i d midmid;
/ t& s' T; E+ v3 g0 N, o  W  2.b)目标值 大于 数组元素,则代表目标值应该出现在区间 [ m i d + 1 , r ] [mid+1, r][mid+1,r],迭代左区间端点:l = m i d + 1 l = mid + 1l=mid+1;- |+ d- r* T  u) R
  2.c)目标值 小于 数组元素,则代表目标值应该出现在区间 [ l , m i d − 1 ] [l, mid-1][l,mid−1],迭代右区间端点:r = m i d − 1 r = mid - 1r=mid−1;& q- t/ R3 P% r& P" Q6 t
3)如果这时候 l > r l > rl>r,则说明没有找到目标值,返回 − 1 -1−1;否则,回到 2)继续迭代。
2 Q$ w& R* w2 L5、双指针8 x9 i! V; b$ H
双指针,主要是利用两个下标在一个数组上,根据问题的单调性,进行指针偏移,由于每个指针只往后偏移,所以时间复杂度可以达到 O ( n ) O(n)O(n),由于思想非常简单,所以出题时,热度不低。
9 E, o; m# Y  h) N8 ?
# y9 f% [! A$ P7 R" w

1 c# w+ h# r) y2 `; K6、差分法
/ m+ R7 k* F3 Q0 S差分法一般配合前缀和。
, R5 m; I- q8 ^# m0 k对于区间 [ l , r ] [l, r][l,r] 内求满足数量的数,可以利用差分法分解问题;
/ \9 v" M, I/ J1 I7 B6 M+ t1 r假设 [ 0 , x ] [0, x][0,x] 内的 g o o d   n u m b e r good \ numbergood number 数量为 g x g_xg . e# g2 s' Y. {! s
x
. O0 l7 I  z+ x+ P​       
, s5 i& d6 l$ p, F5 u ,那么区间 [ l , r ] [l, r][l,r] 内的数量就是 g r − g l − 1 g_r - g_{l-1}g : p7 K: P5 }/ @' L+ ^4 I
r# S% V# K: }" L- `9 c
​        : d( [) S* E5 O; M, @
−g ! n" k2 ?, v* s+ e% ?0 y
l−1' W8 n* \. ^' x: w  u' m) w% D
​        ! h' o0 u6 ^. J# z0 O2 ]5 ~
;分别用同样的方法求出 g r g_rg
+ n# U3 Z! j' y$ Wr: {9 ?& I& T+ h- E6 u. S' A* L
​        " L9 s! |6 o& N% @" y$ I
  和 g l − 1 g_{l-1}g
# |. H5 R0 ?4 S9 r  A3 j" wl−1: Y2 A$ ~9 Q! g+ s: n: X. P/ s/ ^- Q8 r
​        " r& }9 e8 l- q! @$ b
,再相减即可;) I7 L3 l9 {# d' W- \7 ^
& M! o* w0 J+ h- [6 q6 Z, t
0 g: h. q$ [) d: z& w4 k6 ~
7、位运算
7 ^: ^3 \, x! x: _位运算可以理解成对二进制数字上的每一个位进行操作的运算。
6 @. F' b. s: w$ S' v" }位运算分为 布尔位运算符 和 移位位运算符。) h# K/ Y- s  |# H9 {4 x/ N
布尔位运算符又分为 位与(&)、位或(|)、异或(^)、按位取反(~);移位位运算符分为 左移(<<) 和 右移(>>)。/ x  Y! J2 O% l' y5 x: L
如图所示:
( a  X+ T. j# k! i1 H5 p
& u, N% D) \9 T2 e8 l4 P
* S+ ?5 K% {. ~! L+ K9 k
位运算的特点是语句短,但是可以干大事!
" I& B% x! n' b0 |9 l" z7 g  x9 a( |比如,请用一句话来判断一个数是否是2的幂,代码如下:
# I% @+ R3 \$ b& g!(x & (x - 1))9 q7 Z: k* C' p/ u& Q
1$ r4 o; L) `0 E) V! I
8、贪心
: u, \8 w) l  E" u2 f贪心,一般就是按照当前最优解,去推算全局最优解。% A3 ~) a" ^5 R$ @* S* V
所以,只有当当前最优解和全局最优解一致时才能用贪心算法。贪心算法的证明是比较难的,但是一些简单的贪心问题会比较直观,很容易看出来这个能够这么贪。, ^' R9 M' r1 W( q, e
9、迭代/ x% ]5 h7 y7 |- c  s4 R
每一次对过程的重复称为一次“迭代”,而每一次迭代得到的结果会作为下一次迭代的初始值,周而复始,直到问题全部解决。
/ x" F1 X; C2 N( f, [! N10、分治
8 t( s/ _+ g' }  V( H分治,就是把问题分成若干子问题求解,子问题解决后,问题就解决了。一般利用递归实现。属于初学者比较头疼的内容。递归一开始学习的时候,一定要注意全局变量和局部变量的关系。4 f4 A0 {1 t& d+ |9 J
5、算法进阶
* S# Y( N  q4 _* z# @算法进阶这块是我打算规划自己未来十年去完成的一个项目,囊括了 大学生ACM程序设计竞赛、高中生的OI竞赛、LeetCode 职场面试算法 的算法全集,也就是之前网络上比较有名的 《夜深人静写算法》 系列,这可以说是我自己对自己的一个要求和目标吧。
) e4 X8 \% b4 i5 G% N, S2 F/ E如果只是想进大厂,那么 算法入门 已经足够了,不需要再来看算法进阶了,当然如果对算法有浓厚兴趣,也欢迎和我一起打卡。由于内容较难,工作也比较忙,所以学的也比较慢,一周基本也只能更新一篇。5 h0 d8 v/ P5 K! p% Q9 h; M
这个系列主要分为以下几个大块内容:
* c& V: O8 N% r& P" M. F3 F5 x  1)图论5 E/ P/ {- `1 l) {/ L( C* \6 \
  2)动态规划& U: A+ n2 K9 ?) o  x: y
  3)计算几何7 @! u6 B$ x' G. O3 U4 q
  4)数论
2 B  Z; i3 X' x* g6 i  B9 S  |  5)字符串匹配
( q' K4 o2 b% z# T) O  6)高级数据结构(课本上学不到的)" v7 v3 [$ G; }0 }# \
  7)杂项算法  l! W' _, [0 I

( n7 e. k+ `; ^/ d

: e5 f! O; z+ U/ s* M/ a先来看下思维导图,然后我大致讲一下每一类算法各自的特点,以及学习方式:
- F7 ]" M; b! e
6 M* m! B4 F: E& ^' m1 J' z6 \. n6 c
" P" b- X/ [/ H# G  [' z
1 O. A- g0 T' o* N4 i

% f: E$ o& w5 d3 S2 g+ t( |" g, }1)图论: C: Q5 G9 W- x+ b  I
1、搜索概览+ V) b( T& I2 B- P# K
图论主要围绕搜索算法进行展开。搜索算法的原理就是枚举。利用计算机的高性能,给出人类制定好的规则,枚举出所有可行的情况,找到可行解或者最优解。
# E' X8 p2 E% ?6 w' O9 j
1 s- r% R3 J1 R+ @

7 a; l3 X7 |' p: X+ B2 P$ [比较常见的搜索算法是 深度优先搜索(又叫深度优先遍历) 和 广度优先搜索(又叫广度优先遍历 或者 宽度优先遍历)。各种图论的算法基本都是依靠这两者进行展开的。  N  T4 L  ?3 I" d- i
2、深度优先搜索
! I+ V* b6 x$ G' k9 v深度优先搜索一般用来求可行解,利用剪枝进行优化,在树形结构的图上用处较多;而广度优先搜索一般用来求最优解,配合哈希表进行状态空间的标记,从而避免重复状态的计算;  C8 ?, x. w' I" y6 e, a1 X
原则上,天下万物皆可搜,只是时间已惘然。搜索会有大量的重复状态出现,这里的状态和动态规划的状态是同一个概念,所以有时候很难分清到底是用搜索还是动态规划。0 Z& I8 ?( G$ M6 H& z
但是,大体上还是有迹可循的,如果这个状态不能映射到数组被缓存下来,那么大概率就是需要用搜索来求解的。0 W7 g8 e5 F) ?4 U0 ?, ^
如图所示,代表的是一个深度优先搜索的例子,红色实箭头表示搜索路径,蓝色虚箭头表示回溯路径。
& z/ ^5 T3 x6 E3 P) a
' s. e4 l/ W9 x" t

+ u1 B/ N9 }( l8 S3 ~6 {红色块表示往下搜索,蓝色块表示往上回溯,遍历序列为:+ B& X+ Q6 W5 V3 ]: k( @
        0 -> 1 -> 3 -> 4 -> 5 -> 2 -> 6$ V8 T6 v. f8 x2 _) F3 o" n6 ?
1
; G) O9 `! B- q% U% o0 C- P同样,搜索的例子还有:
9 ]6 t' f# x. i% s# `9 {4 j1 \* [/ J! t1 P' q3 p' w; B+ c

! N. i; m) c8 o* q计算的是利用递归实现的 n nn 的阶乘。
' u6 q6 J- a1 ]3、记忆化搜索2 ^. Q! Z+ w$ d3 H
对于斐波那契函数的求解,如下所示:
+ l8 ?6 U- ^! T8 wf ( n ) = { 1 ( n = 0 ) 1 ( n = 1 ) f ( n − 1 ) + f ( n − 2 ) ( n > 2 ) f(n) =9 I$ @  w8 W1 [# I0 V5 k4 V" l
⎧⎩⎨11f(n−1)+f(n−2)(n=0)(n=1)(n>2)
$ k5 ^5 g8 ]# x) F3 D{1(n=0)1(n=1)f(n−1)+f(n−2)(n>2)
+ M; {5 ~( U5 mf(n)= $ O. t8 e2 Q- _" a

/ I4 E2 D* a! Q4 b2 q. M( @9 `  v" Z# Q8 \5 v! E# h4 Q9 S# F

3 p4 J1 Q# k/ a3 g$ `$ ]  n4 a
, W( T! W+ C/ k  C) \' }& O
) a7 Y/ C6 ?  R​       
. p! e0 F5 c) d" I/ C1 k9 J. S  
4 |$ A, _( Z- Z7 [; b; Y1. k% {7 [& v6 Z; W
1  |+ |  V; P$ q5 \+ H
f(n−1)+f(n−2)$ @9 p, t% e/ J# A6 W* @
​        8 }6 h3 H& g% `$ ^6 Q0 S
  ' Z4 L4 Q9 y" d' Q7 C
(n=0)
7 P; E5 k$ s/ k, J(n=1)
( k8 W2 F$ n1 |5 M) R(n>2): P# C6 m5 U, {8 R$ _1 Y7 V" w
​       
7 H- s' d( c) [- D7 T% N
3 H: ^; G. ^8 j" F1 l- y! c3 Y7 v/ n  t  f对于 f ( 5 ) f(5)f(5) 的求解,程序调用如下:
7 b% t, K( C* `4 O
6 J" d6 k( Z' J4 h) r
. x9 ]! O7 k3 G
这个过程用到了很多重复状态的搜索,我们需要将它优化,一般将一些状态缓存起来。# C* [2 O" N2 r; r% _% b7 G4 i
我们通过一个动图来感受一下:" H  U* i0 j! h4 J" [1 |# o0 ~' a

. @! [  {' x) O+ e+ u

% I2 S+ b9 n7 v# H2 C当第二次需要计算 f ( 2 ) f(2)f(2) 和 f ( 3 ) f(3)f(3) 时,由于结果已经计算出来并且存储在 h [ 2 ] h[2]h[2] 和 h [ 3 ] h[3]h[3] 中,所以上面这段代码的fib != inf表达式为真,直接返回,不再需要往下递归计算,这样就把原本的 “递归二叉树” 转换成了 “递归链”, 从而将原本指数级的算法变成了多项式级别。" c: J# d1 S* n2 L+ t
这就是记忆化搜索,像这种把状态缓存起来的方法,就是动态规划的思想了。
! {* D: h" p- l! v. P4、广度优先搜索. W5 t8 ~1 O( Y" ?
单向广搜就是最简化情况下的广度优先搜索(Breadth First Search),以下简称为广搜。游戏开发过程中用到的比较广泛的 A* 寻路,就是广搜的加强版。
% [1 t0 @* ^! g  E我们通过一个动图来对广搜有一个初步的印象。+ M& g$ n: o% u4 A

) ~0 ?5 ~7 f( Y$ C6 J  E# W8 }
/ O8 ^0 r: }4 f3 j/ E; Z: v, G

( K" k8 S; ^' f( m
2 x* K5 L/ z) X5 f0 x
从图中可以看出,广搜的本质还是暴力枚举。即对于每个当前位置,枚举四个相邻可以行走的方向进行不断尝试,直到找到目的地。有点像洪水爆发,从一个源头开始逐渐蔓延开来,直到所有可达的区域都被洪水灌溉,所以我们也把这种算法称为 FloodFill。
& u: r, _7 |! V( I2 k4 o- D9 F4 Y那么,如何把它描述成程序的语言呢?这里需要用到一种数据结构 —— 队列。0 u- H3 L* @( A% B5 O8 P  x9 `
这时候,算法和数据结构就完美结合了。
3 T3 J% i; \- `2 L9 a  C2)动态规划; c  W- f3 O8 F
动态规划算法三要素:
: E( X" s* V$ I( B, g8 w9 ~  ①所有不同的子问题组成的表;+ Q4 n  z! D; ?. G
  ②解决问题的依赖关系可以看成是一个图;' g% M1 t" H; ]: V5 j! Y
  ③填充子问题的顺序(即对②的图进行拓扑排序,填充的过程称为状态转移);5 j' y( r3 x" l! e' G- W

2 n2 H  h* K  }$ S* y

3 v& z# W, V% ]- E; _9 K, ]如果子问题的数目为 O ( n t ) O(n^t)O(n ( c& {2 w. D& {8 Q
t9 n# l, `' Q  C# E+ g
),每个子问题需要用到 O ( n e ) O(n^e)O(n ' O2 x: J" ?! H5 m
e
2 x: j$ `: e0 y ) 个子问题的结果,那么我们称它为 tD/eD 的问题,于是可以总结出四类常用的动态规划方程:(下面会把opt作为取最优值的函数(一般取 m i n minmin 或 m a x maxmax ), w ( j , i ) w(j, i)w(j,i)为一个实函数,其它变量都可以在常数时间计算出来)。. K( e0 v& }0 Z: e
1、1D/1D
0 k" J* _9 n; r# s$ j2 b) kd [ i ] = o p t ( d [ j ] + w ( j , i ) ∣ 0 < = i < j ) d = opt( d[j] + w(j, i) | 0 <= i < j )& ^# {  @, \/ F
d=opt(d[j]+w(j,i)∣0<=i<j)
$ E/ |4 s  X! {; t, \: g' }4 _9 b; z; q状态转移如图四所示(黄色块代表d [ i ] dd,绿色块代表d [ j ] d[j]d[j]):
" n% f! y- R2 t; E  _3 u
' v' k7 S. O0 Z1 b5 M

( z3 w$ X/ M' [! m5 v7 {  F' K5 s% J6 x这类状态转移方程一般出现在线性模型中。
+ w/ X5 o9 _' }9 w2、2D/0D
( M8 N( V0 R& od [ 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} )1 d. E$ P4 {  Y( v7 x! G. f
d[j]=opt(d[i−1][j]+x 1 S$ B3 @9 O' ]! _! w" K! c& V
i
( N$ _- Z: ]6 n" I$ V) [​       
* O8 s4 s' X/ E8 o! L ,d[j−1]+y   }' f3 D6 o% Y% ]2 m0 y
j  I- G" U6 x+ R+ H1 ^1 j
​       
! S! f) r5 j. O) l ,d[i−1][j−1]+z % h5 [$ R% G9 V9 i# T) {6 U
ij
1 `& J7 j  W8 ?/ Q$ G/ `# T+ M​       
1 H. ?5 G2 b& D3 z )* r- x1 S# r# f5 v
状态转移如图四所示:6 K& y  K  ^* W

3 e% S4 |. a0 O, w% D
2 N7 x- f& f0 j% d1 c5 ]
比较经典的问题是最长公共子序列、最小编辑距离。0 ^( P. ~3 s4 q4 H. z3 \0 ^
有关最长公共子序列的问题,可以参考以下文章:夜深人静写算法(二十一)- 最长公共子序列
) H' ~/ A- ]5 D, u2 |有关最小编辑距离的问题,可以参考以下文章:夜深人静写算法(二十二)- 最小编辑距离$ W4 [" C0 R; E" {6 j' l* d
3、2D/1D
* e( b6 A- o  Vd [ 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] )
6 O7 @; A+ [% \3 `& P$ }  a1 Q- kd[j]=w(i,j)+opt(d[k−1]+d[k][j])% w2 p! b$ a- }. `4 o$ j
区间模型常用方程,如图所示:8 M5 c0 B5 z- @8 U- {

! N) S) [" R1 I$ x
: L5 y0 n: H  p5 b
另外一种常用的 2D/1D 的方程为:
0 m' V! E! k  c( {1 J. {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 ), A' p; K. p& e0 k4 [
d[j]=opt(d[i−1][k]+w(i,j,k)∣k<j)# l2 D$ r: k+ [" O( h/ [9 T
区间模型的详细内容可以参考以下这篇文章:夜深人静写算法(二十七)- 区间DP
$ M' U) V9 B) z6 h3 u$ C8 f( X: `4、2D/2D9 ]: U' E% a6 D1 H3 E
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)
: p1 Q; G9 [/ Md[j]=opt(d[i ( D- X. }9 N3 y* E$ e
1 b1 H+ \9 C2 I4 o+ I; c5 ^
][j 2 D0 Z$ s# J  p1 s. M' H$ q
2 z* C7 D' i; {- c2 u, z# G6 M& P
]+w(i 0 z4 h- H$ T5 k3 _6 E
. {( ~9 e' u! }, Q6 b
,j
8 x7 i! j0 W8 E( J6 t+ J( h% V  q& W
,i,j)∣0<=i , d: C8 H2 t% D5 h# O" c$ \

# |) ?  o; S( y+ a$ t1 ` <i,0<=j 9 \+ u  o% ~; J: M& i2 E
/ F9 X$ E- n3 X; X+ S
<j)
& G8 y( K6 b- U6 O5 @% e' g% Y如图所示:
) z. `8 t! j: E) a$ F. @. Y( I* p) H! M+ G0 R

- J: }& V$ l% M( C) J/ i7 N常见于二维的迷宫问题,由于复杂度比较大,所以一般配合数据结构优化,如线段树、树状数组等。
: Y6 ]/ ]$ Z4 R, n对于一个tD/eD 的动态规划问题,在不经过任何优化的情况下,可以粗略得到一个时间复杂度是O ( n t + e ) O(n^ {t+e})O(n
+ W: k& [4 t: \3 Q- `& R" xt+e
8 _! K5 s+ Q/ S) Z! t9 F3 b ),空间复杂度是O ( n t ) O(n^t)O(n 4 _) X' n/ A2 f: h
t4 i4 c; K/ m$ h, _6 k) \
) 的算法,大多数情况下空间复杂度是很容易优化的,难点在于时间复杂度,后续章节将详细讲解各种情况下的动态规划优化算法。; N& V  L8 J2 n
3)计算几何" v) C; _7 W1 Q# A4 O
计算几何的问题是代码量最大的。它是计算机科学的一个分支,以往的解析几何,是用代数的方法,建立坐标系去解决问题,但是很多时候需要付出一些代价,比如精度误差,而计算几何更多的是从几何角度,用向量的方法来尽量减少精度误差,例如:将除法转化为乘法、避免三角函数等近似运算 等等。' w# G; A$ @+ I1 v/ @1 [" |
如果一个比赛中,有一道计算几何的题,那么至少,它不会是一道水题。
  Z' v3 Q: Z0 j+ K( \7 j6 s! E1、double 代替 float
0 j- P. p/ b1 W: k" Ec++ 中 double 的精度高于 float,对精度要求较高的问题,务必采用 double;
$ `) m5 R; }3 |* Y) P2、浮点数判定- ^* P; L" ]( G
由于浮点数(小数)中是有无理数的,即无限不循环小数,也就是小数点后的位数是无限的,在计算机存储的时候不可能全部存下来,一定是近似的存储的,所以浮点数一定是存在精度误差的(实际上,就算是有理数,也是存在误差的,这和计算机存储机制有关,这里不再展开,有兴趣可以参见我博客的文章:C++ 浮点数精度判定);: n7 q4 b  \- j
两个浮点数是否相等,可以采用两数相减的绝对值小于某个精度来实现:# l& |3 }* |) k- M+ X" a9 Q# d4 G; N
const double eps = 1e-8;
& {* V4 q/ a3 `; O, ebool EQ(double a, double b) {
* x! q1 I. z, u( o# v+ _    return fabs(a - b) < eps;
  Z, i# b4 W7 ~% b7 p}
% n! U* @$ z9 ]# t" d" o1
. g& C: S, z/ B2 T( ]( s6 H2. H5 O8 _& T6 L
3
5 [4 Q' g$ W% p) P/ j, X0 T5 U4
( f$ x2 d; {2 U1 o并且可以用一个三值函数来确定某个数是零、大于零还是小于零:
0 }$ i- t: D& `% f' Wint threeValue(double d) {
2 v" S- ^% @, t+ f& q( s% ]' y# |0 ~& J    if (fabs(d) < eps)
5 v1 P% y! v2 o5 [6 {$ W        return 0;% a5 n8 }- j% B- U
    return d > 0 ? 1 : -1;
2 L$ m5 S5 c9 F, L' ]}
% J; A  u; P: I* J13 F- N" F4 m4 z; a" M( U
2( U# a( E/ W- B9 K) e! a" X0 }% A6 z
3
( f' X. D4 l( C. d* W4
/ U$ E3 v) ~' ]+ T5; V, O4 L6 c( J; a  Q9 ?
3、负零判定3 z, o2 Y! A$ m3 a% E- z
因为精度误差的存在,所以在输出的时候一定要注意,避免输出 -0.00:0 n* e$ O! _/ R5 L
    double v = -0.0000000001;
* V. }, _, C) ~    printf("%.2lf\n", v);
* j& q2 |% E+ K, l7 I5 U- e1
( t0 }  N7 t) g5 r0 W+ L; L' w2- ?6 S; z0 H% r( ?
避免方法是先通过三值函数确定实际值是否为0,如果是0,则需要取完绝对值后再输出:7 p8 u( _; a% Z! e/ P% A7 F
    double v = -0.0000000001;
) n5 }" i& ^+ l3 t6 \8 @    if(threeValue(v) == 0) {
# k6 [/ ?% J1 E( z7 T. b$ D/ d! l        v = fabs(v);
8 S; `# d' H  d! _4 d    }; m5 _" |. F! z; l; T
    printf("%.2lf\n", v);* K, v4 B8 S: b) G: t# ^
1
# X* O$ Q4 E( @4 c3 P& }2
; W3 q$ S# i9 j6 ]: {" p5 }- ]3$ S3 Y3 V3 O! k, U: \
4
# y: n2 c  j+ @+ \! s4 u5
* L' J3 w, r; p) ]( ~4、避免三角函数、对数、开方、除法等
) S  H: u: s% M; U1 @, g* hc++ 三角函数运算方法采用的是 CORDIC算法,一种利用迭代的方式进行求解的算法,其中还用到了开方运算,所以实际的算力消耗还是很大的,在实际求解问题的过程中,能够避免不用就尽量不用。' @) K* f/ O2 k6 F" U2 G3 n
除法运算会带来精度误差,所以能够转换成乘法的也尽量转换为乘法运算。0 G- `6 o7 }& t2 P9 s; ]
5、系统性的学习+ K( C% J) e. e4 @: N
基础知识:点、向量、叉乘、点乘、旋转、线段、线段判交、三角形面积;* e8 w' @- H# f3 z' @; @
进阶知识:多边形面积、凸多边形判定、点在多边形内判定;
! q" f1 Z* ]; X; M3 K  W7 p相关算法:二维凸包、三维凸包、旋转卡壳、多边形面积交、多边形面积并、多边形面积异或、多边形和圆的面积交、半平面交、最小覆盖圆、最小包围球、模拟退火。# L' t# H- ]& V' q. X& y$ K0 F# Z

3 v* Z: w+ o5 C1 B8 _3 A1 K$ e
  i# Z# _7 H/ j5 M7 C$ ^# W% n
学习计算几何,最好是系统性的,刷题的过程中不断提炼出自己的模板。0 @+ H- z, M, m, H0 e
4)数论
" R* P4 ~: v9 ~+ M刷题的时候遇到不会的数论题,真的是很揪心,从头学起吧,内容实在是太多了,每个知识点都要证明吃透,不然下次遇到还是不会;不学吧,又不甘心,就是单纯的想把这个题过了,真是进退两难!0 J2 F, \; t8 y$ c* W9 ?
数论对一个人的数学思维要求较高,但是一般也是一些固定的模式,所以把模板整理出来很重要。) n9 s1 R; P$ T# `
当然,数论也有简单问题,一般先做一些入门题提升信心。' k' ~- x4 R1 c9 `
1、数论入门; R! v- c+ P# g7 N0 g3 ]$ R
主要是一些基本概念,诸如:3 n, [  c; e+ n$ \1 y3 C+ W
整除性、素数与合数、素数判定、素数筛选法、因数分解、算术基本定理、因子个数、因子和、最大公约数 (GCD) 和 最小公倍数 (LCM)、辗转相除、同余、模运算、快速幂取模、循环节;4 B+ h" |0 o& `% t3 ?' n6 i# b
2、数论四大定理& N0 O/ R1 K' I7 H- K- Q
这四个定理学完,可以KO很多题:
  p: c$ ^' A9 c2 h欧拉定理、中国剩余定理、费马小定理、威尔逊定理* p- @- G9 p9 S, H( G7 k2 k
3、数论进阶  P* n- U, N4 i$ ~- c) h
系统性的学习,基本也就这些内容了:" n& b* @% ]6 ~! t9 }" L. B# ?! x
扩展欧几里得、逆元、欧拉函数、同余方程组、扩展欧拉定理、RSA、卢卡斯定理、整数分块、狄利克雷卷积、莫比乌斯反演、大数判素、大数因子分解、大步小步离散对数等等。4 a& x' m! `* p5 r" s
5)字符串匹配0 j! A$ {+ Z* b3 x5 S- f
字符串匹配学习路线比较明确。
, Q2 U3 t7 ~# [$ @: K先学习前缀匹配:字典树。2 r! f- {! r7 D3 X  P* i
然后可以简单看一下回文串判定算法:Manacher。9 S4 f) `; a8 o
以及经典的单字符串匹配算法:KMP。
5 W* @0 g# M- F3 D) u实际上平时最常用的还是 BM 算法,而ACM中基本不考察。
+ P2 Q2 V' o2 c# b) [然后就是较为高阶的 前缀自动机、后缀数组、后缀树、后缀自动机了。- ]0 a1 E' d6 z) M( V6 k
关于 算法学习路线 的内容到这里就结束了。& ]3 }0 }  m/ }0 ~1 U0 I
如果还有不懂的问题,可以 想方设法 找到作者的微信进行在线咨询。0 k+ \* \0 ]8 X
参考资料1 J; X& S6 `3 G& P
【阶段一】C语言学习资料:《光天化日学C语言》(日更)  Q- I4 L6 ^  i, [
【阶段二】C语言例题:《C语言入门100例》(日更)1 n6 O  m1 t2 B
【阶段三】算法入门题集:《LeetCode算法全集》(日更)# W9 u  f. l" H" f. t" E1 s1 U0 _; J! _
【阶段四】算法进阶:《夜深人静写算法》(周更)
1 O; X$ r0 ~$ ^( m8 E3 ^————————————————
3 y& V4 W8 q) b$ u) B版权声明:本文为CSDN博主「英雄哪里出来」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。0 @. }: |+ |# l( |
原文链接:https://blog.csdn.net/WhereIsHeroFrom/article/details/118382228
7 F+ i6 ~- h6 r) r9 c' R& g( |6 d' J
: V: k  B: d: M  c

作者: 1051373629    时间: 2021-8-17 17:14
太难得了
# ?' I( ]; i+ ?) v9 `; E+ J




欢迎光临 数学建模社区-数学中国 (http://www.madio.net/) Powered by Discuz! X2.5