数学建模社区-数学中国

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

作者: 杨利霞    时间: 2021-7-8 15:06
标题: ❤️两万字《算法 + 数据结构》全套路线❤️(建议收藏)
9 v8 C2 F0 t, B
❤️两万字《算法 + 数据结构》全套路线❤️(建议收藏)" A) a$ H  z& k! `& l7 _

) R0 ?) v* O* L7 ?- e5 k* d' \3 P前言$ b: ]8 H4 w5 t: G4 n5 b% n$ t1 o
  所谓活到老,学到老,虽然我感觉自己已经学了很多算法了,但是昨天熬夜整理完以后发现,自己还是个弟弟,实在忍不住了,打算把 算法学习路线 发出来,我把整个算法学习的阶段总结成了五个步骤,分别为: 基础语法学习(重要)、语法配套练习、数据结构、算法入门、算法进阶。本文梳理了这五个大项的思维导图,在下文会有详细介绍。
7 C% h# f  p5 P  希望各位能够找到自己的定位,通过自己的努力在算法这条路上越走越远。0 i( l5 p! E7 R
  刚开始切勿心浮气躁,千万不要给自己立 flag,说一定要把这么多东西都学会。就算你的精力旺盛,日夜操劳,时间也是有限的。所以,首先是明确我们要做什么,然后制定好一个合理的 目标 ,再一点一点将要学习的内容逐步付诸实践才是最重要的。4 m7 N% `$ T2 v  C' q
  每日一篇C语言打卡,目前更新到:光天化日学C语言(20)- 赋值运算符与赋值表达式 | 让代码变得更加简介(建议收藏)。
6 A9 O( R/ H8 `5 M5 r9 k5 T
( Q3 L+ x4 Z" V

  ~% i! I5 I, }% z3 C$ T9 n- l6 ]- m
$ d9 s7 g9 b- b& K
8 V; Y3 A& V( s

. K, l& t' x/ b  ~! T" d  \! N
+ _2 P' K* v/ q+ a- U" w9 k
7 Q( b6 W+ w- M6 L; u4 F+ _  r
图片较大,文章中有拆解,需要原图可以留言找我要哈
( F5 O6 [. Y5 f1 P7 g$ W8 U2 G2 I1、基础语法学习
" c" K9 F8 z4 N0 z9 B4 R算法是以编程语言为基础的,所以选择一门编程语言来学习是必须的。9 i$ m2 u. ~7 W) X8 a# F2 |* t
因为作者本身是C/C++技术栈的,所以就拿C语言来举例子吧。如果是 Java、Python 技术栈,可以跳过 C语言相关的内容。这一小节,先给出学习路线图,然后我再来讲,每部分应该如何去学。
3 |: x% ~3 v# ^3 }) E( |$ B$ C
) {3 p9 L6 g- @. M6 {8 Y

4 I7 b9 b7 `/ C- W3 z3 e) T# g: S' \: i! U2 ~* N" B
# m; o9 m. U# P. `, d+ ~& t
1)HelloWorld
. s7 u/ ~$ x5 d) `" T2 u无论是 Java、Python、C/C++,想要上手一门语言,第一步一定是 HelloWorld,先不要急着去配环境。如果环境配了几个小时,可能一开始的雄心壮志就被配环境的过程消磨殆尽,更加不要谈日后的丰功伟业了。
0 H4 H- t$ H# E: V/ M$ o1 E8 H2)让自己产生兴趣
" `) e8 @& K+ o5 c所以,我们需要让这件事情从一开始就变得 有趣,这样才能坚持下去。比如找一个相对较为有趣的教程,这里我会推荐这个:《光天化日学C语言》。听名字就比较搞笑,可能作者本身也不是什么正经人,哈哈哈!虽然不能作为一个严谨的教程去学,起码可以对搞笑的内容先产生兴趣。从而对于语言本身有学习下去的动力。+ q$ E# a  x, F+ I( f6 x- I% U/ M1 y
刚才提到的这个系列,可以先收藏起来。回头再去看,它讲述的是 对白式 的 C语言教学,从最简单的输出 HelloWorld 这个字符串开始讲起,逐渐让读者产生对C语言的兴趣。这个系列的作者是前 WorldFinal 退役选手,一直致力于 将困难的问题讲明白 。我看了他的大部分教程,基本都能一遍看懂。算了,不装了,摊牌了,因为我就是这个作者。
0 A. {- W/ R6 M" P; u9 V3)目录是精髓; f$ B* W9 |9 Z: d/ Z# G
然后,我们大致看下你选择的教程的前几个章节,那些标题是否有你认知以外的名词出现,比如以这个思维导图为例,前几个章节为:
" x. d+ F* r0 `: i' a- W1、第一个C语言程序
; |* [. Y& E3 E9 j; w" M; l/ I1 P2、搭建本地环境8 s( i, ?/ ]7 o2 l# C! Z
3、变量
6 D5 _% ~0 G& ]0 f% U: Z4、标准输出0 z$ D. ~" v# a) g
5、标准输入5 Y( H" \' s( z1 }4 d' v
6、进制转换入门
" w8 m+ [, m5 V$ Y7、ASCII字符
* W$ h( V- c& _; Q2 S8、常量) S4 ?6 c' q/ R5 e7 }! F
# M2 P& k8 P! h9 `4 Z8 w
- t8 e) n7 n0 O9 S2 o" S) s
如果你觉得这些名词中有 3 / 4 以上是没有什么概念的。那么,可能需要补齐一些数学、计算机方面的基础知识。反之,我们就可以继续下一步了。$ p: {% _# L7 w& U+ [7 z
4)习惯思考并爱上它
1 V+ w5 p+ A9 s  ^6 U只要对一件事情养成习惯以后,你就会发现,再难的事情,都只是一点一点积累的过程。重要的是,每天学习的过程一定要吃透,养成主动思考的好习惯。因为,越到后面肯定是越难的,如果前期不养成习惯,后面很可能心有余而力不足。0 [0 g' R1 [9 h- r5 t0 z# ^7 @
就像刷题,一旦不会做就去找解题报告,最后就养成了看解题报告才会做题的习惯。当然这也是一种习惯,只不过不是一种好习惯罢了。1 w% f$ r7 h$ c" f' T
5)实践是检验真理的唯一标准7 S6 s) k+ D; `' S% S/ ~  k
光看教程肯定是不行的,写代码肯定还是要动手的,因为有些语法你看一遍,必定忘记。但是写了几遍,永世难忘。这或许就是写代码的魅力所在吧。: K; D* i) u+ \9 \
所以,记得多写代码实践哟 (^U^)ノ~YO
, j$ h* s9 R: ?% c  J& Z+ W; o8 M6)坚持其实并没有那么难$ ?2 f5 C) P9 d, _# f. l
每天把教程上的内容,自己在键盘上敲一遍,坚持一天,两天,三天。你会发现,第四天就变成了习惯。所以坚持就是今天做了这件事情,明天继续做。
' i2 `" {& }9 ]7)适当给予正反馈: F2 X& n% r7 Q0 O) w
然而,就算再有趣的教程,看多了都会乏味,这是人性决定的,你我都逃不了。能够让你坚持下去的只有你自己,这时候,适当给予自己一些正反馈就显得尤为重要。比如,可以用一张表格将自己的学习计划记录下来,然后每天都去分析一下自己的数据。
* K) J/ ]( ]) U" ?' L; T当然,你也可以和我一样,创建一个博客,然后每天更新博文,就算没有内容,也坚持日更,久而久之,你会发现,下笔如有神,键盘任我行!更新的内容,可以是自己的学习笔记,心路历程 等等。
' B1 \- j: G* H: F- Q4 j1 `看着每天的粉丝量呈指数级增长,这是全网对你的认可,应该没有什么会是比这个更好的正反馈了。
  ]- l0 z  [. z( f- }* M( j8)学习需要有仪式感" g6 @3 `5 f5 @( x
那么,至此,不知道屏幕前的你感想如何,反正正在打字的我已经激情澎湃了。已经全然忘记这一章是要讲C语言基础的了!
; {3 |/ w3 m6 W( e介于篇幅,我会把C语言基础的内容,放在这个专栏 《光天化日学C语言》 里面去讲,一天更新一篇,对啊,既然说了要坚持,要养成习惯,我当然也要做到啦~如果你学到了哪一章,可以在评论区评论 “打卡” ,也算是一种全网见证嘛!9 E$ T( D# z( h; ]! s0 _
我也很希望大家的学习速度能够超越我的更新速度。
0 c: J! a  p  f: x2、语法配套练习
5 y; G$ b4 v* ^5 q0 S4 r, h% W% l学习的过程中,做题当然也是免不了的,还是应征那句话:实践是检验真理的唯一标准。
) @" J+ q2 d6 j; T0 h$ P. ^而这里的题库,是我花了大量时间,搜罗了网上各大C语言教程里的例题,总结出来的思维导图,可以先大致看一眼:0 Z9 Z1 x; h3 \+ o- |% G3 x: l2 V
5 ~! {) h4 O6 w: O5 P0 g, t

! K3 S3 H; j' S2 [$ Z( [7 w5 X) l4 T; R

% ~, b9 t- ]9 w& U2 [7 \9 [$ S从数学基础、输入输出、数据类型、循环、数组、指针、函数、位运算、结构体、排序 等几个方面,总结出的具有概括性的例题 100 道 《C语言入门100例》,目前还在更新中。
( W- o" f, D( z/ l' k% `9 b) Y0 Q! [这里可以列举几个例子:
5 B% w  ]5 c+ Z1、例题1:交换变量的值
: F4 B2 j7 d! b% B& f2 i一、题目描述% T5 D' H$ U0 g5 @0 }
  循环输入,每输入两个数 a aa 和 b bb,交换两者的值后输出 a aa 和 b bb。当没有任何输入时,结束程序。
; ^5 G. F! Y+ i0 c! [5 l% e
3 D) m0 u* Z5 r5 J

8 h; E: P1 U3 Y0 j" ~* U9 |
( e- z" m& D  N5 Q
' @- E. u5 i5 D/ ]2 \
二、解题思路( U5 X8 Z6 l2 s3 Q# W
难度:🔴⚪⚪⚪⚪. X  e7 i0 m8 S# ~) Z

3 [1 A+ \- b" {* x3 j

# N9 h4 m6 D, D6 c这个题的核心是考察如何交换两个变量的值,不像 python,我们可以直接写出下面这样的代码就实现了变量的交换。5 q8 \4 ^8 j8 c3 S( ^5 U0 d$ {
a, b = b, a( ?* s- B3 v9 y; }7 J# B2 ?. f) U
1, o, q, Z9 g' J1 {5 h$ i
在C语言里,这个语法是错误的。
/ ^, S9 I1 N/ t" ~6 s* ]$ y我们可以这么理解,你有两个杯子 a aa 和 b bb,两个杯子里都盛满了水,现在想把两个杯子里的水交换一下,那么第一个想到的方法是什么?
  F9 Z, i5 _- K8 {/ p- |0 ]& Y& `当然是再找来一个临时杯子:7 n/ G0 r% z  t0 {( F& a
  1)先把 a aa 杯子的水倒进这个临时的杯子里;
) s3 W- v" d# [4 L! B  2)再把 b bb 杯子的水倒进 a aa 杯子里;- S& |0 A7 ?1 L# @4 j+ O0 u
  3)最后把临时杯子里的水倒进 b bb 杯子;
( J  F9 o, b- ]/ R* C' M
' o+ P' C4 U+ {

8 q0 C4 y8 l2 Q这种就是临时变量法,那么当然,还有很多很多的方法,接下来就让我们来见识一下吧。
' x/ p0 _3 T! J  r  s# _- i9 z5 b2 u
5 _! f1 @6 B# k! J
三、代码详解+ X) N0 Y+ E( w& k/ S+ i4 N
1、正确解法1:引入临时变量! M1 F# m' x* [& [- Q9 a
#include <stdio.h>& j& b( H: H  I4 N7 i' i$ |2 |8 h9 }
int main() {
- u& l! ~! q/ b" y# t1 v    int a, b, tmp;3 E% T: j- h* ~1 M# S
        while (scanf("%d %d", &a, &b) != EOF) {2 p+ W7 \# @% J! k/ o) r' Z& D4 G5 b
            tmp = a;   // (1)
3 _9 K4 L. U: t7 F1 T            a = b;     // (2)" g8 t5 ?! j8 L) x9 _
            b = tmp;   // (3)
$ U1 Y! s' O1 I6 R2 I! L            printf("%d %d\n", a, b);
5 I$ M3 M1 F8 ^) p8 @        }
- [8 C) B3 h9 t# x3 B6 P0 {        return 0;
) [" s5 I3 p8 P+ k# i& n) z3 f3 [}
$ r% q- @+ T. n- p; }* w4 y7 A1
; I, X- D4 R4 n) @2+ k+ t& _+ J$ {! C3 _
33 E" `# I. w( K* |$ O4 I
4
7 [. ?' O2 j: q- p50 m' s# p* r. d5 ~1 Q0 M
6& C1 J# v. P# g% `$ j
7
! d0 U- Z7 M2 M! S- M85 ^6 y2 |$ w/ ]9 `; H2 E% F
9
$ e5 P, }2 z5 `0 ~/ r10
3 R2 h* ]+ w) @$ c, ?0 D+ Y/ R11
! p: Q" n# h- C# @( 1 ) (1)(1) tmp = a;表示把 a aa 杯子的水倒进这个临时的杯子里;; t5 Q' i2 R3 a' ]
( 2 ) (2)(2) a = b;表示把 b bb 杯子的水倒进 a aa 杯子里;; n4 }! B* h) C) R1 I
( 3 ) (3)(3) b = tmp;表示把临时杯子里的水倒进 b bb 杯子里;
& Q$ ?" g" k7 z这三步,就实现了变量 a aa 和 b bb 的交换。# i  o+ Z' V" a2 i( s
2、正确解法2:引入算术运算
' z" C! ?3 ^. \. a4 T6 `. U* C#include <stdio.h>
1 A0 a7 L5 B% T" U! z4 ~int main() {
1 a% v4 \6 Q# c; A& ^- o    int a, b;" ^$ l& M1 ^& g, u
        while (scanf("%d %d", &a, &b) != EOF) {; ^: i9 d% @0 [+ A6 r
            a = a + b;   // (1)
, t9 ]6 }3 j* {5 _2 I# x  o8 I6 S4 h            b = a - b;   // (2)3 ^( p( _0 y: ?4 f0 h& X
            a = a - b;   // (3)# N! g+ d% ^8 x$ o5 c
            printf("%d %d\n", a, b);
  Y+ i" S" [4 b1 L) a        }+ L% [" y. `) ?5 J
        return 0;" M+ h1 J: U6 g0 k' P
}
+ u7 D& o  \- m# @6 i7 u0 g8 O11 c4 w. A* P5 d
2, {+ M2 n9 v- X2 ?1 U4 t; c
3
4 G4 q& k7 D' s2 p5 M40 H) N; D% T0 B9 c4 o% V$ n: R
5
7 c7 l" |9 B; z1 T8 p& Z, n6
9 h; Z* P6 G) Z, s7' Y3 `: D8 P, _$ ^* E2 D* s5 v( f$ \
8
8 u' F4 d  X7 _, E4 x$ ~- _" R5 N9
( V7 d, Z- R8 s' y2 w6 N10' X- w2 J5 n: ~$ E6 n6 [
11; ~' h+ z" W3 H+ Y3 d  c
( 1 ) (1)(1) a = a + b;执行完毕后,现在最新的a的值变成原先的a + b的值;( F6 m) s( }8 g; {8 J
( 2 ) (2)(2) b = a - b;执行完毕后,相当于b的值变成了a + b - b,即原先a的值;+ ~* v: S8 Z4 h7 ]6 A8 F3 U
( 3 ) (3)(3) a = a - b;执行完毕后,相当于a的值变成了a + b - a,即原先b的值;
9 W& K- r9 Q) z1 c从而实现了变量a和b的交换。2 Y2 t. E* L2 w% D4 l1 R
3、正确解法3:引入异或运算6 e. S5 n0 g, d& N# x+ W* \* ^0 v
首先,介绍一下C语言中的^符号,代表的是异或。( \$ G5 w$ C) d+ a7 ^
二进制的异或,就是两个数转换成二进制表示后,按照位进行以下运算:
( |& G# l1 e  C左操作数        右操作数        异或结果
: }5 a3 I. v& u' d& Y0        0        0& M6 o8 u2 H: `+ Z0 A1 S  u0 I
1        1        04 u7 c0 c$ r) g9 _# c1 K8 f* ~
0        1        1$ J8 J$ m+ ]. F$ ^; e
1        0        16 P- F  J$ R' E. A' ?3 f5 V) v
也就是对于 0 和 1,相同的数异或为 0,不同的数异或为 1。' A7 g7 E2 r: F& q- D/ U6 N& h
这样就有了三个比较清晰的性质:2 n0 b, t2 Q: v, ?; H9 I% b
1)两个相同的十进制数异或的结果一定位零。
6 g+ }7 A. X  [5 A2)任何一个数和 0 的异或结果一定是它本身。
8 F' ?. _2 }/ b+ K3)异或运算满足结合律和交换律。
9 y9 M5 J& v3 }/ g#include <stdio.h>: b; {! e' _4 A( ?- `4 X/ y
int main() {
+ d) X# D" M; S8 t7 `8 n8 `    int a, b;- u  e  o( b, W; o4 J2 ?$ j
        while (scanf("%d %d", &a, &b) != EOF) {' Q& R$ R$ `, l! P; ?
            a = a ^ b;   // (1)
- S; \4 M* N) E8 ]$ L+ w            b = a ^ b;   // (2)
/ l9 a! h0 e/ S. j            a = a ^ b;   // (3)/ r& o) Y& r0 @/ O" ?9 D% W6 z
            printf("%d %d\n", a, b);9 G8 f3 d5 ]& j& \; f( B# S- ~: @0 l
        }
( C3 M& H# E, r8 N        return 0;
/ j' c* A8 }; V1 ]) d}$ X& e, @, m; ?# [& s' I& C: `
1
" I* B7 K$ }3 ?( ?/ K2
) V/ {* V; s/ L& \- Q3; O9 C1 J# s! i
4
* {" {! B0 ~' I8 V5 W- }4 z5
8 `/ i5 F/ h# y9 x, D) K4 Y, j6
& N% N- o$ ~! |. ^0 [7
+ [/ u# `; ^% w. E, I8( b1 |4 Y! [. e3 g/ r' }2 d: l
9
6 Q( p# I& ^+ d3 b, a6 T: ~+ N, t3 ]10% m0 d( J5 v+ [! a5 \
11
: h/ o$ G5 n) p" d: M& M7 I我们直接来看 ( 1 ) (1)(1) 和 ( 2 ) (2)(2) 这两句话,相当于b等于a ^ b ^ b,根据异或的几个性质,我们知道,这时候的b的值已经变成原先a的值了。, t! B! P! `/ }; u5 C! B
而再来看最后一句话,相当于a等于a ^ b ^ a,还是根据异或的几个性质,这时候,a的值已经变成了原先b的值。; o0 o  _8 o+ n: d( E8 _% ?
从而实现了变量a和b的交换。; N( S2 Y: [# a2 G" \) x0 @
+ s+ G/ O* M" N0 \
, p" n4 ^% |) }/ i
4、正确解法4:奇淫技巧
9 |4 y% E4 q2 D( D当然,由于这个题目问的是交换变量后的输出,所以它是没办法知道我程序中是否真的进行了交换,所以可以干一些神奇的事情。比如这么写:  \9 W- D4 z2 @7 e; h
#include <stdio.h>
" V! w, k9 X: n* o; N  T: Oint main() {# V0 k3 T, h1 m1 g/ o$ a
    int a, b;% v6 S$ P0 v4 @: p- s
        while (scanf("%d %d", &a, &b) != EOF) {1 @) B  c' K7 h  e0 U
            printf("%d %d\n", b, a);
$ Z! ^) H' i1 ]' x6 T7 m. z/ u        }- M5 {2 d; M5 a8 f: |
        return 0;
6 a  \% [8 N1 [' F8 V$ ~}- |" y+ m& e: f8 C! u  w4 K
1
& D# N/ ~3 r+ [: a7 @2& ~, @9 ^  r9 r3 ^6 ]6 a, i6 N
36 k+ Y$ @) {- L7 c
4
; g# y' _; A# W  h0 k0 T58 x7 b/ c. P& ~3 D
6
, F/ U+ ^0 T& v  j7
& V! {3 u) o5 Q) s3 g  E  o9 p8
. @; L3 @) l( _* f- Y你学废了吗 &#129315;?5 y3 v; U6 B: a- o9 ^
2、例题2:整数溢出# m" j+ f9 t8 W) p% ^: L
一、题目描述9 g/ s+ [) E: M1 j7 L1 j- `7 y& D
  先输入一个 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 7 F3 a( P; S. W/ j; W$ w5 V
62
2 z, d7 j& I! s5 e- W# w% e ),输出 a + b + c + d a+b+c+da+b+c+d 的值。4 ^# v$ ~( [1 |

5 `4 z4 p2 ^* f) D3 x( {  E( A

8 P+ k6 I) t) @二、解题思路
5 }3 Q9 t- d1 u& z& u' c( x& k) N难度:&#128308;&#128308;⚪⚪⚪+ w6 `: G& \( R; ]$ g

* T7 A( s! |7 a! d. ]! J0 y
* j; T! D. {  x% ?7 `( X- O) @
这个问题考察的是对补码的理解。; ~: K9 |7 F+ u2 @  ?1 R0 ?0 M
仔细观察题目给出的四个数的范围:[ 0 , 2 62 ] [0, 2^{62}][0,2
3 J% o: K' h7 M+ J8 j& d2 b62
0 `' B  U+ U  _( z ],这四个数加起来的和最大值为 2 64 2^{64}2
$ j6 ]2 _) G: |9 k: j646 B' N/ U+ l" X/ x
。而C语言中,long long的最大值为:2 63 − 1 2^{63}-12 , [/ |% T5 I: O' t: b! e1 b  H
639 [# v' [  f* b
−1,就算是unsigned long long,最大值也只有2 64 − 1 2^{64}-12
3 C. v2 l4 h2 W8 [0 ]64# o& M. I2 c" F  {; w3 `" N  i$ ?' B' W
−1。
3 |4 v0 o$ Q% Q, f1 P6 Y/ ?但是我们发现,只有当四个数都取得最大值 2 62 2^{62}2
( C7 L8 Z, {" r1 K, ]62
" `0 h% f' e! s7 s  时,结果才为 2 64 2^{64}2 ; n' \+ x0 t+ x2 ]" u* k
64! E) G- j. I- o( X1 e# \  s
,所以可以对这一种情况进行特殊判断,具体参考代码详解。
% \1 [/ I0 b0 r5 `! I三、代码详解
. P! }4 e( ~* ^5 ]5 v#include <stdio.h>' L, E8 Y, I7 N$ `
typedef unsigned long long ull;                           // (1)- _. Z2 N0 ?& ~' t# s% f/ @+ x+ J
const ull MAX = (((ull)1)<<62);                           // (2)
. K/ x. F' ~, o6 [$ a1 Q
7 B# D: Q4 {5 ?5 o0 o, W. G# y* b

5 z' e% V( [4 O& g0 |, Y" E" ]int main() {
! j2 _- A+ d; x; V( E' b        int t;
7 D( [! _  Q- J2 n        ull a, b, c, d;
6 _$ P& @0 r% O8 ?- n3 R        scanf("%d", &t);) x, o' S+ K- ]) O$ \& y! E1 G# P
        while (t--) {
" ]- f! `7 L% ^                scanf("%llu %llu %llu %llu", &a, &b, &c, &d);     // (3)0 \) k9 W% _8 _% K
                if (a == MAX && b == MAX && c == MAX && d == MAX) // (4)
& `- {+ H: S, n# k                        printf("18446744073709551616\n");             // (5)3 A! a0 S! N4 T) _$ o1 r
                else, V: z2 D- ]7 v3 {/ i6 e' ]( ^& H
                        printf("%llu\n", a + b + c + d);              // (6)
# F$ z8 A1 b0 x$ g6 F        }; ]0 Z2 k+ \) x: e6 J+ w" O
        return 0;7 M7 ?% b3 V+ W
}
( Z1 t2 D8 ]1 {2 @: @7 G1' c0 v# N1 x! l& P
2/ J' ]' Y' n7 ]
36 ~9 e+ P" f" D% b" Z  n& H. _& Z
4
- R0 r# \& N8 y5
+ n7 F, ]3 O: p; y. e( s  m! O6
) d6 X% K; B/ u  x, r7
0 u# a( t1 ?+ l1 Z0 F+ w8 B/ H' o8
9 \# i! E$ d/ H( [7 r99 _8 P9 S  C: G% F1 e3 Q, D
109 d5 z8 ]* _9 D/ ?
117 a3 h& [9 r- [" n( N
12
3 t2 d% M. h" t. Q3 d132 C$ r( X( m5 y% o' G
140 s$ e" b8 P. z- H# p3 r5 C& v4 B8 c
15
, ^' e  N& @% k% D3 o/ z. R5 v+ Q16
1 i: s$ R" m8 B3 Q" d. _2 [+ Q17. R. M1 a9 v+ h8 |7 u
( 1 ) (1)(1) 由于这题数据量较大,所有数据都需要用64位无符号整型。ull作为unsigned long long的别名;$ {2 a) N3 f' {& Q# t0 W
( 2 ) (2)(2) 用常量MAX表示 2 62 2^{62}2 ; j6 Z3 U: {9 ^+ h; W- }1 [' X& e" k
62# ?& I2 A, P4 g6 u
,这里采用左移运算符直接实现 2 22 是幂运算;, a8 ~% d  w" i  N
数学        C语言& Q- b. l  y4 F# k
2 n 2^n2 " ?  {% O( n/ K; E& n
n* S5 p' q( a+ b7 W" J$ l/ }
        1<<n. G; h  Q* b0 v2 V# N- e6 W
需要注意的是,由于 1 是int类型,所以需要对 1 进行强制转换。(ull)1等价于(unsigned long long)1;
/ B" x* W9 U) F0 d* S( 3 ) (3)(3) %llu是无符号64位整型的输入方式;
" z. C  F. e$ j( i; s0 W( 4 ) (4)(4) 这里是对所有数都等于最大值的特殊判断,&&运算符的优先级低于==,所以这里不加括号也没事;
8 s3 g- c3 f7 J1 [7 w  i( 5 ) (5)(5) 由于 2 64 2^{64}2 6 i; V/ O* m, V' ?
648 \, |2 u( J: w, r& T& L  H  f
  是无法用数字的形式输出的,所以我们提前计算机算好以后,用字符串的形式进行输出;
6 i- ]5 T6 q( v' G( 6 ) (6)(6) 其它情况都在 [ 0 , 2 64 − 1 ] [0, 2^{64}-1][0,2
5 h: U3 n" u; k( f  v  ]+ E% R645 b( ?. l3 K0 a! B3 p$ j1 `
−1] 范围内,直接相加输出即可。, X3 G) o+ w3 q2 t$ ^
由于这个专栏是付费专栏,可能对学生党不是很友好,所以作者经过再三思考,打算放出 300 张 一折优惠券, 先到先得。只要拿这个图片来找作者即可享受,仅限前 300 名。! u- P& n3 Y0 u; l1 U: x
为了适当提高一定门槛,你至少需要学会如何下载图片或者截图并且发送到微信里 &#129315;。. i  ?/ g( ^" `4 u; v$ K3 A( a0 k

3 b5 d9 C- t; u' B

; j+ T6 i) u; N3 V7 g3、数据结构
3 q: l7 F5 j; i! m2 @. z9 p: V《C语言入门100例》上的例题,如果能理解前面 25 道,那基本C语言的学习就可以告一段落了,接下来就要开始我们的数据结构的学习了。
7 u, F) @: K3 X% S# Q- W1、什么是数据结构0 F! J. ^9 p9 U3 b/ _
你可能听说过 数组、链表、队列、栈、堆、二叉树、图,没错,这些都是数据结构,但是你要问我什么是数据结构,我突然就一脸懵逼了。
$ O5 ~8 s) k# X/ v, M4 c  I8 h% [1 P/ e如果一定要给出一个官方的解释,那么它就是:% [: {3 `- h/ v/ z2 h1 [: A
计算机存储、组织数据的方式。相互之间存在一种或多种特定关系的数据元素的集合。通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。往往同高效的检索算法和索引技术有关。
7 C4 P6 Y; B$ K' H3 C# y7 f: E& @( e# L9 B& V0 m* Z* N" Q/ k! N
# I0 h# }/ F* s2 e" H+ c, e2 |
是不是还不如说它是堆,是栈,是队列呢?/ ^, U: M$ O) z8 ?+ ?4 u- f1 ?& M
是这样的,我们学习的过程中,跳过一些不必要的概念,能够节省我们更多的时间,从而达到更好的效果,当你还在理解数据结构是什么的时候,可能人家已经知道了栈有哪些操作了。
0 U1 v4 a5 N2 W: ]) a- l' U0 C* z2、数据结构和算法的关系) y/ T) P6 Q! T. j% l1 Y
很多同学搞不明白,数据结构与算法有哪些千丝万缕的关系?甚至有些同学以为算法里本身就包含了数据结构。
# e% N/ D! D# q" m$ R/ K& ~; [, Y* x数据结构主要讲解数据的组织形式,比如链表,堆,栈,队列。
( t' r- r0 F3 B3 g% c$ Y, d而算法,则注重的是思想,比如链表的元素怎么插入、删除、查找?堆的元素怎么弹出来的?栈为什么是先进后出?队列又为什么是先进先出?  x. W( o: D/ s
讲得直白一点,数据结构是有实体的,算法是虚拟的;数据结构是物质上的,算法是精神上的。当然,物质和精神 缺一不可。" m5 o; B  v" F4 F* k
3、数据结构概览" P3 a6 R; |* x* N# f& a
周末花了一个下午整理的思维导图,数据结构:) h: `8 S/ C# q0 H7 d0 o# H2 g
" a% z$ f! g- r8 L  u8 |
) f5 ^  M$ u* ?3 T! P6 j, p
常用的一些数据结构,各自有各自的优缺点,总结如下:
! x6 ^3 q2 N: ~) g. _6 D: o$ {a、数组
8 u0 I: q! U! F2 B! O  C$ r内存结构:内存空间连续
0 Q: L6 |! x8 n; d6 a实现难度:简单- n1 Y5 s6 z2 |* F7 I! E4 j( g
下标访问:支持. Y/ p: h! A/ c7 o! i
分类:静态数组、动态数组
% B0 S. o& h6 Y3 I" o0 U' K插入时间复杂度:O ( n ) O(n)O(n)# K1 g. v. {6 e3 o, ?5 J: ~
查找时间复杂度:O ( n ) O(n)O(n)
5 `. I) {) k( f1 O2 b/ R  g  k" p, Q删除时间复杂度:O ( n ) O(n)O(n)" {: s# F, c3 R$ v# k! |& d3 @
1 F, k' L8 c3 ^% ~! f0 g

3 P* A9 i! D/ j" C' T. B% F( sb、字符串% Y, S, q  q' O& R
内存结构:内存空间连续,类似字符数组' L" o$ l: K9 l6 S3 u3 z- h" a
实现难度:简单,一般系统会提供一些方便的字符串操作函数' M' }9 N& t2 m
下标访问:支持7 C" z4 h( d$ j6 Z' \8 \+ z, F
插入时间复杂度:O ( n ) O(n)O(n)
& A( |* H5 \+ H9 s查找时间复杂度:O ( n ) O(n)O(n)
7 E. @6 a5 f5 {7 L5 V! y6 h( u! k3 J删除时间复杂度:O ( n ) O(n)O(n)$ m# x9 q. i. F
$ p/ V$ f* T; g

% K4 x, M0 `8 c/ T5 Nc、链表
6 c  U. I1 y' @0 W3 ~/ Y内存结构:内存空间连续不连续,看具体实现- \5 @2 A, \' ]$ S
实现难度:一般3 w/ I9 w" c) n2 E
下标访问:不支持
) o8 d) j( y$ ^# @" ^1 ]# k分类:单向链表、双向链表、循环链表、DancingLinks" p2 D" J$ ]1 y
插入时间复杂度:O ( 1 ) O(1)O(1)
1 ^8 r+ o& |5 j" D+ v9 ]查找时间复杂度:O ( n ) O(n)O(n)
. C% k3 ?6 g* A9 r删除时间复杂度:O ( 1 ) O(1)O(1)6 h% t8 E. x2 @! Y: j1 X/ R

+ W# G& {+ v8 K! j( F4 F
- L7 B1 \# d: S6 H: d8 m
d、哈希表: T+ h! o' V9 P$ t( K; K
内存结构:哈希表本身连续,但是衍生出来的结点逻辑上不连续: k, O6 e+ }& u7 Z$ ^
实现难度:一般8 u* a3 b. y+ _3 u$ {
下标访问:不支持
3 J; q: R% ~1 T9 }5 Y, [0 [7 o2 ?/ W; p分类:正数哈希、字符串哈希、滚动哈希# J4 J1 x) p8 S. V  W) _
插入时间复杂度:O ( 1 ) O(1)O(1)
) Q% a# U8 H/ o2 |4 r9 h查找时间复杂度:O ( 1 ) O(1)O(1). x8 K# J# u( S* H
删除时间复杂度:O ( 1 ) O(1)O(1)
' H% v* |" j8 T# @5 z
& U2 o7 _- V7 K2 z. j

6 b* G! J" e' q/ D2 a* ~e、队列3 A5 |& k: C3 |1 ^4 g; U
内存结构:看用数组实现,还是链表实现! P) y" x, H5 r) V& R7 Y
实现难度:一般
0 g5 a# w! ~2 E/ V% p" g7 d/ O下标访问:不支持
% _2 B( v, N! [' _6 R+ v+ ~分类:FIFO、单调队列、双端队列
5 n$ W6 q5 c- n5 i4 b/ S插入时间复杂度:O ( 1 ) O(1)O(1)/ \: H/ d9 w: _% b; ]+ v
查找时间复杂度:理论上不支持
" p8 b+ Z+ c' L' Q/ {删除时间复杂度:O ( 1 ) O(1)O(1). z% X( o% e# U2 }
( n% D, U4 S; h+ Q. \
$ b* d$ D( G+ F) E5 [. U2 G; a. v
f、栈
4 A& [# L+ V, g, b8 ^内存结构:看用数组实现,还是链表实现7 D# o6 \. p( E4 g8 T, n2 H
实现难度:一般4 v% m  ]1 R3 t8 Q3 _0 ~) a
下标访问:不支持5 V; t# L7 i1 x
分类:FILO、单调栈
9 L2 y3 ?! [* O# U  {; [! |/ z7 y插入时间复杂度:O ( 1 ) O(1)O(1)0 O4 B( ]4 i9 D+ x
查找时间复杂度:理论上不支持7 o: W/ R* D9 m% \$ }
删除时间复杂度:O ( 1 ) O(1)O(1)0 y% m0 D- T% N9 ^
( O9 {  E* ^5 ]6 Y$ ^3 Z3 O# Q# X1 o

1 A7 }( r) _( c$ T" ^% ]g、树
( d6 a9 O/ R9 G0 O! H6 b% Y% P内存结构:内存结构一般不连续,但是有时候实现的时候,为了方便,一般是物理连续,逻辑不连续
8 \0 f) b! F+ V3 f/ ]- G实现难度:较难
. E; d& ^# G/ P1 Y7 D9 p" s下标访问:不支持; t# S0 o  k( E( k; z" `; ^
分类:二叉树 和 多叉树
* f) _, v5 h: G" P+ l# V插入时间复杂度:看情况而定
# A* f) D9 A% C+ W2 W查找时间复杂度:理论上 O ( l o g 2 n ) O(log_2n)O(log
" E6 D5 D% A) \7 X9 _" Q' [2) t4 ^/ W( M3 ?& `! D
​       
5 W  m; J7 Q3 ] n)* D& h6 U& q9 ?
删除时间复杂度:看情况而定% e1 u, X: x  J7 {! q% Y0 Z

' W3 A- Z. _2 Z, [8 l2 d5 H& J
( {) ~$ P* P5 t( n& `3 r
1、二叉树
% a6 T, I) m# l$ [  A二叉树的种类较多,比如:二叉搜索树、平衡树。平衡树又可以分为 AVL 树、红黑树、线段树、堆。最平衡的树莫过于满二叉树了。) e4 C% ~2 }; ?, m
其中,堆也是一种二叉树,也就是我们常说的优先队列。
7 f8 @  w% A' u2、多叉树3 x8 S7 C# b2 P+ @/ ~% t3 b0 T, \
B树和B+树是多叉树,当然我们平时学到的并查集其实也是个多叉树,更加严谨一点,应该称之为森林。
1 y4 H  m5 S% ?# Z2 h$ `( T( s6 lh、图
1 x: H; n, L& f& r8 W% {" P3 i/ d: f内存结构:不一定* y/ {  N: ?% v$ W. B* _9 |( \
实现难度:难
2 ~2 w1 c9 M6 L8 Z, w) f下标访问:不支持
9 |3 ]+ s3 J* @& l& X# _7 z; A; ^分类:有向图、无向图( `5 D  I2 a0 Z
插入时间复杂度:根据算法而定
% |0 R9 w( ]; y查找时间复杂度:根据算法而定
1 Z( t! w2 X; Q+ I删除时间复杂度:根据算法而定
+ f# _' D' \, i6 Y7 t: a* e$ r- j9 _# n0 n3 y4 h$ p
+ n# e$ [+ C, q9 t
1、图的概念
2 ~  ?7 o1 Z" K- s) i# c5 b* \在讲解最短路问题之前,首先需要介绍一下计算机中图(图论)的概念,如下:+ K. y* ^& W/ f% B: ]/ Z: P
图 G GG 是一个有序二元组 ( V , E ) (V,E)(V,E),其中 V VV 称为顶点集合,E EE 称为边集合,E EE 与 V VV 不相交。顶点集合的元素被称为顶点,边集合的元素被称为边。  O8 w) p5 J+ x2 |! m, H- O  D
对于无权图,边由二元组 ( 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 为权值,可以是任意类型。" _) r2 I2 @7 u/ V
图分为有向图和无向图,对于有向图, ( 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;
8 S4 b/ y5 f, L& C9 C2、图的存储; y7 W- y% f' p0 K5 P
对于图的存储,程序实现上也有多种方案,根据不同情况采用不同的方案。接下来以图二-3-1所表示的图为例,讲解四种存储图的方案。& @9 R8 @: w& n7 ~. p
5 p' V/ H/ N5 F- u

5 Z+ {( A) G. K3 x6 M1)邻接矩阵1 l) v$ ^; S, `, u) V
邻接矩阵是直接利用一个二维数组对边的关系进行存储,矩阵的第 i ii 行第 j jj 列的值 表示 i → j i \to ji→j 这条边的权值;特殊的,如果不存在这条边,用一个特殊标记 ∞ \infty∞ 来表示;如果 i = j i = ji=j,则权值为 0 00。4 M9 T  X3 A7 l/ B
它的优点是:实现非常简单,而且很容易理解;缺点也很明显,如果这个图是一个非常稀疏的图,图中边很少,但是点很多,就会造成非常大的内存浪费,点数过大的时候根本就无法存储。
5 ~. r7 B; i4 |* F[ 0 ∞ 3 ∞ 1 0 2 ∞ ∞ ∞ 0 3 9 8 ∞ 0 ] \left[! ]/ @) |( ?% F
01∞9∞0∞8320∞∞∞30, L. \# g1 B* |) v5 g% C' m
0∞3∞102∞∞∞0398∞0
9 Z! I3 u* ?- l5 n* B* V\right]
1 A4 o6 s7 r$ J( P2 V" o) h) w* f
+ f' V( h4 L3 s8 g& O8 v0 }# D% O1 J4 l2 j% ^  Q3 j. ?: s
; U8 n  j- r! @- s* O; Y: l
) V! X  y, t. p7 ]3 g5 q: ^
​       
% y" {9 V. d* q' x# K  2 g+ |2 _  ]7 ~6 m
0
, C# e- F" H* N4 j' R! b0 M9 q1$ F- r, y- t+ G" Y2 j+ J( b

. [8 p! P1 T' |, |! Q2 V5 A9
" i2 P4 m9 H5 R. t( }3 @) _​        ! O; s4 K* g, L' `
  
' E$ G1 `1 H4 L5 u
( c% x" I" ]% h% |, w03 N( h" C$ X$ ~) `" |
9 G; j0 X" ?' F4 B& ^
87 q. T3 a$ J7 B
​        4 h0 ]0 K; I( o0 u
  % @% @2 O" W, Y" i; l
34 u3 C) R8 F3 I# o
2) e. m: V9 ?8 @; j3 R" Q
0
7 e. p, @/ M* Q/ l( o# ^
7 H* M: B0 `" R. G; q$ F​        9 A3 Q1 @6 P/ P! ]6 \
  
6 y3 C/ B" A- U' t1 |- a
) e+ c1 Z7 E1 M/ a+ h0 s
% P1 {: i/ t1 p0 J; |1 [3
, |6 S5 y; W$ s1 V- @; F0
& y/ @, `4 m8 ^5 u# C. P​        * {# x2 x0 s  y/ @3 H
  - B' ]. W) Y, @# |1 Z: ]4 A
# x1 C/ h. K9 G

* C3 I  y0 W" ]5 S! p1 w0 Y5 u# B2 b: w1 A& ?; q3 r: t/ q

' Q& o7 h/ I8 v' ^& F​        . _1 B7 |3 i9 z9 a

3 w: }2 \( {) ]; ^; }; P5 V& p2)邻接表2 a5 t: l& m7 D6 m* z
邻接表是图中常用的存储结构之一,采用链表来存储,每个顶点都有一个链表,链表的数据表示和当前顶点直接相邻的顶点的数据( v , w ) (v, w)(v,w),即 顶点 和 边权。
$ \9 z6 \+ I. a* G+ {它的优点是:对于稀疏图不会有数据浪费;缺点就是实现相对邻接矩阵来说较麻烦,需要自己实现链表,动态分配内存。
% w. }3 g/ A, @; w如图所示,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) 二元组。1 O& {" W1 a( c$ g/ l% R
/ k; K* m; K4 V# c8 O8 y$ z9 j
; g# g* R' @; d+ A. c
在 C++ 中,还可以使用 vector 这个容器来代替链表的功能;
$ ~* H- r& {) R( x    vector<Edge> edges[maxn];
* K. E: Q5 o" k& D: j6 f0 a1
0 |6 w1 Q4 a; f2 I4 @9 T3)前向星% p. |* I" u( ^+ `3 M; g5 f
前向星是以存储边的方式来存储图,先将边读入并存储在连续的数组中,然后按照边的起点进行排序,这样数组中起点相等的边就能够在数组中进行连续访问了。
9 N1 l: G5 `: D9 ]# w8 P$ ?( g它的优点是实现简单,容易理解;缺点是需要在所有边都读入完毕的情况下对所有边进行一次排序,带来了时间开销,实用性也较差,只适合离线算法。
, O+ u2 i+ I- ~- k& U如图所示,表示的是三元组 ( u , v , w ) (u, v, w)(u,v,w) 的数组,i d x idxidx 代表数组下标。1 C9 D( a9 p8 k2 _5 ~
. T& F* N0 t2 S* u; d

) H0 `% z) W; `" x那么用哪种数据结构才能满足所有图的需求呢?2 L6 O6 Q8 r* f5 X7 J; v1 A- z
接下来介绍一种新的数据结构 —— 链式前向星。
7 ^+ L3 j, ^9 G4 e4)链式前向星+ p3 g% [2 ?- W: j( j
链式前向星和邻接表类似,也是链式结构和数组结构的结合,每个结点 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 指向下一条边。
, i3 X) x1 M& w9 N' f; x. A具体的,我们需要一个边的结构体数组 edge[maxm],maxm表示边的总数,所有边都存储在这个结构体数组中,并且用head来指向 i ii 结点的第一条边。. ^  G! t: G: n8 l
边的结构体声明如下:- n8 [! J$ a' _- r- \; i8 L7 v
struct Edge {" X6 n% T8 y  b$ f+ c1 R
    int u, v, w, next;
7 a1 W0 L6 g$ F; l. G; R9 `    Edge() {}% R7 d8 ?! S. ?5 N
    Edge(int _u, int _v, int _w, int _next) :* m/ b; k5 s& }! A( ^' U2 p! C
        u(_u), v(_v), w(_w), next(_next) : T* J+ u& C$ Q, a* P( x- \7 [( K
    {- H/ ?' q) ]/ ?1 P0 Y+ q6 _8 D
    }
. h( Y8 u( J3 M0 b}edge[maxm];
" U9 b& S, t7 }15 Q2 F  u; i' X
2
7 y6 H  w/ V7 C+ Z3; l* k3 K9 y3 U; {2 T
4
, d- O$ {7 j9 M0 K5
+ a- t" F  y" F7 U6
- [8 M# t" Z. `3 C: E% G76 z: p4 Q3 f, J2 D7 i8 D, V/ p$ F& f) ?
8
8 e2 z0 l# h$ d  H* U" A初始化所有的head = -1,当前边总数 edgeCount = 0;
, }8 @' r  _& I" r, o% m每读入一条 u → v u \to vu→v 的边,调用 addEdge(u, v, w),具体函数的实现如下:' |5 F4 h9 }8 I; \) J
void addEdge(int u, int v, int w) {
, H5 h$ x2 ?: t( J  V- c    edge[edgeCount] = Edge(u, v, w, head);8 D2 E! Y/ ]) K
    head = edgeCount++;
! U; Y+ o2 O8 L3 C6 d}
6 z: G3 M" F% {7 t) F, N8 K+ A1
# u. g. Q9 V% w% ?8 I& p7 Y2  n" q! F, y/ Z+ u
3
9 e8 J' ?; B" k4
8 Q5 G+ F$ V5 C+ t1 U9 X: K这个函数的含义是每加入一条边 ( u , v , w ) (u, v, w)(u,v,w),就在原有的链表结构的首部插入这条边,使得每次插入的时间复杂度为 O ( 1 ) O(1)O(1),所以链表的边的顺序和读入顺序正好是逆序的。这种结构在无论是稠密的还是稀疏的图上都有非常好的表现,空间上没有浪费,时间上也是最小开销。
  [) [+ j/ S1 c6 Q8 Z" Y9 a调用的时候只要通过head就能访问到由 i ii 出发的第一条边的编号,通过编号到edge数组进行索引可以得到边的具体信息,然后根据这条边的next域可以得到第二条边的编号,以此类推,直到 next域为 -1 为止。5 `9 j# ]" v% J6 g0 `3 B
for (int e = head; ~e; e = edges[e].next) {( E* a+ q8 ^7 f1 ?" B
    int v = edges[e].v;! \+ H/ p' n0 s" N' c0 ]7 n+ {% J
    ValueType w = edges[e].w;$ V& t5 Q; [4 P4 l
    ...
5 T* Q( }$ H1 ~' f9 w# f}  O1 i7 m' S+ g5 L7 Z# D
1
+ E3 g/ Q  V  L- `- h6 s2
& ~+ J! r- d+ g0 Y1 o6 k3& u$ q: ~  l2 d8 {& C
4
9 {; [; z6 \* q- X) z) J; D# I5 m54 ]9 b7 s( P$ w& S0 d
文中的 ~e等价于 e != -1,是对e进行二进制取反的操作(-1 的的补码二进制全是 1,取反后变成全 0,这样就使得条件不满足跳出循环)。
! J. q& k6 u/ p- R0 A4、算法入门
3 a8 y9 D. W4 o4 K" \算法入门,其实就是要开始我们的刷题之旅了。先给出思维导图,然后一一介绍入门十大算法。
) N: b4 [! i( X1 M# O  T& W) B1 z' {: L

! G- H. i+ w( U! T; |' ]  V. I入门十大算法是 枚举、排序、模拟、二分、双指针、差分法、位运算、贪心、迭代、分治。
& ], _! v7 V! K( q7 |$ @9 S对于这十大算法,我会逐步更新道这个专栏里面:《LeetCode算法全集》。9 V/ D. |) c. T! s/ Z- L' k2 W# f
1、枚举* G6 O$ L! k3 e9 D! S4 f" }
枚举可以简单理解成for循环,从一个数组中遍历查找一个值,就是枚举;从一个数组中找到一个最大值,就是枚举;求数组所有数的和,也是枚举。
; n! T) Z7 U- M8 P" o6 p- {对于枚举而言,基本就是循环语句的语法学会,这个算法就算学会了。
% Q& {: W1 [: }2、排序
' a" E2 t0 x# K* f: j! y  R; \" m5 j既然是入门,千万不要去看快排、希尔排序这种冷门排序。
. x# z" `! f; [) m- |冒泡排序、选择排序、简单插入排序 原理好懂,先看懂再说,其他不管。因为这三者都是基于枚举的。/ D6 j; z5 U+ }- x* D
C中有现成qsort排序函数,C++中有现成 sort排序函数,直接拿来用,等算法进阶时再回头来看快速排序的算法实现。
2 p4 w& P# I! G. O* B3、模拟1 |0 L) \/ s# j* M8 _/ z& M) }
模拟就是要求做什么,你就做什么,完全不要去考虑效率问题。
+ Y. I; p/ i1 {, v( U) a2 O) v不管时间复杂度 和 空间复杂度,放手去做!8 s2 O% s$ L( c$ ?
但是,有时候模拟题需要一些复杂的数据结构,所以模拟题难起来也可以很男,难上加难。- r9 Y  P- B$ m# V3 Z
4、二分
& X* p( C7 _2 N; r5 P二分一般指二分查找,当然有时候也指代二分枚举。7 g9 f; q* \9 L' N5 c9 ~" o7 b
例如,在一个有序数组中查找值,我们一般这个干:+ \7 R8 o0 B% b8 ~1 [  {
1)令初始情况下,数组下标从 0 开始,且数组长度为 n nn,则定义一个区间,它的左端点是 l = 0 l=0l=0,右端点是 r = n − 1 r = n-1r=n−1;
" ?2 E6 S; N. E4 N2)生成一个区间中点 m i d = ( l + r ) / 2 mid = (l + r) / 2mid=(l+r)/2,并且判断 m i d midmid 对应的数组元素和给定的目标值的大小关系,主要有三种:
! u* s7 V$ G4 i9 Y4 p. F5 v  2.a)目标值 等于 数组元素,直接返回 m i d midmid;; |+ E$ |) t7 T
  2.b)目标值 大于 数组元素,则代表目标值应该出现在区间 [ m i d + 1 , r ] [mid+1, r][mid+1,r],迭代左区间端点:l = m i d + 1 l = mid + 1l=mid+1;) D% j. x: d# x7 ^
  2.c)目标值 小于 数组元素,则代表目标值应该出现在区间 [ l , m i d − 1 ] [l, mid-1][l,mid−1],迭代右区间端点:r = m i d − 1 r = mid - 1r=mid−1;
5 {! r; ?% P; i/ I1 ^4 w3)如果这时候 l > r l > rl>r,则说明没有找到目标值,返回 − 1 -1−1;否则,回到 2)继续迭代。$ f' ^$ g. V! e0 z$ T4 J  v8 A4 F
5、双指针
, r7 s  @" A- a! A* _5 G# J4 D双指针,主要是利用两个下标在一个数组上,根据问题的单调性,进行指针偏移,由于每个指针只往后偏移,所以时间复杂度可以达到 O ( n ) O(n)O(n),由于思想非常简单,所以出题时,热度不低。
9 H7 b5 l8 Y; }
( c5 }, n0 S/ Y5 b
! p2 h& l9 Z0 i* I  ^; l/ A
6、差分法
% k6 p; }& t+ b- ]2 ~差分法一般配合前缀和。
9 X! V$ h" v, k8 n3 Y2 f' e对于区间 [ l , r ] [l, r][l,r] 内求满足数量的数,可以利用差分法分解问题;
. v' t+ K8 E! P/ g假设 [ 0 , x ] [0, x][0,x] 内的 g o o d   n u m b e r good \ numbergood number 数量为 g x g_xg
4 o9 t, m: |( L+ L+ P, @# Z; hx- f+ v2 l, }5 z$ j( Z
​       
: B* T6 G" ]; @4 d5 c% q ,那么区间 [ l , r ] [l, r][l,r] 内的数量就是 g r − g l − 1 g_r - g_{l-1}g ' X! ~; e+ D; `- t
r+ q, A7 J3 h' D- Z% z
​        6 y6 [" E+ O3 w0 j
−g
# K8 ], U6 s4 T4 h2 n, h/ S* [l−13 D. a  Z$ a- F' a
​        ( Q% _: h% b. ~& X
;分别用同样的方法求出 g r g_rg - s" b/ M2 ~1 r. q9 J8 E8 w4 E: e$ N6 I* p
r2 u! ]4 e: o& n# o6 ?  e
​       
4 [8 \, b7 F* n$ n  和 g l − 1 g_{l-1}g   M  a1 I8 u8 Q, e( ~
l−12 K+ q0 m* m4 ^& R
​       
& Y( d4 q" C0 }* M ,再相减即可;
7 ~" x& y: y$ k5 N0 c8 F* _7 y1 U8 p2 h& k( X4 i5 w

- C. k* J' O! y! Y) [* v8 Y7、位运算$ Y* S# B: Q8 G- t8 k8 p
位运算可以理解成对二进制数字上的每一个位进行操作的运算。
& i  m5 n- a* w) D  D& Z. \, |位运算分为 布尔位运算符 和 移位位运算符。
6 [+ k) x) m: B: P布尔位运算符又分为 位与(&)、位或(|)、异或(^)、按位取反(~);移位位运算符分为 左移(<<) 和 右移(>>)。" y: W6 y( m7 J% p( f; ~7 n
如图所示:% q( u$ X. g# F, z9 w# |! ?$ T

# S; d( b/ ?) U5 F% j: ^9 @
/ S2 T5 ~! `% V" \% }$ P. y3 s
位运算的特点是语句短,但是可以干大事!! n9 z% _* Q2 a  X  |0 D) w
比如,请用一句话来判断一个数是否是2的幂,代码如下:
, Y0 E. z2 |/ r6 `!(x & (x - 1))5 e  V6 h0 D, z, ~
1
0 B# w; N% Y& }8、贪心, F1 v7 I/ P0 ~7 A  `  P
贪心,一般就是按照当前最优解,去推算全局最优解。
% p& S4 o4 S$ ]/ l* t9 f; m; ?所以,只有当当前最优解和全局最优解一致时才能用贪心算法。贪心算法的证明是比较难的,但是一些简单的贪心问题会比较直观,很容易看出来这个能够这么贪。
7 h9 z! x# y' A* _9、迭代
* t1 r4 m! {: T' w- t每一次对过程的重复称为一次“迭代”,而每一次迭代得到的结果会作为下一次迭代的初始值,周而复始,直到问题全部解决。4 n- u% M9 J* |1 x! o, P
10、分治" ?8 v1 s8 s. h' p% f4 B
分治,就是把问题分成若干子问题求解,子问题解决后,问题就解决了。一般利用递归实现。属于初学者比较头疼的内容。递归一开始学习的时候,一定要注意全局变量和局部变量的关系。
5 A  u. ]- v/ k5、算法进阶' _  ^7 e& H0 V/ ~+ t% i
算法进阶这块是我打算规划自己未来十年去完成的一个项目,囊括了 大学生ACM程序设计竞赛、高中生的OI竞赛、LeetCode 职场面试算法 的算法全集,也就是之前网络上比较有名的 《夜深人静写算法》 系列,这可以说是我自己对自己的一个要求和目标吧。) q3 ~8 z2 P! n" t; `+ m
如果只是想进大厂,那么 算法入门 已经足够了,不需要再来看算法进阶了,当然如果对算法有浓厚兴趣,也欢迎和我一起打卡。由于内容较难,工作也比较忙,所以学的也比较慢,一周基本也只能更新一篇。' o" x9 x3 y8 k3 j. J
这个系列主要分为以下几个大块内容:1 J5 c* T& |3 Q( m/ g
  1)图论
  K; P- G$ ]& `3 E% w  2)动态规划
) W: ]- A; I' n/ L2 c) s  3)计算几何4 r  t! Q. l. Y7 F4 ~4 `* r# C
  4)数论' v3 ~) i4 ?: V5 c8 [! o1 y7 r
  5)字符串匹配
# p5 b3 B( O' t6 h/ Z! Q' v/ P  6)高级数据结构(课本上学不到的)/ ^  v) r. h, g9 k
  7)杂项算法
$ K) k) `* m' c: e5 t: b
/ C2 Y1 g" f( M3 `* ]* R5 q
! ]( E4 E5 q1 n3 y( u- V* h0 j
先来看下思维导图,然后我大致讲一下每一类算法各自的特点,以及学习方式:4 ^! U' H$ D( m" N! ^; ?
% b, q# v% A; H8 g

9 x1 e' r; X) c! U8 G4 U$ P# E( b! f' D" l) i3 ]4 q
0 ]' @' {( \; f. K* C; B7 w4 d
1)图论
' M; p6 @6 w7 g! a1、搜索概览/ U+ t' ^3 x" C. J# I9 C4 ~3 t
图论主要围绕搜索算法进行展开。搜索算法的原理就是枚举。利用计算机的高性能,给出人类制定好的规则,枚举出所有可行的情况,找到可行解或者最优解。
( X- ^# L4 H8 f8 K; H' L! {. L  ~/ _+ \9 e# M! b

; V! x' W8 @/ A4 g/ M比较常见的搜索算法是 深度优先搜索(又叫深度优先遍历) 和 广度优先搜索(又叫广度优先遍历 或者 宽度优先遍历)。各种图论的算法基本都是依靠这两者进行展开的。
% h, Z8 d$ u& j' K2、深度优先搜索1 F, ?- E/ Q1 ?$ {0 Y9 \0 D" x. w
深度优先搜索一般用来求可行解,利用剪枝进行优化,在树形结构的图上用处较多;而广度优先搜索一般用来求最优解,配合哈希表进行状态空间的标记,从而避免重复状态的计算;
6 |# R8 M1 A2 N- G5 a. c/ F原则上,天下万物皆可搜,只是时间已惘然。搜索会有大量的重复状态出现,这里的状态和动态规划的状态是同一个概念,所以有时候很难分清到底是用搜索还是动态规划。, r# ~9 S1 o: |1 _7 c
但是,大体上还是有迹可循的,如果这个状态不能映射到数组被缓存下来,那么大概率就是需要用搜索来求解的。
4 h4 x9 P7 X5 r) l# w* _' {如图所示,代表的是一个深度优先搜索的例子,红色实箭头表示搜索路径,蓝色虚箭头表示回溯路径。8 o- A- y$ _) L7 q
. e6 e2 Z, C  m, N; [
1 Q" w! d& T) C& u# S5 G" S% ?* q: v
红色块表示往下搜索,蓝色块表示往上回溯,遍历序列为:  ^# U0 o. O5 r/ m) C5 B* ^5 @
        0 -> 1 -> 3 -> 4 -> 5 -> 2 -> 6
$ `% b5 C5 [/ [& C. \' G) B1: ]: |$ H8 x6 W. d/ L2 u) Q
同样,搜索的例子还有:8 E2 L9 J- s5 Q7 Q/ Z$ x
" \$ Q1 Q: _) u; A3 y# p: I

$ ^- S" I; m, W9 A: \! y$ n计算的是利用递归实现的 n nn 的阶乘。* N* L2 P, L6 C. K  f# B; N
3、记忆化搜索+ M) M/ P( V) Q: J6 N
对于斐波那契函数的求解,如下所示:0 ?; d3 p6 g7 d" E. j' T
f ( n ) = { 1 ( n = 0 ) 1 ( n = 1 ) f ( n − 1 ) + f ( n − 2 ) ( n > 2 ) f(n) =( }/ m1 l. L0 x, ?
⎧⎩⎨11f(n−1)+f(n−2)(n=0)(n=1)(n>2)
% Q. L. `9 C( t. ]$ [, W" s{1(n=0)1(n=1)f(n−1)+f(n−2)(n>2)
8 t1 b. S+ \( d" r. @0 s  E- pf(n)= : P: l9 C3 {8 Y( U. {( w

/ f" G, M* L7 M0 M1 S9 l$ z- }& K& V% @0 W4 F  ~3 Y
0 R4 f2 k2 y4 K

# M5 i) P" L! O4 ]
+ D$ \, |: o8 r+ t8 f3 Z7 D7 x​        8 v9 z. D" Q4 P  g; \# j
  
6 ^9 v, R) Y4 g1: X: N6 ~( `! @& v8 ^1 i6 X
1  h$ j) b5 n1 s6 C/ V' d
f(n−1)+f(n−2)+ I5 `  T- W! w$ W" t
​        ' S: I$ ]- M7 x6 a
  5 d1 B' @2 e3 f% z# i
(n=0)# |% P& l+ M$ e/ v2 o8 a* a/ s6 v$ O
(n=1)- W( T/ Y7 V4 e0 O* g4 Z
(n>2)
9 w6 }, m7 l  i4 l, ]​        , A" v2 @( D1 N6 |8 z2 f5 H2 x
. N; p; k8 ]# A
对于 f ( 5 ) f(5)f(5) 的求解,程序调用如下:
1 D7 {: E0 B# \( z/ C/ e& r8 i+ o9 S: v; H+ `

! p% [4 t4 N; A) r9 d这个过程用到了很多重复状态的搜索,我们需要将它优化,一般将一些状态缓存起来。
# j4 f6 D% L8 ?/ X8 M- p; |8 f7 F我们通过一个动图来感受一下:& ~$ B+ s. [2 o

5 ?7 t) M! s8 s

9 |9 [  S: q6 d当第二次需要计算 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表达式为真,直接返回,不再需要往下递归计算,这样就把原本的 “递归二叉树” 转换成了 “递归链”, 从而将原本指数级的算法变成了多项式级别。( H% Q, r4 n8 O! r
这就是记忆化搜索,像这种把状态缓存起来的方法,就是动态规划的思想了。
: ^, b  v9 A/ B3 d: ?% v* R; V4、广度优先搜索( H, x/ E2 L- O6 S$ I% Z' ^2 b
单向广搜就是最简化情况下的广度优先搜索(Breadth First Search),以下简称为广搜。游戏开发过程中用到的比较广泛的 A* 寻路,就是广搜的加强版。% Q" T; B9 q; `7 W' C" v
我们通过一个动图来对广搜有一个初步的印象。/ t: V& g% E0 N6 N
- G; E: K# o) Q- H6 G% f6 W5 `
/ k" Q! T- C: i0 E. Z2 S' C% E( A: ^
" \" B' W# U8 f- T% ~
9 \: f8 H/ W) y6 ?- B0 s/ }
从图中可以看出,广搜的本质还是暴力枚举。即对于每个当前位置,枚举四个相邻可以行走的方向进行不断尝试,直到找到目的地。有点像洪水爆发,从一个源头开始逐渐蔓延开来,直到所有可达的区域都被洪水灌溉,所以我们也把这种算法称为 FloodFill。
; ?$ c% R3 T7 M/ b7 d  u2 t那么,如何把它描述成程序的语言呢?这里需要用到一种数据结构 —— 队列。
- {5 l# F' f" a" K: C这时候,算法和数据结构就完美结合了。
1 K3 o* r8 L% ^/ |- K8 g2)动态规划/ p3 I% e+ _. ?8 S' q1 N
动态规划算法三要素:! v% N2 a* ]# ~: W
  ①所有不同的子问题组成的表;
9 M# J8 G+ i& M1 L1 ~+ g' F  ②解决问题的依赖关系可以看成是一个图;1 @# t! O, I  V2 t0 S, H
  ③填充子问题的顺序(即对②的图进行拓扑排序,填充的过程称为状态转移);
# ]- t6 }0 h. e5 n
2 `1 a7 I+ P7 \& Z) z' E3 [

0 I+ _0 |$ U% N: k! G1 S" E如果子问题的数目为 O ( n t ) O(n^t)O(n
& i- f: K+ N$ v/ c4 I% Ct
& z6 X3 S" K: J) F! Q ),每个子问题需要用到 O ( n e ) O(n^e)O(n
! |  Q: l* e$ d8 j7 d" a# F% x8 k6 ^e
% i8 _# @4 O1 `4 _; Y" ^* H7 g) `- Y ) 个子问题的结果,那么我们称它为 tD/eD 的问题,于是可以总结出四类常用的动态规划方程:(下面会把opt作为取最优值的函数(一般取 m i n minmin 或 m a x maxmax ), w ( j , i ) w(j, i)w(j,i)为一个实函数,其它变量都可以在常数时间计算出来)。: U$ n& u# w4 ?4 R0 P7 |
1、1D/1D
2 ?4 s! a7 x. e8 L+ Pd [ i ] = o p t ( d [ j ] + w ( j , i ) ∣ 0 < = i < j ) d = opt( d[j] + w(j, i) | 0 <= i < j )7 |$ a  H; i! `: y8 Y2 Q- w
d=opt(d[j]+w(j,i)∣0<=i<j)5 n, q/ E, Q; e+ O; x
状态转移如图四所示(黄色块代表d [ i ] dd,绿色块代表d [ j ] d[j]d[j]):
2 f! N$ d: E* d0 s! D* P9 h+ C9 C( v) t% N5 R
$ {* _( b5 n/ i6 T" r8 C  F" i6 @
这类状态转移方程一般出现在线性模型中。
( ]/ v7 S2 A9 j  A- s4 N7 G2、2D/0D+ w; c: Q& \1 p% ~
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} )) f3 b3 Y5 I2 Q, E5 c" |& f8 D
d[j]=opt(d[i−1][j]+x ) B+ d7 Y2 E1 k& \
i- }4 q; r' [# W2 m$ H, }% |
​        & C, K/ P, D% C  e9 W2 ^5 i3 L
,d[j−1]+y
2 h; h) E9 {% O7 ^; Cj2 w; f: Q+ }, M1 {& U' N7 }- p! V
​       
& h  g2 T/ t2 Q, @+ k& }6 G ,d[i−1][j−1]+z
0 [6 {/ K# e3 L8 {# ~8 [6 }0 X2 G' ?ij
; H$ M% Y- I1 r# H! G  P6 y​       
: d9 z$ V, e7 G2 u3 O7 U )$ O0 B, ]2 N2 F, [
状态转移如图四所示:
; A/ z% j) f. e9 k* i
+ n9 g4 J% S) P& H8 J  v

5 P( Q9 j0 a3 A) h' l. w比较经典的问题是最长公共子序列、最小编辑距离。2 `: h: u% L& _$ a
有关最长公共子序列的问题,可以参考以下文章:夜深人静写算法(二十一)- 最长公共子序列' G9 f# z9 f0 u% @8 K+ }+ ]- F; {) V
有关最小编辑距离的问题,可以参考以下文章:夜深人静写算法(二十二)- 最小编辑距离
- [, J: m0 T4 \7 F& ?3、2D/1D
9 H+ p4 F0 p, ~) o. O0 Gd [ 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] )
. L1 q( e3 Q" w  a2 m6 ]d[j]=w(i,j)+opt(d[k−1]+d[k][j])
* V3 S5 |3 B/ S  Q9 A5 A5 J& k区间模型常用方程,如图所示:
0 h- A, j; s0 R+ C2 ?, ^( t: r
8 F' G1 g  S( D

# O( p- L( S) ?/ ]0 ?+ O0 C另外一种常用的 2D/1D 的方程为:
) S$ L5 C) b+ d2 M! ^$ k- vd [ 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 )
( F# _- P' B; g% nd[j]=opt(d[i−1][k]+w(i,j,k)∣k<j)5 }$ `! Y$ E0 M2 o! R
区间模型的详细内容可以参考以下这篇文章:夜深人静写算法(二十七)- 区间DP& S; z0 |: r1 M9 e: O- X
4、2D/2D) q' A0 Y. q" z1 N
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)
, g& Q9 C8 ~4 N: ^* A5 Y# o% sd[j]=opt(d[i
* V5 ]/ [* t/ w" S8 g- k, [2 ~$ G% `0 h# T) D& [- m
][j 2 Y: T' Z8 D# o0 A$ l% \8 k

- `8 q7 x2 U4 X4 g& w* B3 j ]+w(i
: m+ b& Y/ @9 m) w& B' r7 }; J$ ?3 Z# w. [* |
,j . y% j1 ]( i! i& S

9 H5 F" w9 p/ o ,i,j)∣0<=i
7 g/ c5 m' h! ?# o' K2 H. ?( w5 h' q- X; X- F; z' H! ]
<i,0<=j $ d  B: I8 ?: s7 b  E; S

/ {5 X: Q% R! l; t <j)
9 L* P7 _6 F  l7 i如图所示:
) Q! d# u5 S  H3 Y, E( E5 B, n: ], Z0 k: Y7 Z9 D2 Z$ }

& |8 _( S" r7 ]6 M: }常见于二维的迷宫问题,由于复杂度比较大,所以一般配合数据结构优化,如线段树、树状数组等。5 N5 _- ~/ o; n+ }: h0 S
对于一个tD/eD 的动态规划问题,在不经过任何优化的情况下,可以粗略得到一个时间复杂度是O ( n t + e ) O(n^ {t+e})O(n 3 w& l2 G- ]  w" m
t+e
9 z; d% H) |" y1 I8 P ),空间复杂度是O ( n t ) O(n^t)O(n
, T/ s5 X$ G. J2 Wt5 P  ~, E5 l; L" v; i
) 的算法,大多数情况下空间复杂度是很容易优化的,难点在于时间复杂度,后续章节将详细讲解各种情况下的动态规划优化算法。4 _8 w8 `. ~+ Z# R
3)计算几何$ {4 x3 ~& F7 z8 L. m$ W/ o6 X9 J
计算几何的问题是代码量最大的。它是计算机科学的一个分支,以往的解析几何,是用代数的方法,建立坐标系去解决问题,但是很多时候需要付出一些代价,比如精度误差,而计算几何更多的是从几何角度,用向量的方法来尽量减少精度误差,例如:将除法转化为乘法、避免三角函数等近似运算 等等。
# e1 T! l! v8 D: H" B9 l如果一个比赛中,有一道计算几何的题,那么至少,它不会是一道水题。9 E8 V0 b) n* W/ G% I# d
1、double 代替 float
! g) X5 P  `+ n2 ^, k: M& `# S4 `c++ 中 double 的精度高于 float,对精度要求较高的问题,务必采用 double;
) c4 @0 a2 ^$ f2、浮点数判定
+ J0 E  p/ p, i5 G+ d& v由于浮点数(小数)中是有无理数的,即无限不循环小数,也就是小数点后的位数是无限的,在计算机存储的时候不可能全部存下来,一定是近似的存储的,所以浮点数一定是存在精度误差的(实际上,就算是有理数,也是存在误差的,这和计算机存储机制有关,这里不再展开,有兴趣可以参见我博客的文章:C++ 浮点数精度判定);" S! V% X/ A' b
两个浮点数是否相等,可以采用两数相减的绝对值小于某个精度来实现:# r, z9 p; M  O+ F, B
const double eps = 1e-8;! ~) a+ k) ?5 @5 s, ]9 ?- R# D- M
bool EQ(double a, double b) {
- x  n; e, G' x/ v    return fabs(a - b) < eps;
# w- a; t3 P5 Y( S" N}
: J  k1 T$ L- _) S1
0 \- g' R: L: F& t; S# u# `23 S% {" n  L" l1 u  ?  q( q+ D
3
5 k6 \8 V+ k  U% d. _, v: Z$ w48 i/ q2 a( E+ m$ }& W. V
并且可以用一个三值函数来确定某个数是零、大于零还是小于零:. K5 Q! q' }. Y" J: f! v1 j* t6 I
int threeValue(double d) {
5 {! K) H0 [  F* F, e9 U    if (fabs(d) < eps)
* i. Q! ?- X6 s6 J* [        return 0;0 V% n, o9 N4 U
    return d > 0 ? 1 : -1;2 z9 M) E9 w/ G7 Z2 b% h
}
1 U  \! L' A% M9 a1
/ x0 ~+ _+ L4 E1 [3 B27 m3 \$ f( z7 m/ n" \5 f5 d$ `) [
3
+ ~( n6 W& i/ F3 `43 K- H+ V$ f0 v% A1 V
59 E1 _# F9 N+ ]7 i7 `
3、负零判定$ a# |+ V9 T, e, O# x2 P% r
因为精度误差的存在,所以在输出的时候一定要注意,避免输出 -0.00:
0 p6 `. z: K; N9 s) u. Y& }$ T- W    double v = -0.0000000001;$ H9 E2 R/ i; G, u! Q
    printf("%.2lf\n", v);) M, y& i# S. J7 ?; c/ Y5 S
18 X# [, i  Y$ ?% ?7 E! ]
2
' d& k9 j* `: A+ l8 V- X: W避免方法是先通过三值函数确定实际值是否为0,如果是0,则需要取完绝对值后再输出:
% ^: i0 t9 ^' v: Y; s1 W4 d    double v = -0.0000000001;
- q: Q! y; [, f    if(threeValue(v) == 0) {1 U; o  Z( e9 y5 p3 b+ x
        v = fabs(v);
7 P9 V6 f( H- ~% V" }& V    }' l. O5 j7 ^) K. V$ E& `
    printf("%.2lf\n", v);
, `- ~: `3 |$ [+ o( N7 k1
, _; y7 u7 Z+ N6 z8 y+ Q2 j( f5 G! ?2
; r9 F9 F8 i8 _. Q( O3; j7 B- r1 F* n
4& i9 a: u+ v* c/ ?$ v! x9 L
5. ?5 w! q8 i/ H+ H
4、避免三角函数、对数、开方、除法等
8 c  ^$ P7 j9 {- ?/ }c++ 三角函数运算方法采用的是 CORDIC算法,一种利用迭代的方式进行求解的算法,其中还用到了开方运算,所以实际的算力消耗还是很大的,在实际求解问题的过程中,能够避免不用就尽量不用。
: n/ u+ m/ ~; }除法运算会带来精度误差,所以能够转换成乘法的也尽量转换为乘法运算。
" `! P: N6 X  X  _, c5、系统性的学习: _# p7 ?6 e" \4 E* D% k. H4 ~
基础知识:点、向量、叉乘、点乘、旋转、线段、线段判交、三角形面积;
& s0 a# H! t! \& ?0 w& G; K进阶知识:多边形面积、凸多边形判定、点在多边形内判定;& E0 d- J! }# r! ~" p( h6 \: ?. \
相关算法:二维凸包、三维凸包、旋转卡壳、多边形面积交、多边形面积并、多边形面积异或、多边形和圆的面积交、半平面交、最小覆盖圆、最小包围球、模拟退火。1 b' H. G: h7 K. Q1 }

5 Y3 Q; p6 I( A3 i2 U* v

* s' l7 N) s! u: Z2 U学习计算几何,最好是系统性的,刷题的过程中不断提炼出自己的模板。
; @3 A( }4 ?: Z, R3 Z4)数论. o8 [6 U, `" o) D" ^+ B0 y/ ~
刷题的时候遇到不会的数论题,真的是很揪心,从头学起吧,内容实在是太多了,每个知识点都要证明吃透,不然下次遇到还是不会;不学吧,又不甘心,就是单纯的想把这个题过了,真是进退两难!
; d+ a  c1 @- q数论对一个人的数学思维要求较高,但是一般也是一些固定的模式,所以把模板整理出来很重要。
6 M( E$ i  v0 @9 q6 \' j当然,数论也有简单问题,一般先做一些入门题提升信心。
: R5 o& Y- x0 ~$ @5 M, s" e6 P1、数论入门7 I0 v, w" O* m: S& q
主要是一些基本概念,诸如:
$ T8 `1 x5 @1 Q+ G1 c" C1 p整除性、素数与合数、素数判定、素数筛选法、因数分解、算术基本定理、因子个数、因子和、最大公约数 (GCD) 和 最小公倍数 (LCM)、辗转相除、同余、模运算、快速幂取模、循环节;
3 e' K; T( ]+ Y+ |+ M! z' b3 W2、数论四大定理) l" Q) L) D4 ?; k% [5 ]6 X
这四个定理学完,可以KO很多题:
, {8 n6 U) l) ^1 |# Q: u4 W. r欧拉定理、中国剩余定理、费马小定理、威尔逊定理
# G' g) Q8 {/ C; e1 r3、数论进阶  e- f1 w; P4 R' c$ g. g. s
系统性的学习,基本也就这些内容了:
* N0 R; r& L& R4 f. `& _( q- t扩展欧几里得、逆元、欧拉函数、同余方程组、扩展欧拉定理、RSA、卢卡斯定理、整数分块、狄利克雷卷积、莫比乌斯反演、大数判素、大数因子分解、大步小步离散对数等等。8 k9 H* V. ]* A$ g7 a! t. `  P
5)字符串匹配( o1 i5 W4 I2 H7 M; a7 ?9 {7 T0 N& B1 L
字符串匹配学习路线比较明确。  u) O8 x# k6 r6 b9 n
先学习前缀匹配:字典树。
: A3 a9 G# O5 w& d1 U  Z然后可以简单看一下回文串判定算法:Manacher。0 |" L1 S: x" k' g+ D, I; f) m/ s
以及经典的单字符串匹配算法:KMP。, t7 J  w0 E, p* ~! H* {. }/ ~
实际上平时最常用的还是 BM 算法,而ACM中基本不考察。
$ J: ^- A& y' I9 B. N* ?( f然后就是较为高阶的 前缀自动机、后缀数组、后缀树、后缀自动机了。
7 ]" E9 c! p  j' u关于 算法学习路线 的内容到这里就结束了。$ C1 d( S: ^% H: S
如果还有不懂的问题,可以 想方设法 找到作者的微信进行在线咨询。
1 m) M0 H% N) D! Q5 B4 V参考资料, h% N5 U$ v( u; C. ?4 v7 r
【阶段一】C语言学习资料:《光天化日学C语言》(日更)4 c3 G6 O# Z' O1 ?5 `
【阶段二】C语言例题:《C语言入门100例》(日更)/ I% W5 G: p0 R5 {/ t0 z% l+ N6 [% N
【阶段三】算法入门题集:《LeetCode算法全集》(日更)
; D( ?. @; q; i' s% r2 x【阶段四】算法进阶:《夜深人静写算法》(周更)
4 U3 j  ]( T. O! \7 ^( L$ \# B4 \+ b————————————————# s: @) ?7 L$ u( L5 T* J3 X
版权声明:本文为CSDN博主「英雄哪里出来」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
! s/ z; F. Q% U& `原文链接:https://blog.csdn.net/WhereIsHeroFrom/article/details/118382228
8 y% `* G5 F3 i4 i0 V. V
8 ^# ~1 a0 F$ W  U
! P, b* }) U9 N1 _. m2 Q- X
作者: 1051373629    时间: 2021-8-17 17:14
太难得了
/ h) K9 H# p6 n! [5 d3 L9 o




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