数学建模社区-数学中国

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

作者: 杨利霞    时间: 2021-7-8 15:06
标题: ❤️两万字《算法 + 数据结构》全套路线❤️(建议收藏)
8 @+ b+ N. Z, B: F# U' t3 U: v
❤️两万字《算法 + 数据结构》全套路线❤️(建议收藏)) N! j, ~) F6 n9 ]1 f
% ?8 c* b% P' [2 V$ m  r2 T; W6 Y
前言
6 a: E7 ~( V& `  I0 t3 w* _  所谓活到老,学到老,虽然我感觉自己已经学了很多算法了,但是昨天熬夜整理完以后发现,自己还是个弟弟,实在忍不住了,打算把 算法学习路线 发出来,我把整个算法学习的阶段总结成了五个步骤,分别为: 基础语法学习(重要)、语法配套练习、数据结构、算法入门、算法进阶。本文梳理了这五个大项的思维导图,在下文会有详细介绍。/ C8 H* a! ~" H! ?
  希望各位能够找到自己的定位,通过自己的努力在算法这条路上越走越远。2 r6 X, I! I4 t
  刚开始切勿心浮气躁,千万不要给自己立 flag,说一定要把这么多东西都学会。就算你的精力旺盛,日夜操劳,时间也是有限的。所以,首先是明确我们要做什么,然后制定好一个合理的 目标 ,再一点一点将要学习的内容逐步付诸实践才是最重要的。) x5 F/ ?4 U' X" `7 J
  每日一篇C语言打卡,目前更新到:光天化日学C语言(20)- 赋值运算符与赋值表达式 | 让代码变得更加简介(建议收藏)。6 t. n) ~9 d; Z' z

% ^5 D  t7 Z; s! L/ N# U7 P" ]
9 w) O* a/ d8 v0 t( ~$ |. |

6 Q. {& {% z) H  e1 W" A; s& @8 d* N

# V' s  s3 l% h7 _5 A# C! f
: R- a; J( q4 [  k5 S* H( h* N2 T

: ]  N2 |0 e' m: j7 F% Q1 |
$ R* l$ M) i' w) k

9 Y( K" w9 \6 U4 o* W# C图片较大,文章中有拆解,需要原图可以留言找我要哈
' l5 E; ?2 S/ N, Y3 v, E6 }) p/ V. o1、基础语法学习
, l+ N4 S5 J3 g4 y' }算法是以编程语言为基础的,所以选择一门编程语言来学习是必须的。
* A4 a6 J1 I/ \5 j$ ~+ |5 D+ R因为作者本身是C/C++技术栈的,所以就拿C语言来举例子吧。如果是 Java、Python 技术栈,可以跳过 C语言相关的内容。这一小节,先给出学习路线图,然后我再来讲,每部分应该如何去学。1 R5 F  _/ b! ~0 c0 m& L  K$ l
( f* X$ o- E4 M% B. I0 b
$ K& E9 H+ |# N- L: m, h
4 u4 x( c- a4 p: t5 ]

2 g' r' I) C+ f- C1)HelloWorld
' n  u; H) m( J4 h6 _) M无论是 Java、Python、C/C++,想要上手一门语言,第一步一定是 HelloWorld,先不要急着去配环境。如果环境配了几个小时,可能一开始的雄心壮志就被配环境的过程消磨殆尽,更加不要谈日后的丰功伟业了。
- Q) V8 a% P: X# d4 }/ p  Z* ~2)让自己产生兴趣& a) }$ Y5 A" j1 l2 V2 Z
所以,我们需要让这件事情从一开始就变得 有趣,这样才能坚持下去。比如找一个相对较为有趣的教程,这里我会推荐这个:《光天化日学C语言》。听名字就比较搞笑,可能作者本身也不是什么正经人,哈哈哈!虽然不能作为一个严谨的教程去学,起码可以对搞笑的内容先产生兴趣。从而对于语言本身有学习下去的动力。
! b/ o" o5 e* U# h8 j# o8 `; p刚才提到的这个系列,可以先收藏起来。回头再去看,它讲述的是 对白式 的 C语言教学,从最简单的输出 HelloWorld 这个字符串开始讲起,逐渐让读者产生对C语言的兴趣。这个系列的作者是前 WorldFinal 退役选手,一直致力于 将困难的问题讲明白 。我看了他的大部分教程,基本都能一遍看懂。算了,不装了,摊牌了,因为我就是这个作者。
1 q8 Z; ~$ P" n- i* z& p3)目录是精髓2 d# V5 l* g5 h
然后,我们大致看下你选择的教程的前几个章节,那些标题是否有你认知以外的名词出现,比如以这个思维导图为例,前几个章节为:
; w  {' t5 ?' K4 y8 z, @1、第一个C语言程序
' R( M" w9 s  T; T* v2、搭建本地环境' E# N8 a0 w- z1 X( U9 m* A1 d
3、变量' `  d( {1 \" d7 t3 W
4、标准输出
( X, H/ |4 w3 |* Q( d% S5、标准输入3 w5 v0 j, k. @" R5 I! T0 v
6、进制转换入门
0 \3 N8 X! X( c+ s7 E+ P7、ASCII字符& F5 W# [! K% L$ B
8、常量% f* B! _! L) s  E5 x/ ^, B

! x$ V! w0 O# z/ ?

  O+ o% U" C, T/ J" e5 w3 O+ H+ H如果你觉得这些名词中有 3 / 4 以上是没有什么概念的。那么,可能需要补齐一些数学、计算机方面的基础知识。反之,我们就可以继续下一步了。
9 p7 R9 n" z+ Y1 t' }' X4)习惯思考并爱上它
5 p  }  a3 y, Z  {7 h$ \只要对一件事情养成习惯以后,你就会发现,再难的事情,都只是一点一点积累的过程。重要的是,每天学习的过程一定要吃透,养成主动思考的好习惯。因为,越到后面肯定是越难的,如果前期不养成习惯,后面很可能心有余而力不足。
; g' }; ]  c$ i$ c: T就像刷题,一旦不会做就去找解题报告,最后就养成了看解题报告才会做题的习惯。当然这也是一种习惯,只不过不是一种好习惯罢了。
* w4 k5 |& V" L' h0 c5)实践是检验真理的唯一标准
4 e: G$ u$ o8 {光看教程肯定是不行的,写代码肯定还是要动手的,因为有些语法你看一遍,必定忘记。但是写了几遍,永世难忘。这或许就是写代码的魅力所在吧。
. F+ o  A% b  L0 H* v4 {所以,记得多写代码实践哟 (^U^)ノ~YO
: r, }  \; ]/ M, \& m6)坚持其实并没有那么难( y% ?7 J$ `3 I4 [/ G: k4 y- ^
每天把教程上的内容,自己在键盘上敲一遍,坚持一天,两天,三天。你会发现,第四天就变成了习惯。所以坚持就是今天做了这件事情,明天继续做。% Z' b, R* F  L, Q. p4 {
7)适当给予正反馈# u- R, v, p- b0 t! n- w
然而,就算再有趣的教程,看多了都会乏味,这是人性决定的,你我都逃不了。能够让你坚持下去的只有你自己,这时候,适当给予自己一些正反馈就显得尤为重要。比如,可以用一张表格将自己的学习计划记录下来,然后每天都去分析一下自己的数据。
/ f- e! _5 c) w6 S当然,你也可以和我一样,创建一个博客,然后每天更新博文,就算没有内容,也坚持日更,久而久之,你会发现,下笔如有神,键盘任我行!更新的内容,可以是自己的学习笔记,心路历程 等等。7 o# H. t& d: w3 d: O& J7 M* y
看着每天的粉丝量呈指数级增长,这是全网对你的认可,应该没有什么会是比这个更好的正反馈了。
+ h6 @" L3 |2 K  M  w6 U2 \8)学习需要有仪式感: {5 k& d4 q5 Q" d' i) V# v) w
那么,至此,不知道屏幕前的你感想如何,反正正在打字的我已经激情澎湃了。已经全然忘记这一章是要讲C语言基础的了!
4 f3 |  ]4 m! Y  c介于篇幅,我会把C语言基础的内容,放在这个专栏 《光天化日学C语言》 里面去讲,一天更新一篇,对啊,既然说了要坚持,要养成习惯,我当然也要做到啦~如果你学到了哪一章,可以在评论区评论 “打卡” ,也算是一种全网见证嘛!$ |) \+ d5 p# o3 v
我也很希望大家的学习速度能够超越我的更新速度。- F/ h+ @& c6 S
2、语法配套练习
7 \8 V" ~# f( W学习的过程中,做题当然也是免不了的,还是应征那句话:实践是检验真理的唯一标准。. t  g$ x( g4 ~7 q
而这里的题库,是我花了大量时间,搜罗了网上各大C语言教程里的例题,总结出来的思维导图,可以先大致看一眼:# }6 e7 K% ~3 k6 ^* ^

; p: p) J  t1 F) V: U

8 u/ Q/ {' e4 Z7 s
; A; ], s0 u7 W4 U
) g6 T/ e) u) _  l! |
从数学基础、输入输出、数据类型、循环、数组、指针、函数、位运算、结构体、排序 等几个方面,总结出的具有概括性的例题 100 道 《C语言入门100例》,目前还在更新中。
. V  s. h$ Q1 |( N% \这里可以列举几个例子:
1 q/ E) A. U9 ~8 C1、例题1:交换变量的值
2 F& `  J, D' x- ]) t4 W一、题目描述: H8 R7 f" \; R1 c  p
  循环输入,每输入两个数 a aa 和 b bb,交换两者的值后输出 a aa 和 b bb。当没有任何输入时,结束程序。
+ ^9 m: K0 W9 S' J8 y5 u8 r6 n" {: c+ ], [; D
* S8 S6 V, |7 A5 N* W/ J

9 H# o0 B% Y) x9 w

3 m. ?( L+ i( u  w: n& O( w二、解题思路
' w- f" m- @/ n( g8 s难度:🔴⚪⚪⚪⚪
% q4 j  X9 d$ t  R
* E3 L& y( B4 r5 K/ i3 N  o
, E& t( f, ^/ D* B
这个题的核心是考察如何交换两个变量的值,不像 python,我们可以直接写出下面这样的代码就实现了变量的交换。: f  g+ _* z6 L% l" A8 H5 W+ Q
a, b = b, a
5 J2 f7 `5 K7 ?$ C7 U% a" N4 _: `* V1
7 ]/ @/ X# d- T! o( r在C语言里,这个语法是错误的。
# m% q2 ^+ l. v我们可以这么理解,你有两个杯子 a aa 和 b bb,两个杯子里都盛满了水,现在想把两个杯子里的水交换一下,那么第一个想到的方法是什么?5 D- I, K, p+ m1 |/ q! r$ i/ T
当然是再找来一个临时杯子:
8 [- l3 l( d9 ~1 d  1)先把 a aa 杯子的水倒进这个临时的杯子里;6 `* I( c3 X& s* e. w+ ^
  2)再把 b bb 杯子的水倒进 a aa 杯子里;1 |: R2 H9 g0 Y: q# m, ]
  3)最后把临时杯子里的水倒进 b bb 杯子;- w/ L7 {7 x/ K4 m, M3 A" E

4 {( c" n0 r0 o8 n

5 g% K! H8 J6 P3 O: f' _0 g, d3 t+ v  X这种就是临时变量法,那么当然,还有很多很多的方法,接下来就让我们来见识一下吧。# }: H) t2 Q4 O0 S/ A
/ x3 T5 N0 Y" W0 u$ ?
) i( t. d+ e7 G/ }; U7 h& K* z
三、代码详解$ t% A8 g* W8 R
1、正确解法1:引入临时变量
+ ]3 C. [: w8 K+ |) z#include <stdio.h>
$ `" V+ A, S* m9 {4 o) nint main() {
. A: s. n: ?* z+ k& r5 y* U    int a, b, tmp;* a$ o2 E5 {3 B6 T$ l0 s$ `+ F
        while (scanf("%d %d", &a, &b) != EOF) {$ m! a$ T" `9 q2 B2 h/ e
            tmp = a;   // (1)
/ Y# m  @/ r& `7 x) h            a = b;     // (2)
( y' |" J0 Q* u. G2 ]6 U            b = tmp;   // (3)
+ y* R+ C; u' A: D" \; y3 k" d            printf("%d %d\n", a, b);1 G9 \  j) b7 j
        }
! n% F0 o* O* m6 [+ M, z( v+ n        return 0;2 B1 ]( T- Q% o, Y2 z
}& t1 x- `5 @: ]4 u
1  ~. K' C" u' C( z
25 s. Z+ l& y( e1 f
3
7 Y, u* l8 D7 D1 j; Q' c4
8 L8 G3 j6 L) w3 q7 z5
: \- Z. I6 q2 {3 l( |0 O' t! a6
2 E3 [1 Q9 N8 J6 Q+ u" @' i7
( @" e# K9 p& f% @9 u8
5 Y6 X1 H4 m9 `! H# `, x9
$ o. l/ [2 ^7 o) w107 V+ M. E9 g: z
117 I; |& B7 P5 R3 S$ a1 d
( 1 ) (1)(1) tmp = a;表示把 a aa 杯子的水倒进这个临时的杯子里;
2 ^$ t  q( R+ k; r# V& w( D( 2 ) (2)(2) a = b;表示把 b bb 杯子的水倒进 a aa 杯子里;$ Z+ z5 Y! w1 b6 i; D+ y
( 3 ) (3)(3) b = tmp;表示把临时杯子里的水倒进 b bb 杯子里;3 R1 x* r1 v. Z: n- B: s. ]
这三步,就实现了变量 a aa 和 b bb 的交换。
' L% `8 j' e% |/ W! x1 C) i2、正确解法2:引入算术运算+ d- C9 p( X  I3 M; y9 f
#include <stdio.h>
6 l: e- ~, K2 N" ~5 kint main() {
  x) F- {9 `) p. P9 A    int a, b;
1 H2 k. ~( {4 y; b        while (scanf("%d %d", &a, &b) != EOF) {5 {; J8 @  O# C' n# o3 j  D
            a = a + b;   // (1)
2 V. J9 b# I2 t  F6 ?            b = a - b;   // (2)
- M4 ^0 d* |, F' ]            a = a - b;   // (3)
8 e! V% \! d+ h            printf("%d %d\n", a, b);
/ N% J3 ^" \/ v        }# ~1 U3 V7 O/ q0 i! P# r0 X
        return 0;
) z! K3 i% k; C7 d, q}2 `3 w9 E* ~$ ^  U# R( W# J$ {5 G8 W0 ?
1" g' n' A- E! m
20 U6 A# [+ ^: t' g
3+ ]# E/ x. j% q- A" a
4
/ l# N4 l* b& e: j5
* t8 n/ I( [# `4 l. |6
8 U, L8 e8 f2 I" ?7
5 C. U# }9 u4 w4 A% L8  \3 D; Z. h1 [* _2 L
9
0 u3 E- S5 N0 R0 }7 b3 t( ?: U10$ B. X, t* u' d2 W  R* ?6 ^4 j
11% @2 ~3 H- R2 s3 C: k4 }
( 1 ) (1)(1) a = a + b;执行完毕后,现在最新的a的值变成原先的a + b的值;$ C3 E. K& [4 b- {6 U3 b
( 2 ) (2)(2) b = a - b;执行完毕后,相当于b的值变成了a + b - b,即原先a的值;
, Q1 w7 V7 l) _/ q( 3 ) (3)(3) a = a - b;执行完毕后,相当于a的值变成了a + b - a,即原先b的值;
  j# M2 E6 M' H; f4 N从而实现了变量a和b的交换。
$ M! m& _+ K! O% ~! {3、正确解法3:引入异或运算
9 U6 b: V( |* T+ k首先,介绍一下C语言中的^符号,代表的是异或。
) E* z0 f- a! F. v3 r- M* G% n二进制的异或,就是两个数转换成二进制表示后,按照位进行以下运算:
  Q) `, x- @; a. D2 T左操作数        右操作数        异或结果* B! ]+ y6 o0 ?2 h7 W# d. I
0        0        0
1 B, |7 X: N+ Q( n' h6 @1        1        0
5 f4 t* \% h. Z# r0        1        15 W6 s5 o; Y$ |- n, Y4 t
1        0        1
# Q% ?2 d4 K- S5 I* e, l也就是对于 0 和 1,相同的数异或为 0,不同的数异或为 1。
0 S! G( r( T5 ~: s) u这样就有了三个比较清晰的性质:
/ A8 E3 `# k, u) q* B1)两个相同的十进制数异或的结果一定位零。8 A6 R& ?2 n$ ^; F) w* R
2)任何一个数和 0 的异或结果一定是它本身。# v. a1 k: p5 p! j% W( J
3)异或运算满足结合律和交换律。+ E7 a( c* Y3 @* ^; M
#include <stdio.h>
7 i$ `" y" l6 j% |9 E5 {+ Hint main() {7 x; T3 A: N( D. @
    int a, b;
. X+ J& Q* @, P( L        while (scanf("%d %d", &a, &b) != EOF) {% K3 D; w0 x+ I; J$ j0 w
            a = a ^ b;   // (1)
% ^' X# I# u; o4 d. V- s3 E2 S2 t            b = a ^ b;   // (2)2 k8 V0 F6 d. I) X' a  b" Z
            a = a ^ b;   // (3); s6 ~' Y8 K, y" F, ~6 [2 s  p
            printf("%d %d\n", a, b);
( z9 E/ m8 M' n9 e        }  m: G6 h! r" D" Q
        return 0;2 v. f& t/ {1 A- Z" S
}% M0 ]  d/ K  U0 a* T& F
19 N8 E; N" \. z7 P9 |3 k
2
* I8 D8 \0 a& w" d  H3
$ x% S6 L9 T- E1 W4
* G# k1 q" j9 j, }' C# Q52 z0 Z6 n& t2 b+ w, \1 q# |$ y
6" Z% d% u7 t, z/ ?% S: {+ O: o  {
7+ H4 w# C# Y2 a. |' F4 k
82 K/ W6 c- v6 [) T
9' }! h) V# _0 f- |0 i$ p
10
( o( f3 u: Q9 W4 Z+ B& b11
7 f. x9 `  q/ s) T. _/ ^3 ^6 i- b. R我们直接来看 ( 1 ) (1)(1) 和 ( 2 ) (2)(2) 这两句话,相当于b等于a ^ b ^ b,根据异或的几个性质,我们知道,这时候的b的值已经变成原先a的值了。( X* a; |* f$ ]
而再来看最后一句话,相当于a等于a ^ b ^ a,还是根据异或的几个性质,这时候,a的值已经变成了原先b的值。
' S1 U1 \/ [9 [/ d8 t) n从而实现了变量a和b的交换。' Z/ ^* Q  Y  B( s" l

1 W! j/ a5 M% j) C' i! I& U  m
6 m* e" [' }) M4 b0 d, X
4、正确解法4:奇淫技巧
& Y. a7 M5 x0 v; H/ |" ~4 v当然,由于这个题目问的是交换变量后的输出,所以它是没办法知道我程序中是否真的进行了交换,所以可以干一些神奇的事情。比如这么写:
7 }0 f6 \( K4 S# _& B#include <stdio.h>
0 `5 k9 T2 F* E6 d$ }int main() {0 r( }  @( u# p/ R: s( l& D8 N
    int a, b;
& C( ?% ^, g' A& C. c$ a        while (scanf("%d %d", &a, &b) != EOF) {1 _" j0 T# L( T, _6 r  G" j
            printf("%d %d\n", b, a);
; {+ I" f. p7 E; q        }. [" e7 x4 C8 r2 A1 v* s
        return 0;( P8 u( ^5 O1 m* S2 j, B6 H9 V# x
}$ V  J- ~/ Y1 {/ V/ Q/ D, Q
1
6 B7 ]/ n/ k8 J* t2
$ g. B2 S( t; l  E( k! l3
+ `1 [$ I! v3 R0 j9 g4
# \" O. C; U) N# u5
! `3 u/ t/ I. o0 f7 ^6
; B9 L' F% q3 s+ q4 N% q72 y4 H' Y9 z& L6 ?& ~0 _
8
6 c  |$ S6 r# I7 |1 b你学废了吗 &#129315;?
0 e0 d$ C7 W8 g$ }2、例题2:整数溢出' X& T5 v8 |$ a
一、题目描述+ r" R6 c$ S: ~& g* _1 b' 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 ' C! B0 G3 Y+ J' R4 O
62
) U; D/ f8 ?9 l& P+ ]( [ ),输出 a + b + c + d a+b+c+da+b+c+d 的值。; P: Q. v( d* I' y( ~2 }  k( z
' g! C' u! B$ m8 h5 m
2 k$ P7 ^2 W0 X. s' d
二、解题思路" X3 B* M8 u; a' r  O$ X
难度:&#128308;&#128308;⚪⚪⚪
% {) L: ^, y6 ]9 p% u. f5 M, U) x0 x' I* f0 z- m
8 {0 ]4 g+ |; h" c7 o4 \
这个问题考察的是对补码的理解。
8 ^; H: G' a* l+ r  @; I仔细观察题目给出的四个数的范围:[ 0 , 2 62 ] [0, 2^{62}][0,2
$ r( v) ?' _" s8 ~. N6 ~62
! _3 A. B! q: z! {/ ? ],这四个数加起来的和最大值为 2 64 2^{64}2
, S) f# i0 L6 f  V$ ^9 Q* z64
  H0 {0 W7 P) c 。而C语言中,long long的最大值为:2 63 − 1 2^{63}-12 . t) ~2 ^- a) v' ?0 i/ u( n
631 J; _2 Q5 i6 b8 z* ?, _
−1,就算是unsigned long long,最大值也只有2 64 − 1 2^{64}-12 ) x. n5 h2 u+ e
64
, K( X+ y" Z4 I −1。
" n9 A( }4 }2 G2 T" y  ?但是我们发现,只有当四个数都取得最大值 2 62 2^{62}2
; \4 x$ h6 Q" U4 G. w62
) P/ Z$ h3 c: r% h  时,结果才为 2 64 2^{64}2
3 H( p1 }- }6 X+ U( m64
! B* u& x; I5 ~$ q4 v- P ,所以可以对这一种情况进行特殊判断,具体参考代码详解。
" w: p. N0 J9 ]# c三、代码详解
1 g/ Z' a6 J8 ^  w: Q' S#include <stdio.h>
% o' W3 O: o3 A6 f1 ?7 atypedef unsigned long long ull;                           // (1)
) p3 s$ m, U5 L8 r6 Z8 z: _2 fconst ull MAX = (((ull)1)<<62);                           // (2)3 O; ^3 H, [  i7 X! s

. |2 i. Q. a! f9 |( f1 c, }

3 d  i8 i$ H3 I; I' iint main() {4 C6 h; c. X, n; Z
        int t;$ O1 J9 L" {; w: V
        ull a, b, c, d;
9 q& l. ?) k! \7 j        scanf("%d", &t);1 F7 F. p. r& I2 m, {. N( d0 D
        while (t--) {
* f6 Q- z. V3 w2 h2 h2 |                scanf("%llu %llu %llu %llu", &a, &b, &c, &d);     // (3)
% n: v% Y( v* O  n6 a  j                if (a == MAX && b == MAX && c == MAX && d == MAX) // (4)
# g3 B: @8 g3 R! v                        printf("18446744073709551616\n");             // (5)
: o; a' D' h6 g5 A2 ^, i                else- W8 w7 v" a) M  K8 s2 A  s$ |
                        printf("%llu\n", a + b + c + d);              // (6)9 |2 h5 C( G% Y8 F0 O+ N
        }
. t2 O7 E- S9 F! i, G4 E        return 0;
" U3 s! V' m. H! d6 I5 Q7 E' e" g2 r}
4 O- U) Y7 d4 s1: H2 |4 E4 B- C7 a8 C' c
2
/ y+ ?5 n- S7 H* p  E30 ~$ V) p# @$ J+ O" a1 t! r" l
4) p3 I! Z) C0 t" C" a* v8 L3 U
5
& ~3 I# B5 u6 X9 \' S9 e6
1 }" ]4 C9 O$ p8 [8 v$ o. r7
! L+ C8 Y' Z. _  ]8
" R2 Z  x7 m0 B6 O6 b6 e% `9
$ s2 c5 d5 m0 ]/ H  Z10
# \7 I9 ^  f- ~$ w11
" r# B; e. ?8 p+ p. |. f5 t5 ?12. Q# ~$ E- m: o9 R0 i1 d
13
8 k( h0 G; f- V% e) z14; ^  u* v+ p, u/ y" h6 Y
15) c& r# W: Q0 N0 {$ H& u- V& i
16
, h+ W; c8 ^+ i& P5 b4 Q8 Q$ T17
6 l1 w/ @" ?0 G; \' o: i( 1 ) (1)(1) 由于这题数据量较大,所有数据都需要用64位无符号整型。ull作为unsigned long long的别名;
+ M8 Z. z# r& k( 2 ) (2)(2) 用常量MAX表示 2 62 2^{62}2
% s4 [& ^& D3 C7 V8 K2 A9 G62
0 R3 p- g3 L4 }: N8 R$ l" I3 n3 h ,这里采用左移运算符直接实现 2 22 是幂运算;
1 |7 x3 G. `! C( O/ {/ W数学        C语言
+ p/ b2 g- y9 c3 L3 z) d2 n 2^n2 1 H$ A" ]( \6 a; }, N! o: |" M1 q
n) M9 v0 M  Z- y! d$ ]; R; d
        1<<n) e/ o7 n  J* }9 b- k
需要注意的是,由于 1 是int类型,所以需要对 1 进行强制转换。(ull)1等价于(unsigned long long)1;8 O0 ?1 R( W$ ?& q, u6 U& [
( 3 ) (3)(3) %llu是无符号64位整型的输入方式;
0 y% X7 x2 H/ {6 S( 4 ) (4)(4) 这里是对所有数都等于最大值的特殊判断,&&运算符的优先级低于==,所以这里不加括号也没事;+ W0 `  P' C& l5 U
( 5 ) (5)(5) 由于 2 64 2^{64}2
  O. P; q) V% p' ?64# Q; S9 |9 g4 K7 Q
  是无法用数字的形式输出的,所以我们提前计算机算好以后,用字符串的形式进行输出;: I; R9 _9 n( m* Y
( 6 ) (6)(6) 其它情况都在 [ 0 , 2 64 − 1 ] [0, 2^{64}-1][0,2
7 ]* p& X2 o2 s2 i2 S6 ?1 g: w6 a64
4 S5 Y3 q8 p' A) t  L −1] 范围内,直接相加输出即可。' S1 l& \- h1 R$ x
由于这个专栏是付费专栏,可能对学生党不是很友好,所以作者经过再三思考,打算放出 300 张 一折优惠券, 先到先得。只要拿这个图片来找作者即可享受,仅限前 300 名。; [: x7 A; Y; I1 g5 O
为了适当提高一定门槛,你至少需要学会如何下载图片或者截图并且发送到微信里 &#129315;。
2 t- ]" _! Y. [9 A2 n! T
) C' [5 ?3 V6 S$ t

. f7 e; n1 U! H& K; L# U3、数据结构  H1 S4 u' o# M  h: A* ~
《C语言入门100例》上的例题,如果能理解前面 25 道,那基本C语言的学习就可以告一段落了,接下来就要开始我们的数据结构的学习了。- ~0 J9 d1 Z# [5 H1 v2 ?( E; n
1、什么是数据结构
1 ?$ h, L- b0 C* @9 x4 A你可能听说过 数组、链表、队列、栈、堆、二叉树、图,没错,这些都是数据结构,但是你要问我什么是数据结构,我突然就一脸懵逼了。
5 v* V7 c3 \' m如果一定要给出一个官方的解释,那么它就是:( X9 O3 E* Y+ b3 B
计算机存储、组织数据的方式。相互之间存在一种或多种特定关系的数据元素的集合。通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。往往同高效的检索算法和索引技术有关。
  s% B; g- ?% D7 ~
+ H8 c  ^/ e: j; ?$ z! x5 L

# W: y2 j# S5 R# m+ \/ \是不是还不如说它是堆,是栈,是队列呢?
$ S) B; q& w' F( C8 G. u是这样的,我们学习的过程中,跳过一些不必要的概念,能够节省我们更多的时间,从而达到更好的效果,当你还在理解数据结构是什么的时候,可能人家已经知道了栈有哪些操作了。, d- o" a4 |8 t* Z6 A4 |+ R4 C" F2 C9 }
2、数据结构和算法的关系
- ^8 R" C+ a* v+ `, q, S& m" i" V很多同学搞不明白,数据结构与算法有哪些千丝万缕的关系?甚至有些同学以为算法里本身就包含了数据结构。
7 M1 Q9 L6 ]. j8 g& M( r数据结构主要讲解数据的组织形式,比如链表,堆,栈,队列。
! I* s) F5 B6 N: J- u而算法,则注重的是思想,比如链表的元素怎么插入、删除、查找?堆的元素怎么弹出来的?栈为什么是先进后出?队列又为什么是先进先出?
# I  D1 O) v& h6 D7 r2 c: q! V讲得直白一点,数据结构是有实体的,算法是虚拟的;数据结构是物质上的,算法是精神上的。当然,物质和精神 缺一不可。& G4 {* B: k# d3 \. @; G( g6 ?
3、数据结构概览
/ m2 D& q+ f$ [+ Y2 e) ]- }周末花了一个下午整理的思维导图,数据结构:
' j8 z9 q% i2 T# U7 B  \8 @
6 g5 s' E/ o' {% v

) s  U! V8 F0 Z3 @9 M1 t& i常用的一些数据结构,各自有各自的优缺点,总结如下:3 {. o# C7 H6 y% C# o. T
a、数组, _% j* L& b5 f1 R; X
内存结构:内存空间连续- r$ @) e8 n4 d: ]7 B- k( L- h
实现难度:简单
" L9 D0 T' P, {8 `( W7 n下标访问:支持- `( H' M: e8 L5 Z6 r5 z
分类:静态数组、动态数组
* s, F9 P* ^3 g# Y插入时间复杂度:O ( n ) O(n)O(n)
! y8 ^2 G! x8 k; P: A$ m查找时间复杂度:O ( n ) O(n)O(n)0 R$ R. O' ?# y- {7 ~$ O% X
删除时间复杂度:O ( n ) O(n)O(n)
0 K1 q, d9 `% m9 m  ]$ p" f$ [) Z$ _. W1 w8 Z1 N9 `

% D! b; h& K$ J' `1 Bb、字符串7 C5 O" ~2 e; d
内存结构:内存空间连续,类似字符数组! |8 t, }( J! U. _) F) m
实现难度:简单,一般系统会提供一些方便的字符串操作函数" [- t4 S+ ?( \$ h) k' g
下标访问:支持* e% m, L1 k  D# ^* |+ d
插入时间复杂度:O ( n ) O(n)O(n)9 J4 A9 f0 n6 o# X
查找时间复杂度:O ( n ) O(n)O(n)0 q3 g) T* C" `
删除时间复杂度:O ( n ) O(n)O(n)2 S6 _# ^8 a* \0 c! u% p

4 m0 ^8 {6 e3 t' _. W0 S8 j6 N
* N! h3 C. o. R) z$ t
c、链表) I% `8 g! z1 W
内存结构:内存空间连续不连续,看具体实现
" E: D* Q! ]) Z实现难度:一般
4 j) u7 l( T' X: K2 |: Z. g下标访问:不支持
/ Y3 ^! k+ n4 f* H1 t2 e$ a: p分类:单向链表、双向链表、循环链表、DancingLinks+ }; S& b, r1 G" o
插入时间复杂度:O ( 1 ) O(1)O(1)3 f3 B! |3 u% T- J$ P  T! a# E' N
查找时间复杂度:O ( n ) O(n)O(n)7 X3 B5 F5 J  z; `* u: Y
删除时间复杂度:O ( 1 ) O(1)O(1)& S" [/ W, }/ _" |( @

  y2 }& W( d3 Y. O' O- s2 k
4 D+ J, E# p, W
d、哈希表- e: x4 F) z, m. \
内存结构:哈希表本身连续,但是衍生出来的结点逻辑上不连续
- e( Y% U1 w2 E( t实现难度:一般5 y5 Q4 D9 z4 Z+ G
下标访问:不支持
) |1 S9 H1 `: W" y  N2 f3 [分类:正数哈希、字符串哈希、滚动哈希
9 I0 O: P. ^  K插入时间复杂度:O ( 1 ) O(1)O(1)$ H. `) N& G" m. A
查找时间复杂度:O ( 1 ) O(1)O(1)
- p7 C9 J8 P/ b- N. z# B# A3 E删除时间复杂度:O ( 1 ) O(1)O(1)
- o- l9 d$ E( H% m1 q1 I2 A8 ]) p: x

' u5 H8 v3 w' z6 f! qe、队列8 B4 M0 u8 e! {2 d
内存结构:看用数组实现,还是链表实现9 z) K3 S: J. a1 z. E( }
实现难度:一般
" j1 D4 X% I3 u( P2 d. l5 h# O下标访问:不支持
) \2 M* D" X: n! x8 Q分类:FIFO、单调队列、双端队列
, L* c( `4 p% K; N+ y6 ]插入时间复杂度:O ( 1 ) O(1)O(1)
, }: V. M; k# ^+ ?0 t/ P6 a查找时间复杂度:理论上不支持
1 I7 M' s5 v2 E6 j删除时间复杂度:O ( 1 ) O(1)O(1)  S/ l6 G' }# m# ]" `3 w1 `& t* d" o
( {$ k* S. a9 G$ W. _7 |% i
. P$ I. x& w0 A3 a1 [* o# G: o2 s
f、栈
) \# e! E# a' b. \6 N内存结构:看用数组实现,还是链表实现8 q2 G  ]4 F! g. \6 d
实现难度:一般
" G% E& ?- t3 O/ {" i下标访问:不支持- o, u* a. v8 f9 h8 W
分类:FILO、单调栈5 R5 J7 J* x  g
插入时间复杂度:O ( 1 ) O(1)O(1)
* M# F4 H2 `& w6 P' W  P4 E+ e, \* l查找时间复杂度:理论上不支持
4 F, m3 }+ g4 T& B; B删除时间复杂度:O ( 1 ) O(1)O(1); n  j: r+ y3 Y( k
: s" S1 ]- x; ~) _- H+ n
6 L) }4 ~/ g" C/ E- U
g、树( {7 H, {8 r. |* ~# }/ g, R4 D
内存结构:内存结构一般不连续,但是有时候实现的时候,为了方便,一般是物理连续,逻辑不连续; m* H/ x+ Z$ F. {! k) \1 y
实现难度:较难
& i2 c: V) f2 [0 i( S下标访问:不支持
) o  K7 }4 \9 S) k7 h! Q+ {8 |" O0 X) O分类:二叉树 和 多叉树
1 a/ Q, a' u7 L: B) u5 g+ ]/ U, b# H* P- g插入时间复杂度:看情况而定
4 B' h$ h+ N7 e4 ~. m) P7 ?3 O' M+ u查找时间复杂度:理论上 O ( l o g 2 n ) O(log_2n)O(log % `4 X& g% A& n6 ^2 R
2
" k! j! J  j# G. z/ u​        6 x# T9 P( x1 w6 _3 @
n)
1 Z( Z, b5 b, z. g: N2 p删除时间复杂度:看情况而定
* r0 p$ V' B+ \: z) V) M/ g& v8 G. K7 X* x

( N' I" U3 q/ K& ?  Q; b1、二叉树  U; p$ c5 x7 y( X* Y% P/ |: V9 S1 U
二叉树的种类较多,比如:二叉搜索树、平衡树。平衡树又可以分为 AVL 树、红黑树、线段树、堆。最平衡的树莫过于满二叉树了。3 a6 L6 J# _$ }, e5 d# f
其中,堆也是一种二叉树,也就是我们常说的优先队列。
9 \! I/ d4 ]2 s* l6 h4 h) g8 e2、多叉树0 G; E  R5 W: A7 A  t
B树和B+树是多叉树,当然我们平时学到的并查集其实也是个多叉树,更加严谨一点,应该称之为森林。
2 G2 s- K6 N9 y5 P! R4 s: q) zh、图1 @( L' {9 \+ F/ b, j; ?0 U
内存结构:不一定
" F- h' [! B+ i& A; Z0 Y$ V实现难度:难# E& e' b0 I  C: t
下标访问:不支持
% I% K( y0 ?& b* i分类:有向图、无向图
2 [4 N, W- H2 ]插入时间复杂度:根据算法而定/ g; y% W4 }. q9 _8 H
查找时间复杂度:根据算法而定, _2 d+ V# ~6 g. U) h4 W
删除时间复杂度:根据算法而定. Q$ T; n# p% }+ N+ K* l
% m8 w; u6 D. _2 F  m

7 Z! A2 Z( a0 L) |6 w6 S- W" b4 i1、图的概念. |) n6 s1 d; Q
在讲解最短路问题之前,首先需要介绍一下计算机中图(图论)的概念,如下:$ y6 u3 B0 J4 o( j
图 G GG 是一个有序二元组 ( V , E ) (V,E)(V,E),其中 V VV 称为顶点集合,E EE 称为边集合,E EE 与 V VV 不相交。顶点集合的元素被称为顶点,边集合的元素被称为边。
6 N, ]% u. j% y对于无权图,边由二元组 ( 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 为权值,可以是任意类型。" d9 I! ~# g% j  H0 e
图分为有向图和无向图,对于有向图, ( 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;
+ h3 ~" J3 y  v! R# L5 y2、图的存储0 u7 o; A. B0 [8 I
对于图的存储,程序实现上也有多种方案,根据不同情况采用不同的方案。接下来以图二-3-1所表示的图为例,讲解四种存储图的方案。
: k7 e+ p- p4 L& [) E" G7 r2 `) o4 Z
4 t# j* Y1 G. o3 s$ J+ a6 I
1)邻接矩阵
" J) ^: a! D& x9 t% p" V邻接矩阵是直接利用一个二维数组对边的关系进行存储,矩阵的第 i ii 行第 j jj 列的值 表示 i → j i \to ji→j 这条边的权值;特殊的,如果不存在这条边,用一个特殊标记 ∞ \infty∞ 来表示;如果 i = j i = ji=j,则权值为 0 00。6 {, _5 I0 N( G6 k
它的优点是:实现非常简单,而且很容易理解;缺点也很明显,如果这个图是一个非常稀疏的图,图中边很少,但是点很多,就会造成非常大的内存浪费,点数过大的时候根本就无法存储。
( v! `7 Z( a1 @* b( j[ 0 ∞ 3 ∞ 1 0 2 ∞ ∞ ∞ 0 3 9 8 ∞ 0 ] \left[! a& A6 m# Y  ]* y, z9 X9 _( d
01∞9∞0∞8320∞∞∞30
/ E7 Y, N- S# u' t- z4 w0∞3∞102∞∞∞0398∞0/ E+ y8 Y/ Z8 R) }
\right]$ ?6 T5 d* s" d: I$ K  j

; U5 ^+ t/ N: j5 r0 U+ y3 g5 B) F6 z& H  h/ D# Y

6 E9 D; F2 }$ |4 g; [3 X% ]: K
4 `* Q2 P- k( J7 K: Y: _8 @​        # ~! g. P8 n- D# W% ?
  
4 D( q  C! y; Y$ H( o& r0, Z# s+ m- D& }" q$ c: q, ]- q
1
/ h$ H+ o/ `) h) t7 w1 x* b
  h. R/ x5 l2 c3 t& |97 R& P# o/ I/ A8 {+ C# j2 I- B( e0 X
​        " D% i" m% n. Z0 {
  
. h" z% [) n$ Q2 t5 k3 X
. T3 f# Q( Z* R7 d. l0
' k7 V( T. a' L  v" ~4 e$ \( [6 H  w' x
0 ?( c3 H+ Q* Q) V# {0 O3 Y: y84 u' |% b9 R# n/ n
​       
1 G8 r  G" O! O# v# T  % _7 T$ e1 \+ S6 R4 \' L
3
9 e8 L5 E, c# t6 O" S* S2
8 t# p% Z! @) n' E( R& J0
7 e! ?' a' d! g0 g" C; B
$ s: `. J2 q9 ~& |8 H​       
. C( v9 `" m- r& L4 }  
7 u+ M) B  X& L+ v- H" U9 p8 z3 U2 @' F+ u
9 C3 G1 }/ b3 t, y
39 v* @/ q" B4 Q$ p" I
0( o2 y( ^/ X/ g4 m0 c5 B+ |
​       
8 R4 i+ T; W" K4 y, I* k4 \; ^" j  
* y: s; r3 _) `
+ Z8 x/ I6 N$ H% a3 m) y0 T
4 g! R& d/ p- E
# M' v: b0 e9 u" I' s9 K7 S5 g- S5 B  s, B8 \) F6 A; c) a
​        0 J( I1 {. |& t! M

3 p; g* }  E8 A2 W+ m5 H2)邻接表
) K7 i7 L6 \- b' f1 [邻接表是图中常用的存储结构之一,采用链表来存储,每个顶点都有一个链表,链表的数据表示和当前顶点直接相邻的顶点的数据( v , w ) (v, w)(v,w),即 顶点 和 边权。
. F3 A& P4 Y; x  ]5 n/ E它的优点是:对于稀疏图不会有数据浪费;缺点就是实现相对邻接矩阵来说较麻烦,需要自己实现链表,动态分配内存。
/ q' A$ w+ E  |) G% U; Z; H如图所示,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) 二元组。
# f+ Z2 u0 L7 i, z
3 L  Y$ n$ g& V/ `4 N0 f, I4 x5 n; g
2 X/ }. q) ^: `0 X
在 C++ 中,还可以使用 vector 这个容器来代替链表的功能;( \* D4 Q- G) o
    vector<Edge> edges[maxn];& T# Y# p" V7 f) x
11 d$ d9 }& ^0 \# `- y
3)前向星) C: x6 o9 a$ S" B
前向星是以存储边的方式来存储图,先将边读入并存储在连续的数组中,然后按照边的起点进行排序,这样数组中起点相等的边就能够在数组中进行连续访问了。  L, `4 z9 _* ^' u+ v$ t
它的优点是实现简单,容易理解;缺点是需要在所有边都读入完毕的情况下对所有边进行一次排序,带来了时间开销,实用性也较差,只适合离线算法。) F, q- u$ o6 U1 O
如图所示,表示的是三元组 ( u , v , w ) (u, v, w)(u,v,w) 的数组,i d x idxidx 代表数组下标。/ y8 t8 x% U, J9 s7 H, C
/ z' i  Y8 [) u$ U

' T, x, M8 y' s& S' d# n那么用哪种数据结构才能满足所有图的需求呢?7 X: x* a& u* f" z
接下来介绍一种新的数据结构 —— 链式前向星。
5 o5 {! ^" L, d4)链式前向星
/ N+ A  R, T6 i( X/ [3 W  c链式前向星和邻接表类似,也是链式结构和数组结构的结合,每个结点 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 指向下一条边。. ^% y8 t7 J9 H, s4 [* Q
具体的,我们需要一个边的结构体数组 edge[maxm],maxm表示边的总数,所有边都存储在这个结构体数组中,并且用head来指向 i ii 结点的第一条边。0 N( [# R5 G* J8 l- D# n0 I3 t( q! G
边的结构体声明如下:* q8 T" R, O/ Y# _% f6 s* Z
struct Edge {
/ v, D7 l( x" J$ g* _  u& d$ K    int u, v, w, next;' a4 C4 s8 G+ X! ]
    Edge() {}( O. F. R2 C' x6 \
    Edge(int _u, int _v, int _w, int _next) :) p7 F2 u4 ?) k. I8 y3 z
        u(_u), v(_v), w(_w), next(_next)
4 S# L1 k$ c* F+ g, G- \- v  t    {1 j5 [' @3 p/ `2 X
    }
. L% l5 D& v  f1 r0 R}edge[maxm];
+ q0 q0 F# X! }$ D% W& e' X1
+ Y6 }  p1 ]) F2
1 E* u$ P  y; p. t3
2 J6 o- e: R! T, s5 A4
8 X4 ~' ^$ W7 _9 F5
; A6 ?6 D- r- _8 x6: k3 Q5 Y8 I* K9 b5 K5 s; i
7
8 G/ u: S3 ^: [, @8
7 j4 |9 {7 w+ @  T! }6 F' ~初始化所有的head = -1,当前边总数 edgeCount = 0;
- a5 ]- c; x$ h& Z每读入一条 u → v u \to vu→v 的边,调用 addEdge(u, v, w),具体函数的实现如下:1 {0 A7 O5 j: f- Y! s
void addEdge(int u, int v, int w) {( ]  H: P( x* Y3 n+ L$ W$ ]
    edge[edgeCount] = Edge(u, v, w, head);
4 H3 o( p; l/ Z& k    head = edgeCount++;$ e9 P3 r5 z) ]* O' ?+ i. T
}- G) |; x* y& l
1
7 S9 L# K& h5 v' m! ~! C2% Q% s9 X1 B  K0 W; Z) Q! u, L
3
! V' B, ~% Q! j5 g- a" ?  o0 B+ s4
; K, F" w3 V4 ~8 c8 D% K6 h这个函数的含义是每加入一条边 ( u , v , w ) (u, v, w)(u,v,w),就在原有的链表结构的首部插入这条边,使得每次插入的时间复杂度为 O ( 1 ) O(1)O(1),所以链表的边的顺序和读入顺序正好是逆序的。这种结构在无论是稠密的还是稀疏的图上都有非常好的表现,空间上没有浪费,时间上也是最小开销。& |7 J" i$ |  X" |2 _8 r. i
调用的时候只要通过head就能访问到由 i ii 出发的第一条边的编号,通过编号到edge数组进行索引可以得到边的具体信息,然后根据这条边的next域可以得到第二条边的编号,以此类推,直到 next域为 -1 为止。
' h3 P* h8 T) a1 yfor (int e = head; ~e; e = edges[e].next) {- e! C" S, R- T, q' P0 x8 t
    int v = edges[e].v;
* |- s' [9 \* v1 R    ValueType w = edges[e].w;
# ^! @( F4 v$ y" G0 L    ...4 D$ N0 k: x4 N# t+ G1 h3 d0 c
}! U/ ~4 h0 F9 e( ^
1
* ]9 w, J/ `4 K8 A4 v28 ^1 ]7 v5 v  T! s$ `4 ?2 y! ~
3
1 X2 }0 f# `: l2 g; C4
% l/ P' Z8 H4 I3 Z3 _# X* \7 ^5
: S' G3 f0 ^+ c# O% c& x( S2 X文中的 ~e等价于 e != -1,是对e进行二进制取反的操作(-1 的的补码二进制全是 1,取反后变成全 0,这样就使得条件不满足跳出循环)。
/ B1 j, M% F6 ^0 C  y9 }, [/ M4、算法入门% I! t1 a9 a; k( W4 [8 N
算法入门,其实就是要开始我们的刷题之旅了。先给出思维导图,然后一一介绍入门十大算法。( M4 }+ R4 |2 n- i1 F
% O9 |' y" D) m, P! N: ]. I- |0 b

/ O3 _; D# {. x; N; x( E2 b% m入门十大算法是 枚举、排序、模拟、二分、双指针、差分法、位运算、贪心、迭代、分治。
% B3 U' T8 k/ L8 _2 t: C. U对于这十大算法,我会逐步更新道这个专栏里面:《LeetCode算法全集》。. j* ~: E' w$ g9 H
1、枚举5 H" \! o7 g; e6 @
枚举可以简单理解成for循环,从一个数组中遍历查找一个值,就是枚举;从一个数组中找到一个最大值,就是枚举;求数组所有数的和,也是枚举。
* j6 i( Y5 c9 a& C对于枚举而言,基本就是循环语句的语法学会,这个算法就算学会了。
7 L$ a: {! ~$ r- j% @2、排序
8 z. j1 k1 a" R既然是入门,千万不要去看快排、希尔排序这种冷门排序。- S2 t6 s, M( y9 M! ~
冒泡排序、选择排序、简单插入排序 原理好懂,先看懂再说,其他不管。因为这三者都是基于枚举的。' O4 t6 P' o8 b+ \* N6 P; D% y
C中有现成qsort排序函数,C++中有现成 sort排序函数,直接拿来用,等算法进阶时再回头来看快速排序的算法实现。* \" ~+ _3 c" F
3、模拟
& I) D5 l" G: i) |2 l6 J0 E模拟就是要求做什么,你就做什么,完全不要去考虑效率问题。
7 s/ r& D9 y; m! z7 |  e不管时间复杂度 和 空间复杂度,放手去做!: U! ~* x, d) R" v! l9 @* }
但是,有时候模拟题需要一些复杂的数据结构,所以模拟题难起来也可以很男,难上加难。+ o/ d* C6 g7 ^: k
4、二分1 Z% D9 m0 z' k
二分一般指二分查找,当然有时候也指代二分枚举。$ Q) I, D6 n. T9 y* U: i
例如,在一个有序数组中查找值,我们一般这个干:
, g: o$ g1 a' }; f0 D1)令初始情况下,数组下标从 0 开始,且数组长度为 n nn,则定义一个区间,它的左端点是 l = 0 l=0l=0,右端点是 r = n − 1 r = n-1r=n−1;8 ^, t% [4 C/ c/ n
2)生成一个区间中点 m i d = ( l + r ) / 2 mid = (l + r) / 2mid=(l+r)/2,并且判断 m i d midmid 对应的数组元素和给定的目标值的大小关系,主要有三种:
2 X$ H& B1 w5 W' D* x  s  2.a)目标值 等于 数组元素,直接返回 m i d midmid;
/ V& M: U  U! L7 u+ V  2.b)目标值 大于 数组元素,则代表目标值应该出现在区间 [ m i d + 1 , r ] [mid+1, r][mid+1,r],迭代左区间端点:l = m i d + 1 l = mid + 1l=mid+1;$ Y4 M* H  M# `9 ^1 d0 J% Q' I
  2.c)目标值 小于 数组元素,则代表目标值应该出现在区间 [ l , m i d − 1 ] [l, mid-1][l,mid−1],迭代右区间端点:r = m i d − 1 r = mid - 1r=mid−1;- S' ~- n" D  M, I- G  f
3)如果这时候 l > r l > rl>r,则说明没有找到目标值,返回 − 1 -1−1;否则,回到 2)继续迭代。/ R2 B8 m7 @" v8 w5 P/ n. A
5、双指针: G% R. ?' g, j4 v6 A
双指针,主要是利用两个下标在一个数组上,根据问题的单调性,进行指针偏移,由于每个指针只往后偏移,所以时间复杂度可以达到 O ( n ) O(n)O(n),由于思想非常简单,所以出题时,热度不低。0 o3 T' A& l7 s4 k& q

( n9 V  S: ]* {$ a: C" O" i0 J

0 S) b8 _/ `; \, L7 S7 {3 x6、差分法0 `* {3 O, U2 r4 \4 f. S$ N- F/ t" t
差分法一般配合前缀和。
! |% L2 u. V" ~/ s4 d对于区间 [ l , r ] [l, r][l,r] 内求满足数量的数,可以利用差分法分解问题;
) A# [" R6 i* R% j假设 [ 0 , x ] [0, x][0,x] 内的 g o o d   n u m b e r good \ numbergood number 数量为 g x g_xg
) v9 B4 V# O6 a2 o1 Xx
5 ?7 z$ D; N# O8 }7 G​       
/ s6 B! Z1 i2 e: u6 Z( r ,那么区间 [ l , r ] [l, r][l,r] 内的数量就是 g r − g l − 1 g_r - g_{l-1}g 1 I) G8 q, Q4 D' X1 E+ W! ^
r
: A' U: }; w9 r9 r6 W  C​       
8 b5 }/ T& ~/ u1 ~. o% W0 t0 u( `+ Y −g , v, M/ o6 g5 Y8 Q# d1 q9 w& j
l−1
+ n- E- a) h  J+ @& r* |0 F​        + O" A$ p. K2 F( q
;分别用同样的方法求出 g r g_rg
! Y% b2 O) ?' G' P. y/ N3 h" Gr/ q' B, r+ b0 c1 ?  O5 ~5 g
​        9 O/ J8 r2 ^. C% q4 t
  和 g l − 1 g_{l-1}g
& O+ J" H5 P. Y. p, [3 V; ml−1" O: E( \( v9 N. |) x7 R1 i
​        - [9 R- q2 V4 P" V0 |
,再相减即可;! ]; ]1 u6 s: Y: Q* e0 J9 `3 _

7 K7 l( E6 T/ N4 l0 w

) f9 a" W4 Q2 p" Z/ A% D, d7、位运算
" _7 S% h) T1 ~" `6 p位运算可以理解成对二进制数字上的每一个位进行操作的运算。
; B( \3 O# A+ i位运算分为 布尔位运算符 和 移位位运算符。  M5 f- X( b- e4 `; h
布尔位运算符又分为 位与(&)、位或(|)、异或(^)、按位取反(~);移位位运算符分为 左移(<<) 和 右移(>>)。* _! _5 ?7 b- G
如图所示:" \0 Y2 @* {5 A0 X

! j" g& k. q$ G0 A9 Q4 @0 u% Q( x

- y5 K+ Z1 r6 e! Q4 `' u位运算的特点是语句短,但是可以干大事!
9 t3 s  p: N- o) K" o, c& N7 u. Q* l比如,请用一句话来判断一个数是否是2的幂,代码如下:5 H; A4 Z8 d) C9 k2 _7 v6 x, ]
!(x & (x - 1))
! d) B- ?0 j6 S1
9 R5 O0 g  u) U6 ^& Z& n/ `2 o8、贪心7 H; Y' k. S1 V; I/ u
贪心,一般就是按照当前最优解,去推算全局最优解。
4 a- C' t! s+ ?5 r所以,只有当当前最优解和全局最优解一致时才能用贪心算法。贪心算法的证明是比较难的,但是一些简单的贪心问题会比较直观,很容易看出来这个能够这么贪。
6 E  ]9 D+ c" t6 q3 P9、迭代
% v0 _. m5 c0 J* o每一次对过程的重复称为一次“迭代”,而每一次迭代得到的结果会作为下一次迭代的初始值,周而复始,直到问题全部解决。4 [5 q5 [+ k4 g; ], }
10、分治
) w, {' C& s; `( W9 E& r" [3 [分治,就是把问题分成若干子问题求解,子问题解决后,问题就解决了。一般利用递归实现。属于初学者比较头疼的内容。递归一开始学习的时候,一定要注意全局变量和局部变量的关系。& W  Z. ^; m, E& o1 f* L* ~
5、算法进阶7 c7 a8 S1 P3 {$ W" D/ q2 e
算法进阶这块是我打算规划自己未来十年去完成的一个项目,囊括了 大学生ACM程序设计竞赛、高中生的OI竞赛、LeetCode 职场面试算法 的算法全集,也就是之前网络上比较有名的 《夜深人静写算法》 系列,这可以说是我自己对自己的一个要求和目标吧。
; t9 \- f+ N4 A% C如果只是想进大厂,那么 算法入门 已经足够了,不需要再来看算法进阶了,当然如果对算法有浓厚兴趣,也欢迎和我一起打卡。由于内容较难,工作也比较忙,所以学的也比较慢,一周基本也只能更新一篇。
. _& @3 a5 `5 j7 G, ?) X4 p) H& ~这个系列主要分为以下几个大块内容:' ^6 n% j+ w0 n% ~7 k4 h* j- Q
  1)图论
4 m( K1 n! N; y+ {/ _* w' G  2)动态规划
/ d. [$ {8 D! F$ D+ s  3)计算几何6 _% x0 w" m1 G/ T8 K( [
  4)数论
: R. A" ^. L/ ]' A+ C! O0 }- M6 d  5)字符串匹配
* V  b* X0 Z" A2 f/ [- I  6)高级数据结构(课本上学不到的)
  p% ]7 `( [( T  7)杂项算法
) A% h0 P: t3 {  r' }/ j% x
0 H3 ?9 H% R! h% V, a$ f

  B, v4 V6 y/ Q. ~3 e! _3 m' p先来看下思维导图,然后我大致讲一下每一类算法各自的特点,以及学习方式:
6 G# M( S4 z; r1 ]+ ]2 X# T6 n: I/ Y! t6 P* A% p) O- G

; c+ g$ H0 x$ z1 }% C6 _
3 w3 x# j' G- S& ]2 \+ W$ X
) B  H( J2 Q/ ^0 L. W
1)图论
4 Y- u1 U% b$ k" E" N, |! _' _1、搜索概览( {3 c4 ~( z% k/ e
图论主要围绕搜索算法进行展开。搜索算法的原理就是枚举。利用计算机的高性能,给出人类制定好的规则,枚举出所有可行的情况,找到可行解或者最优解。' g6 g# A+ B0 D) M
( R3 i, ?4 o7 o: Y" \
; I7 z9 o( D; y. x& s; `
比较常见的搜索算法是 深度优先搜索(又叫深度优先遍历) 和 广度优先搜索(又叫广度优先遍历 或者 宽度优先遍历)。各种图论的算法基本都是依靠这两者进行展开的。  u) W6 T; n( B# [2 p- E
2、深度优先搜索6 p2 A/ R1 `& b9 K. s
深度优先搜索一般用来求可行解,利用剪枝进行优化,在树形结构的图上用处较多;而广度优先搜索一般用来求最优解,配合哈希表进行状态空间的标记,从而避免重复状态的计算;
) n3 a2 D! n$ F! _原则上,天下万物皆可搜,只是时间已惘然。搜索会有大量的重复状态出现,这里的状态和动态规划的状态是同一个概念,所以有时候很难分清到底是用搜索还是动态规划。
1 F9 z7 G! g: G/ b7 ?: O0 \- u但是,大体上还是有迹可循的,如果这个状态不能映射到数组被缓存下来,那么大概率就是需要用搜索来求解的。; k1 S7 N' i8 w! [
如图所示,代表的是一个深度优先搜索的例子,红色实箭头表示搜索路径,蓝色虚箭头表示回溯路径。
7 _" G6 v( V% y4 R' d! i
2 S$ q- y; |) e; @) O$ S" B( }
- j# r$ a$ q3 k
红色块表示往下搜索,蓝色块表示往上回溯,遍历序列为:
# i+ G6 J4 N+ r! _& y; O6 l5 x/ J# _        0 -> 1 -> 3 -> 4 -> 5 -> 2 -> 6! W0 O) f, G% f
1) f! N% {2 N, e- s. v, |
同样,搜索的例子还有:, k+ P2 j. \: p# @. [8 r8 p

6 e! B/ W7 R2 g
5 K) r4 h9 ~+ N% w0 _! N% V
计算的是利用递归实现的 n nn 的阶乘。4 v) s- P+ Q: U* X. R) W
3、记忆化搜索
# ]( n1 C9 o- E9 B  J' s/ m/ S/ o% V对于斐波那契函数的求解,如下所示:
. W8 }& }' \4 k7 F: sf ( n ) = { 1 ( n = 0 ) 1 ( n = 1 ) f ( n − 1 ) + f ( n − 2 ) ( n > 2 ) f(n) =/ h/ M5 ^! L+ W! B" K+ P; c& a) C
⎧⎩⎨11f(n−1)+f(n−2)(n=0)(n=1)(n>2)
* }& X, j5 w7 v{1(n=0)1(n=1)f(n−1)+f(n−2)(n>2)( y$ Z7 D+ K! x6 _5 t# K
f(n)= 7 U7 Z" E, Q2 J" I
+ t9 _6 ^& d6 u: b( n

, p+ ^4 m, B' P6 Z8 l: R4 e! O
8 t3 V! f3 h0 i; V! K; J# @4 ]. x5 i0 |: L# ~, ?
9 }+ _- _- G% v' }/ z: B$ C6 _
​       
# A- @5 K) b$ [* U5 }. O; d" w3 B0 K: b  * A7 F3 K: N0 G* o( c
11 m# ~4 O  {% a
1' H5 p' l9 Q3 [3 T! a" u" R
f(n−1)+f(n−2)
7 F9 R2 E- C) h. a7 ?1 L​       
4 K4 c6 w6 E$ q4 y8 R3 h# x  
& \  o& A' K; [(n=0)+ t3 f- c; R% K3 f* u
(n=1)
/ o! N; y6 i! t9 Y(n>2)( A% ]9 B0 [: x* J
​        8 M9 D5 H; k% q
, @. s3 [! a9 R: L+ l
对于 f ( 5 ) f(5)f(5) 的求解,程序调用如下:( K7 J% ?9 `. F4 p* t4 f
" W8 P+ V1 s/ T, Y+ Z
' ]* r+ M' }  B( j* _% Z# X. Y
这个过程用到了很多重复状态的搜索,我们需要将它优化,一般将一些状态缓存起来。
: K2 m3 E, V9 i4 A) r1 m  C: R, J我们通过一个动图来感受一下:
' ~9 t) q: |" E& e" b5 q& S' }0 T) n7 L- G/ `
9 i1 j4 ?" H4 u4 c! p
当第二次需要计算 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表达式为真,直接返回,不再需要往下递归计算,这样就把原本的 “递归二叉树” 转换成了 “递归链”, 从而将原本指数级的算法变成了多项式级别。5 l8 O- Y. Z5 x: [: M( Z- q$ Z
这就是记忆化搜索,像这种把状态缓存起来的方法,就是动态规划的思想了。& r0 s# `/ T6 t8 C3 Y
4、广度优先搜索
; }5 D* d: J5 J3 h单向广搜就是最简化情况下的广度优先搜索(Breadth First Search),以下简称为广搜。游戏开发过程中用到的比较广泛的 A* 寻路,就是广搜的加强版。9 i# P  ]" _# p
我们通过一个动图来对广搜有一个初步的印象。9 _# @2 i- ^  L% e+ h  y( D
: o, N9 L6 R  [' @
1 F! N5 h% y# J1 [( d
5 v9 ?$ t! S4 I. |( r

. B, x7 Q. t5 M+ |从图中可以看出,广搜的本质还是暴力枚举。即对于每个当前位置,枚举四个相邻可以行走的方向进行不断尝试,直到找到目的地。有点像洪水爆发,从一个源头开始逐渐蔓延开来,直到所有可达的区域都被洪水灌溉,所以我们也把这种算法称为 FloodFill。+ z$ r: a/ @0 v# B
那么,如何把它描述成程序的语言呢?这里需要用到一种数据结构 —— 队列。1 f6 x; I# [" ?+ E
这时候,算法和数据结构就完美结合了。
, M9 k: S) K2 W0 d! K2)动态规划
+ D; d' t4 w# `7 w8 C9 T/ \, B动态规划算法三要素:: }/ y! x9 _; [' K$ P0 F
  ①所有不同的子问题组成的表;8 a, M4 z4 ?* i2 j3 {
  ②解决问题的依赖关系可以看成是一个图;
8 T& J  j) U2 K3 I2 r+ Y  ③填充子问题的顺序(即对②的图进行拓扑排序,填充的过程称为状态转移);
, w' L; ~! m5 m5 f8 w5 K6 W( ]4 X! ]5 t3 i5 ^2 {7 m" H

" q6 z& T2 `$ s0 V如果子问题的数目为 O ( n t ) O(n^t)O(n
. T9 W+ z- S4 O- N$ X$ gt1 Q" s+ E% i; m6 @% |+ N
),每个子问题需要用到 O ( n e ) O(n^e)O(n
: J1 `2 r/ w: r. D1 I( l6 e0 K* ee
) L3 K8 ~; `8 G! V$ c ) 个子问题的结果,那么我们称它为 tD/eD 的问题,于是可以总结出四类常用的动态规划方程:(下面会把opt作为取最优值的函数(一般取 m i n minmin 或 m a x maxmax ), w ( j , i ) w(j, i)w(j,i)为一个实函数,其它变量都可以在常数时间计算出来)。
! }2 b0 {) C3 e8 ]1 W; A1、1D/1D% l: A( _1 T2 I& C/ g3 Y
d [ i ] = o p t ( d [ j ] + w ( j , i ) ∣ 0 < = i < j ) d = opt( d[j] + w(j, i) | 0 <= i < j )$ z3 M9 k) ~2 @- f2 i0 O  \1 g# }
d=opt(d[j]+w(j,i)∣0<=i<j)3 W, k2 M, U  Z0 L( T
状态转移如图四所示(黄色块代表d [ i ] dd,绿色块代表d [ j ] d[j]d[j]):
7 f  K) u; G+ t6 t& m4 {
1 q+ w# x! y2 `- s5 N, U: A

9 ^+ P5 d! A! R% N& L  G这类状态转移方程一般出现在线性模型中。8 U1 }1 |4 H7 @) R, E% z) t$ t
2、2D/0D& i- B4 f% k  A  t' q$ w! 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} )" W* V  {2 ]% `  J
d[j]=opt(d[i−1][j]+x
( ^" m3 F- ]( J3 [i
* m3 [( w1 P+ M- m- x/ ?​       
# Y1 @# a: S+ n ,d[j−1]+y
/ N: x4 D5 {% e# H! s4 jj
$ I& q5 x; L& ^. |/ u​       
6 ~. v, P6 p& h* q; N ,d[i−1][j−1]+z 4 L8 q- g' B$ N  b
ij
! j! w% z( W; i$ `" j% i​       
/ j1 z: i1 M5 Y# w) g9 K' t- L) B ): F6 ]2 r' P( d8 [) S
状态转移如图四所示:* A$ c* V" ]+ L, G
1 N: Q; W$ g3 V; A; H) {1 m
9 z8 f4 w* @( B9 X$ H5 \
比较经典的问题是最长公共子序列、最小编辑距离。
; ~" m) _( J& U' N: ~  D. U; i有关最长公共子序列的问题,可以参考以下文章:夜深人静写算法(二十一)- 最长公共子序列! W" [" k6 W3 f1 ^3 d
有关最小编辑距离的问题,可以参考以下文章:夜深人静写算法(二十二)- 最小编辑距离
2 `' T5 g3 d* k5 e5 \3、2D/1D
* q3 z( Z4 ]9 [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] )1 s6 c" \4 b1 s7 s7 o
d[j]=w(i,j)+opt(d[k−1]+d[k][j])4 I; l' q. u0 }
区间模型常用方程,如图所示:
2 P. y: m$ A2 C4 m; D+ }. z3 w/ V' X6 ?# ^2 g0 m0 O  `/ h
, U' W* L, `+ P8 ~! q* w: e( B
另外一种常用的 2D/1D 的方程为:
9 z: N+ u, T1 S/ [7 W, R- 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 )
: s: @$ G5 o! n; u8 N* od[j]=opt(d[i−1][k]+w(i,j,k)∣k<j)" }4 e2 w, q1 ~+ D( [
区间模型的详细内容可以参考以下这篇文章:夜深人静写算法(二十七)- 区间DP) [& e% J5 c: P2 m; M
4、2D/2D; S7 D. c8 {, A
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)
* ~0 P9 c0 O5 t/ T' A7 E9 y+ ud[j]=opt(d[i
$ D. z8 R$ l: l4 e5 S$ G* f' i: o3 U4 m: i) |+ l
][j
3 ?/ q/ w! f$ _( a
$ X  l) k. N# J# O( R0 c- W ]+w(i
$ n. s7 {% w% R5 D: i
  p3 ]' O$ N4 J ,j
1 o% @7 n& R  `5 B1 B1 m6 T9 |; x" F. D
,i,j)∣0<=i
. k& B6 Z# k2 J9 k
! \( s) t( v! J7 n <i,0<=j 2 M' V8 F  H$ e# |

  |% M' E5 V1 P <j)
7 ]% u+ I. r: u# y0 i如图所示:% C4 G# \7 v6 A2 Y# d2 e0 S

9 `7 K: {0 Q5 P- H- c- G* y% Z  w# m% s
+ p4 K5 g& Q( j8 D3 _4 o2 ~
常见于二维的迷宫问题,由于复杂度比较大,所以一般配合数据结构优化,如线段树、树状数组等。- a4 Z6 d" i+ Y# Y, P$ s
对于一个tD/eD 的动态规划问题,在不经过任何优化的情况下,可以粗略得到一个时间复杂度是O ( n t + e ) O(n^ {t+e})O(n
* n1 w& V0 f, M; I: T7 Ft+e- h2 e( t5 ^/ O- @3 r* Q* m
),空间复杂度是O ( n t ) O(n^t)O(n
6 J& {8 X: j3 ?. f, m( p4 ut, o, `) S# O# O1 g* I8 b  D* z' Q
) 的算法,大多数情况下空间复杂度是很容易优化的,难点在于时间复杂度,后续章节将详细讲解各种情况下的动态规划优化算法。
  G% a. H# N2 d3)计算几何
' E: S: E' S4 y计算几何的问题是代码量最大的。它是计算机科学的一个分支,以往的解析几何,是用代数的方法,建立坐标系去解决问题,但是很多时候需要付出一些代价,比如精度误差,而计算几何更多的是从几何角度,用向量的方法来尽量减少精度误差,例如:将除法转化为乘法、避免三角函数等近似运算 等等。( f( Q+ t2 j2 f0 v) K
如果一个比赛中,有一道计算几何的题,那么至少,它不会是一道水题。7 W& w( B& o; f& a: w! e9 X
1、double 代替 float
' U& L6 Q3 b! ]2 K9 o' g, n( |c++ 中 double 的精度高于 float,对精度要求较高的问题,务必采用 double;
; w+ P/ b5 o; ^' Z2、浮点数判定% k* B0 n( E$ `
由于浮点数(小数)中是有无理数的,即无限不循环小数,也就是小数点后的位数是无限的,在计算机存储的时候不可能全部存下来,一定是近似的存储的,所以浮点数一定是存在精度误差的(实际上,就算是有理数,也是存在误差的,这和计算机存储机制有关,这里不再展开,有兴趣可以参见我博客的文章:C++ 浮点数精度判定);5 {) |! H% r" X& h
两个浮点数是否相等,可以采用两数相减的绝对值小于某个精度来实现:0 a/ Q" o. a- b4 D, B( d8 w* t
const double eps = 1e-8;9 D- Y. V) C8 c6 K( s( }
bool EQ(double a, double b) {1 G0 i, q; t5 n9 z+ K
    return fabs(a - b) < eps;) |( `- X1 L1 f0 n
}. ~6 L0 S5 p2 w* x: a5 a
10 x1 k; i8 A7 L% t
2
2 M2 V3 \" Q0 A% a) ~3
: k0 Q7 h0 V: ^, l3 J4
1 x% G8 v. `. [2 v7 S* A0 \并且可以用一个三值函数来确定某个数是零、大于零还是小于零:
4 B8 N. k* `2 Y* Iint threeValue(double d) {
& y& l, A3 m" Y- ^* u    if (fabs(d) < eps)3 Z, U) l2 k( Q$ v
        return 0;8 v8 |; \! ?% ^$ j7 O
    return d > 0 ? 1 : -1;; l& O! X& _) V
}# e1 G) Y' Q- n, q4 Z) J
16 J( E- E$ O7 H0 P& G$ I  G
2
4 m1 q$ ~& F8 h0 q. E9 i34 c" Y& g  h2 S' X( ^5 ~
4& K% J. d8 ~9 j! O
5% B: |  q9 e0 Y0 r$ _& w; h
3、负零判定
: Q$ a5 J+ R. H! \: J2 M因为精度误差的存在,所以在输出的时候一定要注意,避免输出 -0.00:
7 i* f8 |0 B- V1 {9 e1 i    double v = -0.0000000001;
) b" Z. U9 ~8 [    printf("%.2lf\n", v);) t3 Q9 p5 D* m* d3 p4 g# a
18 O* B5 ~5 ?* |( K# x
2
0 K  W. l* G6 r: E6 g2 Q. U避免方法是先通过三值函数确定实际值是否为0,如果是0,则需要取完绝对值后再输出:
' C. {! p9 `  c6 P0 n0 s    double v = -0.0000000001;
" x# X4 d4 n! o! b) b% i* x- m    if(threeValue(v) == 0) {5 Y2 H) z& u7 W* W4 N$ f
        v = fabs(v);
$ H" k$ l5 m& q: w. z5 Y' L% I    }
+ x/ G+ C6 ?3 Q9 U! e    printf("%.2lf\n", v);
6 l+ `* Y! A5 |* `17 J' X2 P/ W# ~& h' W
2
) T. Y7 ^4 g, @6 H) y' t38 e8 Z/ E0 S: u! J6 E0 F* R
4; i% c2 p, N. Q& H$ E* J: d- b  l
5( T: w- C* D- Z8 n/ q; q: V
4、避免三角函数、对数、开方、除法等
- b% K0 a/ G+ L/ H1 S$ pc++ 三角函数运算方法采用的是 CORDIC算法,一种利用迭代的方式进行求解的算法,其中还用到了开方运算,所以实际的算力消耗还是很大的,在实际求解问题的过程中,能够避免不用就尽量不用。& N4 U5 c8 ?9 k1 r+ k& }
除法运算会带来精度误差,所以能够转换成乘法的也尽量转换为乘法运算。
1 e0 [6 [- o  R" W/ w9 ?5、系统性的学习, r' J8 B  z5 o0 c% N+ b2 {
基础知识:点、向量、叉乘、点乘、旋转、线段、线段判交、三角形面积;2 A/ j! z; }* ^0 q4 o
进阶知识:多边形面积、凸多边形判定、点在多边形内判定;* \5 r: L% X  @/ e
相关算法:二维凸包、三维凸包、旋转卡壳、多边形面积交、多边形面积并、多边形面积异或、多边形和圆的面积交、半平面交、最小覆盖圆、最小包围球、模拟退火。
- s1 g/ X' y* o
2 k* o, G1 d5 ]
# `) \- [- ~8 W( l
学习计算几何,最好是系统性的,刷题的过程中不断提炼出自己的模板。
9 w' c# @+ f. m4 `6 X' w9 z4)数论8 @- `1 [/ o5 y4 D
刷题的时候遇到不会的数论题,真的是很揪心,从头学起吧,内容实在是太多了,每个知识点都要证明吃透,不然下次遇到还是不会;不学吧,又不甘心,就是单纯的想把这个题过了,真是进退两难!: N% Z, F. K* Z2 k- X
数论对一个人的数学思维要求较高,但是一般也是一些固定的模式,所以把模板整理出来很重要。
6 x9 ^& m5 B" j2 [5 t当然,数论也有简单问题,一般先做一些入门题提升信心。
1 ~' A5 F7 T- t8 ]. o9 m+ n  R1、数论入门4 w" {% V' x; E5 J4 O* P0 T9 v
主要是一些基本概念,诸如:3 l$ X2 W  r1 u. r0 h
整除性、素数与合数、素数判定、素数筛选法、因数分解、算术基本定理、因子个数、因子和、最大公约数 (GCD) 和 最小公倍数 (LCM)、辗转相除、同余、模运算、快速幂取模、循环节;
; v0 y' j- l# _1 j4 h2、数论四大定理/ K5 J, p8 p* X" g5 i* [
这四个定理学完,可以KO很多题:7 g3 Y/ z) k2 d7 d
欧拉定理、中国剩余定理、费马小定理、威尔逊定理
1 p) Z- v' O: s$ B+ ]# I3、数论进阶; x- D! Q) F7 t! `* g( p6 `. q0 v0 P
系统性的学习,基本也就这些内容了:
" W5 L" H2 N% k扩展欧几里得、逆元、欧拉函数、同余方程组、扩展欧拉定理、RSA、卢卡斯定理、整数分块、狄利克雷卷积、莫比乌斯反演、大数判素、大数因子分解、大步小步离散对数等等。
5 v& e2 y$ S7 P+ @5)字符串匹配
. U; ?, e( Q2 j0 P1 `* }4 P字符串匹配学习路线比较明确。6 j' _% V2 Y( x7 d) i: f+ `
先学习前缀匹配:字典树。
8 t- \+ X  ]8 |2 M  E5 F6 z2 X然后可以简单看一下回文串判定算法:Manacher。
0 E( p7 M9 e, Z& Q% T5 a4 p以及经典的单字符串匹配算法:KMP。/ l  K  g$ l" p2 n4 X2 K6 J
实际上平时最常用的还是 BM 算法,而ACM中基本不考察。
# x: @' S2 g* u  V然后就是较为高阶的 前缀自动机、后缀数组、后缀树、后缀自动机了。2 d1 P, b8 Z% R9 p
关于 算法学习路线 的内容到这里就结束了。
7 w% e0 f2 `# T+ z0 e# I  N如果还有不懂的问题,可以 想方设法 找到作者的微信进行在线咨询。' h0 {( S8 l# s# w# |8 g
参考资料4 q7 T& \- I$ S
【阶段一】C语言学习资料:《光天化日学C语言》(日更)! e7 C3 h& O# G4 e* B2 f6 h- t
【阶段二】C语言例题:《C语言入门100例》(日更)- W! [: E! c3 D' k, I/ w# j  }( s% ~
【阶段三】算法入门题集:《LeetCode算法全集》(日更)
2 e  Z/ ~6 N; j) v; E) }【阶段四】算法进阶:《夜深人静写算法》(周更)
% s. b' a- @; O7 H————————————————' j4 L: X7 e6 x; g8 j; {# c# h! \
版权声明:本文为CSDN博主「英雄哪里出来」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
/ ^& o+ [! P; m1 Q# r: d$ I- s! M原文链接:https://blog.csdn.net/WhereIsHeroFrom/article/details/118382228
( M: ?; s  d6 A2 `. b9 j& D+ a5 g4 Z- q: x: i! `
. t0 f9 P7 S# E- p( z' V& f& z* Y( Y+ G

作者: 1051373629    时间: 2021-8-17 17:14
太难得了2 y6 ~" h1 K  V% P! ?- T





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