1.引言——什么是算法
- 算法(Algorithm)是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系统的方法描述解决问题的策略机制。
一大段话可能看起来很空洞或者可以说是乏味和枯燥。简单来说算法其实就是一个提出问题和解决问题的过程。如下图所示,这是洛谷的一道题目,可以说是最最基础的一道题,由此我们可以看到一道完整的算法题应当会以什么样的方式呈现。A+B是小学一年级学习的加法内容,但如何将我们熟悉的东西用程序设计语言的方式展现,
其实就是学习算法一直需要面对的一个问题,对于较大的数据,又该如何处理,也是我们一直需要去解决的问题之一。
下面给大家介绍三个常用的在线判题系统OJ(Online Judge):
- 洛谷:https://www.luogu.com.cn/
- 牛客:https://ac.nowcoder.com/acm/home
- AcWing:https://www.acwing.com/about/
下方为洛谷测评状态示意图:
后续我们可能会参加ACM国际大学生程序设计竞赛(ICPC)、中国大学生程序设计竞赛(CCPC)、蓝桥杯程序设计大赛、中国高校计算机大赛——团体程序设计天梯赛等等。
2.基础算法
一栋大楼并非平地而起,地基的搭建往往是建造一栋楼最艰难的时刻。基础算法可以称的上是整个算法学习的地基。在学习基础算法时,除了对知识的理解,还有很重要的一点是探索个人学习算法的方式。我认为自己现在还没有找到一个适合我的学习方法,目前也仍在探索中。
下面来大致分享一些基本算法。
2.1 暴力
我接触到的第一个算法就是暴力。暴力如同他的字面意思,就是暴力求解,可以理解成小学奥数学过的枚举法。指的是从所有可能的解的集合中一一枚举各元素,用题目中给定的检验条件判定哪些是无用的,哪些是有用的。能使命题成立的就是答案。
该算法可以求的一些情况较少的问题的解,若问题规模太大,该算法便不适用。正如枚举法并不是时时适用一样,暴力也被数据和时间局限,往往数据量过大,暴力便不适用了。因此他的优缺点都很明显,简单易想,在部分题目中或某题目中的局部使用有较好的效果。但运算量过大。对于数据量较大的题目,效率较低,当问题规模变大时,循环的阶数越大,执行速度越慢,容易超时。
2.2 位运算
单看每一个都很简单无非就是与(&),或(|),非(~),异或(xor),还有一些移位计算,并没有什么难点。但当深入学习之后,了解了二进制状态压缩成对变换等等,就会逐渐体会到位运算多么重要。在后续很多算法中,我们都需要用位运算去实现一个算法,例如并查集需要用到lowbit等,位运算更像是水,一时半会儿不喝没有什么关系,但一旦干渴的时间长了,那是性命之忧,起初可能意识不到位运算的作用,但当你需要用到的时候,想不起来如何使用,也就只能干瞪眼了。
2.3递推与递归
一个实际问题的各种可能情况构成的集合通常被称为“状态空间”,而程序的运行则是对于状态空间的遍历,算法和数据结构则通过划分、归纳、提取、抽象来帮助提高程序遍历状态空间的效率。递推和递归就是程序遍历状态空间的两种基本方式。
对于一个待求解的问题,当它局限在某处边界、某个小范围或者某种特殊情形下时,其答案往往时已知的。如果能够将该解答的应用场景扩大到原问题的状态空间,并且扩展过程的每个步骤都具有相似性,就可以考虑递推或递归求解。以已知的“问题边界”为起点向“原问题”正向推导的扩展方式就是递推。
2.4前缀和和差分
前缀和是一种重要的预处理,能大大降低查询的时间复杂度。
根据下面两张图,相信大家一定能对一维和二维前缀和有一个初步理解。
前缀和和差分其实是一对互逆运算,差分序列B的前缀和序列就是原序列A。前缀和序列S的差分序列也是原序列A。
2.5 二分
二分法是一种随处可见却非常精妙的算法,经常能为我们打开问题的突破口。二分的基础用法是在单调序列或单调函数中进行查找。因此当问题答案具有单调性时,就可以通过二分把求解转换为判定(根据复杂度理论,判定难度小于求解),这使得二分法的运用范围非常广泛。二分的实现方式多种多样,但是其细节之处确实需要仔细考虑。
2.6 排序
我们经常会用到排序算法,这里把它分成三类:
- 选择排序、插入排序、冒泡排序
- 堆排序、归并排序、快速排序
- 计数排序、基数排序、桶排序
前两类是基于比较的排序算法,第三类换了一种思路,不直接比较大小,而是对被排序的数值采取按位划分、分类映射等处理方式。
2.7 贪心
贪心是一种在每次决策时采取当前已一下最优策略的算法,因此使用贪心法要求所有问题的整体最优解可以由局部最优性导出。
3.数据结构
3.1 栈
栈是一种“后进先出”的线性数据结构。栈只有一端能够进出元素,一般被称为一端为栈顶,另一端为栈底。添加或删除元素时,我们只能将其插入到栈顶(进栈),或者把栈顶元素从栈中取出他(出栈)。
3.2 队列
队列是一种“先进先出”的线性数据结构,一般来讲,元素从右端进入队列(入队),从左端离开队列(出队)。于是我们称队列的左端为队头,右端为队尾。可以在C++中用一个数组和两个变量(记录队头和队尾位置)来实现队列结构的。
3.3 链表与邻接表
数组是一种随机访问,但不支持在任意位置插入或删除元素的数据结构。与之对应,链表支持在任意位置插入或删除,但只能按顺序依次访问其中的元素。我们可以用一个struct表示链表的节点,其中可以存储任意数据。另外用prev和next两个指针指向前后相邻的两个节点,构成一个常见的双向链表结构。为避免左右两端或者空链表中访问过界,我们通常建立额外两个节点head与tail代表链表头尾,把实际数据节点存储在head和tail之间,来减少链表边界处的判断,降低编程复杂度。
3.4 Hash表
又称为散列表,一般由Hash函数(散列函数)与链表结构共同实现。Hash表一般包括两个操作:
- 计算Hash函数的值
- 定位到对应链表中依次遍历比较
3.5 字符串
STL-string本质是string是C++风格的字符串,而string本质上是一个类。有这样的特点:string 类内部封装了很多成员方法。
例如:查找find、拷贝copy、删除delete、替换replace、插入insert
string管理char*所分配的内存,不用担心复制越界和取值越界等,由类内部进行负责。
还有KMP模式匹配和最小表示法等,在此不详细叙述了。
3.6 并查集
并查集(Disjoint-Set)是一种可以动态维护若干个不重叠的集合,并支持合并和查询的数据结构。
因此,可以想到并查集包括两个基本操作:
- Get:查询一个元素属于哪个集合
- Merge:把两个集合合并成一个集合
具有非常明显的传递关系的题目,或者说并查集擅长动态维护许多具有传递性的关系,能在无向图中维护节点之间的连通性会用到并查集。(下图为一道并查集经典例题配图,判断是否有环状结构)
3.7 树状数组
什么是树状数组?顾名思义,就是用数组来模拟树形结构。
什么问题需要用到树状数组?大部分基于区间上的更新以及求和问题。
4.搜索
4.1 深度优先搜索(DFS)
深搜是一种与递归和栈紧密结合的算法。顾名思义就是按照深度优先搜索的顺序对“问题状态空间”进行搜索的算法。使用递归实现的指数型、排列型和组合型枚举。
4.2 广度优先搜索(BFS)
广搜又称为宽度优先搜索,是一种与队列紧密结合的算法。
5.动态规划
动态规划对状态空间的遍历构成一张有向无环图,遍历顺序就是该有向无环图的一个拓扑序。有向无环图中的节点对应问题中的“状态”,图中的边则对应状态转移的“转移”,转移的选取就是动态规划中的“决策”。在很多情况下,动态规划用于解决最优化问题。
“状态”“阶段”和“决策”是构成动态规划算法的三要素,而“子问题重叠性”“无后效性”和“最优子结构”是问题能用动态规划求解的三个基本条件。
6.图论
图论涉及的内容广泛,形象,实际,知识体系综合性较强,还融合了之前学习的各类算法与数据结构。个人认为并非三言两语就可以解释清楚一些基本定义与概念的,故而不再详细展开谈。图论的应用价值非常高,一直是程序设计竞赛的重要关注点。
7.学习总结
上述也仅仅列出了一些常用算法,只能算是冰山一角,个人认为算法并无难易,刚开始学并查集和树状数组的时候,我学了五天并查集,同一节课的视频仔仔细细看了三遍,但还是觉得有的点很难理解(因为当时正是算法集训时期,我负责并查集与树状数组方面的授课),有一种自己依葫芦画瓢能写出来,但给他人讲解的话还是很含糊不清,但是当我思考了足够多次,很多问题就迎刃而解了。现在再回看备课是做过的例题以及看过的视频,就会觉得很好理解。因此,我觉得算法和很多东西一样,入门难,只要坚持不懈,总有完全理解的一天。虽然本蒟蒻至今都不算完全入门,和校内很多大佬还有很大差距,但相信坚持一定会有收获。
算法这项学习一定需要大量刷题和思考,必要时候需要借助他人力量,有很多自己一直不理解的点,可以尝试去问问大佬们,他们总是会给出一些让我们觉得非常精妙的方法。
希望大家能够愿意尝试接触算法,了解算法,学习算法,即使能够坚持学习算法的人少之又少,但坚持的过程本身就是一种磨练。希望大家都能够收获绞劲脑汁AC时的自豪,看懂大佬题解时对其绝妙算法的赞叹!