ACM 学习心得

ACM 学习心得

STL:完美的艺术品

STL 由四大部分组成:算法、容器、迭代器、仿函数。

算法(algorithm)

算法定义了一组与实现无关的操作,也是 ACM 学习的核心。C++ 算法库的内容全都是一些比较基本的算法,包括移动、转换、遍历、删除、过滤等等。C++ 算法库本身是基于抽象的,在迭代器的抽象下,使得这些算法可以在不同结构的容器中重用。一个比较坑的地方就是我高中的时候学完 C++ 之后报名了 NOIP。那一年刚刚允许用 STL(之前一直不准用),然后我对于标准库的依赖很严重,连快排都不会写,后来才慢慢的把基础补上去。

容器(Container)

容器库中包含了大量的基本数据结构与基本对象:数组、位容器、链表、双端队列、向量、集合、映射、元组、优先队列以及两种适配器。这些基本的数据结构可以在编程过程中大大简化程序员的工作,同时这些容器使用起来十分的方便,也可以与算法库无缝的结合。

迭代器(iterator)

标准模版库中有一层很重要的抽象:迭代器。迭代器通过大量的运算符重载使得本身被适配成指针的模式。方便便利与操作容器的内部,同时保证容器的一致性。

仿函数(functor)

最后一组是仿函数,也是 STL 中最容易被忽视的一组。greater、less、equal_to、plus等都是仿函数。仿函数也被称为函子,这一类对象有一个极为特殊的特性,重载了 operator() 运算符。这一技术使得对象变的可调用,表现的就像函数一样,但与函数不同的就是它的内部可以保存状态。是高阶函数与函数柯里化的一个简单的实现。这种实现可以让「函数」变成一等对象,在程序中创建、传递。经常被于谓词之类的方面,以提高程序的可读性与美观性。但是 ACM 中很少使用。

贪心算法:往往没有那么好贪

一开始感觉贪心算法是最无脑的算法了,很简单也很好理解,排个序然后排头找就可以了。后来做一些题,觉得贪心要是难起来也是相当恶心。

什么时候用贪心呢:最优子结构。能用贪心的情况下,局部最优解一定是包含在最优解里的,这样每一步都可以在找到局部最优解之后「不负责任的」抛弃其他情况。由于这个特点,所以贪心不能回溯,不能记忆。问题的关键是有很多问题是没有办法一眼瞅出来到底是贪心还是动态规划,这就需要慢慢的证明了。

实际上即便是确定了用动态规划去解,贪心也可以做一个简单的估计,来初步的拟合结果。当时刚刚学 DP 的时候,做一道题,是有关于分书的,然后我就是用的一种修正过的贪心,发现除去一些特别的数据,结果和 DP 是一样的。如果选取合适的策略,贪心可以为最优解拟合出一个很满意的边界。当然在 ACM 中没有这么干的,但是在实际有用的应用,能确定一个大致的边界有时就已经够了。中比赛骗分最常见的手法就是贪心骗 DP。不过往往要想一个比较高级的贪心。

很多经典问题都是贪心思想:霍夫曼树、最小生成树、最短路等。但是我个人感觉其实贪心并没有那么简单,RQNOJ 上面有很多特别恶心的贪心,有些的思想难度不亚于 DP,要「小心的贪,谨慎的贪」。

贪心有这几个重要的部分:候选集、解集、贪心策略、可行函数、解决函数。伪代码如下:

    while(解决函数 == false && 候选集.hasElement) {
        var 元素 = 候选集.getOne()
        if (元素可行) {
            加入解集
        }
    }

搜索:这个也能搜,那个也能搜,统统都能搜

当时我们那届,省队有一个「搜索帝」。特别厉害,遇到不会做的题就用搜索,然后一路从 NOIP 搜到了 NOI…… 然后搜索和 DP 这两个在数学领域完全不沾边的概念,在计算机里却非常相似,DP 有的时候很像记忆化搜索。所以没掌握好搜索的我连带着 DP 一块跪了_(:3」∠)_

我搜索一直就迷迷糊糊的,只明白了图上的搜索,隐式图的搜索就不行了。

广度优先搜索(BFS)

广度优先搜索还是比较好理解的。但缺点是会爆堆内存。

首先队列中有一个顶点。顶点出队,然后相邻的未访问的顶点入队。检查是否存在可行解,如果没有则重复直到队列为空。BFS 用类似于扩散的方式来实现点的寻找,在解树上的表现是每次扩展都有下一层的节点被访问。

伪代码:

队列 Q 加入顶点
while (Q 不为空) {
    var 顶点 = Q.pop_front()
    for(顶点相邻的未访问顶点) {
        if (是目标顶点) {
            break
        }
        标记为访问
        入队
    }
}

深度优先搜索(DFS)

深度优先搜索就有点麻烦,主要的原因是大部分实现是递归的实现,一开始接触的时候很容易晕。DFS的一个坑就是会爆栈。所以有时候会手动模拟。首先访问顶点,把一个与顶点相邻的未访问的顶点作为顶点去访问,重复直到找到可行解或者全部被访问。在解树上的表现是每次都是一下子走到底再回头。

伪代码:

访问顶点
标记访问
for(顶点相邻的未访问顶点) {
    重复
}

深度优先搜索的内存增长很慢,每次都只处理一个节点,但是简单的递归写法存在调用栈溢出的风险,在高压力的情况下要手动模拟。而广度优先搜索内存增长非常快,如果情况比较密集,就很容易溢出。

深度优先搜索往往用来确认是否存在最优解,广度优先搜索更适合于寻找最优解。

除此之外还有一些变体,比如:迭代深化深度优先搜索(IDDFS)等价于广度优先,但是逐步放松深度限制。这种变体使得内存消耗较为缓慢,在相邻状态极为庞大而最优解相对简单的情况先非常好用。当目标解已经确定的,希望找到路线的时候,双向广度优先搜索就非常棒了,可以很好的减轻内存压力。思路是从起点与终点同时进行广度优先搜索,发现公共点就停止。迭代深化深度优先搜索在 ACM 中应用较少,因为很少遇到这么大的解集。

动态规划:明白是明白,但就是不会写

继搜索之后,第二个头大的就是动态规划。主要是这个东西最奇怪的一点就是:这个题我不会做,看了答案之后知道这么做对,但是把答案拿走之后又不会了…… 不过在老师刷题的时候手下留情多了。很多题目都是比较简单的递推基本上方程一眼就能看出来。高中学的时候老师说这个是最难理解的,考试的时候也特别能拉开分数。必须要刷很多的题目才能明白,我就属于当时偷工减料的那种……

动态规划有一个特点是局部最优解不一定是整体最优解,这是与贪心算法很大的区别。原来局部的最优解可能在以后的决策当中被舍弃。很多问题动态规划看起来都能用贪心去解,但是数据一变就会发现贪心算法不行。感觉这的地方还是要看人,像我这样的基本上看到一个动态规划都感觉是贪心算法,然后一会写完结果 WA。有些细心的人会先想一想然后再做。

动态规划应该是算法比赛里面最神秘的一个领域了,会的人感觉很简单,随便做一个题一眼就能写出公式,不会的怎么看都看不懂。网上有很多大神写了很好的文章,有的人在理解的时候把 DP 抽象成状态机,但是我感觉这样思想更难理解,因为状态的转移的动机变得很奇怪。原来状态转移是由环境决定的,而现在的状态转移是由节点本身的性质与策略所推动的。

动态规划最难的地方是状态的定义(个人感觉),当时奥赛组的大神说过,只要定义好状态,其他的就没有什么难度。定义状态的过程其实就是在拆分问题。问题拆分好了之后状态方程就水到渠成了(但是我往往连状态都定义不出来……)。往往一开始就迷失在如何定义状态了(老师OS:就是做题做少了)。

更加「迷人」的一个东西就是如同玄学一般的「无后效性」。恶心的是对于同样的一个问题,不同的状态的定义有写是由后效性的,有些是无后效性的。所以如果不定义好状态的话,往后会非常困难。

动态规划有这几个概念:状态决策,阶段,状态转移(通过这些名词我们可以进一步把动态规划复杂化……)

数字三角形是我第一个接触的动态规划,个人感觉这个比背包要好理解,而且可以把图画出来,状态也很好确定。整个题下来一直是很直观的。

01背包是一般动态规划上来就会讲到的例子,背包一族的题目都是围绕着这个类型展开的。然后是就算动态规划全部忘了也不会忘记的方程:

f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}

01背包讲完之后会使用滚动数组的技术来压缩空间,注意 V 是逆序枚举的。

f[v]=max{f[v],f[v-c[i]]+w[i]}

最长不下降子序列是一个一维状态的动态规划,看懂背包之后做这个题又懵了:「这也是动态规划」?

d[i] = max{1, d[j] + 1}

最后一部分是到底什么时候用搜索,什么时候用动态规划,这里直接贴一段我从网上看到的:首先,可以从数据范围中得到命题人意图的线索。如果一个背包问题可以用动态规划解,V一定不能很大,否则 O(VN) 的算法无法承受,而一般的搜索解法都是仅与 N 有关,与 V 无关的。所以,V 很大时(例如上百万),命题人的意图就应该是考察搜索。另一方面,N 较大时(例如上百),命题人的意图就很有可能是考察动态规划了。

另外,当想不出合适的动态规划算法时,就只能用搜索了。例如看到一个从未见过的背包中物品的限制条件,无法想出动态规划的方程,只好写搜索以谋求一定的分数了。

图论:最有趣也是最杂的

图论是我最喜欢的部分,因为这个能画出来 (′?‵)。图论是离散数学的范畴,但是高中刚刚接触的时候并不知道有离散数学这一门课,单纯感觉很有趣:那时候的数学都是坐标系,几何体之类的,而图论本身对于空间没有任何要求。再就是相比较其他数学分支,图论用计算机语言太好描述了。

图论比起其他的部分显得很杂,有很多方面要去学。遍历、拓扑排序、最短路,二分图。在往上就是网络流之类的。图论的算法特别杂而且每一个都要好几种……

最小生成树:最小生成树具有 MST 性质,这一点使得最小生成树可以用贪心去解。最小生成树有两种解法,第一种是 Prim、第二种是 Kruskal 算法。

Prim

按照边去找点,算法的复杂度仅仅与边有关,适合稠密图,复杂度O(v^2)

1. 初始化

2. 输出顶点 V0,将顶点 0 加入集合 U 中

3. 重复以下操作

3.1 选取最短边与对应的邻接点编号

3.2 输出顶点的编号与对应的权值

3.3 顶点 k 加入 集合 U

3.4 松弛

Kruskal

按照边去贪心,注意每次加入的边不能出现环,适合稀疏图,复杂度 O(e lg e)。

1. 初始化 U = T; TE = {}

2. 重复直到 T 的联通分量为 1

2.1 在 E 中寻找最短边 (u, v)

2.2 如果 (u, v) 分别位于两个不同的联通分量

2.2.1 (u, v) 加入 TE

2.2.2 合并连个联通分量

2.3 E 中标记 (u, v) 无效

这个图是我模拟出来的,上面的是保持联通的边界,在边界之上图就回出现多个联通分量,斜线的斜率越低说明 V/E 越高,也就意味着图越密集。而最下面那条的曲线之下是 Prim 算法的可行区域。

我初步拟合了一下,在 V < 20 以内时候, V/E < 0.2 的时候就才能用 Prim;在 V < 100 的时候 V/ E < 0.0075 才能用 Prim。也就是说,所谓的 「稀疏图」的范围其实并没有那么「稀疏」,大部分情况下都可以用 Kruskal,只有当边数是点数的几十倍以上的时候再考虑 Prim。(更关键的是 Prim 更难写)

最短路也是两种比较常用:Dijkstra 和 Floyd 分别对应的单源最短路和图上所有的最短路。

Dijkstra

核心很简单,就是一个松弛操作:

dist[i] = min {dist[i], dist[k] + G[k][i]}

1.  初始化数组 dist, path, s;
2. 当 s < n 时,重复
    2.1 dist[n] 最小值的编号为 k
    2.2 k 加入 s
    2.3 松弛 dist

Dijkstra 可以使用优先队列优化。

Floyd

更简单,直接一个三重循环内部松弛就出来了。

可以看出来松弛在图论里是经常使用的一种模式,

时间: 2024-10-12 04:32:00

ACM 学习心得的相关文章

Linux系统理解以及Linux系统学习心得

原创作品转载请注明出处  <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 作者:严哲璟 说一下我对Linux系统的理解 1.加载Linux内核准备:在加载基本输入输出模块(BIOS)之后,从磁盘的引导扇区读入操作系统的代码文件块到内存中,之后开始整个系统的初始化. 2.main.c的start_kernel函数是整个操作系统的入口,这也与Linux是基于C语言的特性相符,start_kernel具体做的动作很多

我的MYSQL学习心得(八)

我的MYSQL学习心得(八) 我的MYSQL学习心得(一) 我的MYSQL学习心得(二) 我的MYSQL学习心得(三) 我的MYSQL学习心得(四) 我的MYSQL学习心得(五) 我的MYSQL学习心得(六) 我的MYSQL学习心得(七) 这一篇<我的MYSQL学习心得(七)>将会讲解MYSQL的插入.更新和删除语句 同样的,只会讲解跟SQLSERVER不同的地方 插入 将多行查询结果插入到表中 语法 INSERT INTO table_name1(column_list1) SELECT (

我的MYSQL学习心得(一)

我的MYSQL学习心得(一) 使用MYSQL有一段时间了,由于公司使用SQLSERVER和MYSQL,而且服务器数量和数据库数量都比较多 管理起来比较吃力,在学习MYSQL期间我一直跟SQLSERVER进行对比 第一期主要是学习MYSQL的基本语法,陆续还有第二.第三.第四期,大家敬请期待o(∩_∩)o 语法的差异 我这里主要说语法的不同 1.默认约束 区别:mysql里面DEFAULT关键字后面是不用加括号的 --sqlserver CREATE TABLE emp ( id INT DEFA

在马哥linux运维学院学习心得

题目:在马哥linux运维学院学习心得 姓名:谭龙 班级:M18 学号:26 时间:2016-02-29--2016-06-02(正常毕业时间预计在7月中上旬)   正文: 个人基本情况: 我是一名在校的即将毕业的大四学生,毕业时间为2016.7.专业为矿物加工工程专业,纯正的四川-广安人(邓小平故居就在那).因找不到工作,加上自己也不知道干什么,在堂弟的推荐下,来参加了马哥linux运维学院的学习:怀揣着一颗对计算机懵懂的心,开始涉足从未接触过了linux. 个人收获与心理变化: 在一开始接触

第一篇大数据学习心得

之前未习惯发布学习心德博文,后续会采用这种方式发布学习心得,希望能够很好的督促自己. 计划会按scala,Hadoop,Spark的顺序去学习. 刚学scala的时候,眼前一亮,这语法跟python,java很像啊,刚好两者很熟悉,偷笑,后面果然学的得心应手.今天就不发表具体的技术内容.反正王学林老师的视屏讲解很好,声音非常富有感染力,想开小差都比较难,呵呵,话语精炼,个人较喜欢的风格,这里说下这段时间学习scala的小心得?,视频学完一章紧接着进行敲代码,调试,最后记笔记,对,记笔记,不一定是

spring核心知识(学习心得)

直接进入主题,主要分为两大部分:框架学习心得和spring框架的核心知识. 学习心得 1.学习框架的时候,一定要弄清楚的几个问题: a. 这是一个什么框架 轻量级还是重量级, 侵入式还是非侵入式,是解决单个问题还是整体的解决方案. b. 框架的设计理念是什么(为了解决什么问题而出现) c. 框架的优缺点 d. 框架的架构是怎样的 e. 框架的核心是什么 f. 框架能实现哪些功能 在学习一个框架的时候如果都不知道它能够提供哪些功能,就更加不用谈功能实现和充分利用框架了 2. 在学习多个框架以后,如

C++用法的学习心得

c++这门课,在我刚进入大学的就已经开始接触了.因为自己的专业就是计算机科学,因此c++嘛,对于我来说还是比较重要的.不同于其他专业,一开始我接触就是c++了,跳过了c语言一类的课.就我自己认为,c++这课学起来还是很有难度的.大一上课的时候,老师就说过这课在生活中的应用很广泛.处于初学者的我,开始给我的感觉就是很是乏味枯燥,提不起兴趣.不过仔细想想自己的专业就是和它有关,就算将来自己不从事这个行业,还是很有学习它的必要.因为多一门技术总归是不会吃亏的. 作为男生嘛,自己没有少玩游戏.很多人玩游

Android学习心得(16) --- Dex文件结构实例解析(2)

我在博客上发表一些我的Android学习心得,希望对大家能有帮助. 这一篇我们讲述一下通过一个实例来分析dex文件结构和组成. 参考Leb128数据类型 Android学习心得(5) --- dex数据类型LEB128 参考实例分析学习理解dex文件结构Android学习心得(15) --- Dex文件结构解析(1) 参考baksmali工具使用Android学习心得(4) --- MAC下smali文件编写与运行 1.编译 我们通过一个例子来分析dex文件的构成 创建一个Hello.java文

Android Window PhoneWindow Activity学习心得--第三弹

Android Window  PhoneWindow Activity学习心得--第三弹 前面 我们完成了从Activity到PhoneWindow的整体跨度 正如我们所知道的与Activity组件关联的一个应用程序窗口视图对象关联一个ViewRoot对象,而将 一个Activity组件的应用程序窗口视图对象与一个ViewRoot对象关联是通过该Activity组件所使用的 窗口管理器(WindowManager)来执行的. 在我们初始化DecorView完成之后,我们需要关联应用程序窗口视图