算法/算法启蒙

算法/算法启蒙

算法

写在前面:“学好程序与算法,走遍天下都不怕。”


枚举

  • 背景
    • 找不到合适的数学公式和技巧
    • (改良后)枚举复杂度不是特别大
    • 通常用于找到一种情况使之满足题意的题目
    • 配合假设法找到目标情形:假币问题
  • 技巧
    • 跳跃枚举法:跳过对没有必要的情况的枚举
    • 局部枚举法:枚举局部,剩下的由该局部确定。例如熄灯问题

递归

  • 作用
    • 替代多重循环,如:n皇后问题。
      • 这种类型往往要运用到一个全局/静态变量来存储前面算过的结果,譬如n皇后就用到了一个全局数组来保存每一行的皇后拜访情况。全局/静态变量的好处就在于所有递归函数共享成果,就像递推迭代一样,每一步会影响下一步。
      • 递归函数形式:T function( T f(n) ),函数意义:在前n-1步已经完成的情况下决定如何走第n步,往往第一个被调用的function参数为0或1(然后依次调用 11 ~ n0n_0
    • 解决实质是递归形式的问题
      • 有些问题本身就是递归定义的,比如不少表达式就是递归定义的:逆波兰,四则运算。逆波兰直接递归调用自身定义,四则运算则包含项,因子和表达式自身等多个概念,是一种间接递归调用自身定义
      • 函数,数列的递推公式
      • 关键是搞清楚问题是怎样递归定义的,可以借助画图,写代数式的办法捋清楚。
    • 将问题分解为规模更小的子问题来求解
      • 如何来分解?
      • “n=1+(n-1)”法
        • 比方说要解决一个规模为n的问题,先找到解决该问题的第一步怎么做,然后再把剩下的问题解决,剩下的问题规模刚好是n-1且解决过程自相似,可以用上递归n-1。e.g.台阶问题
      • “n=(n-1)+1”法
        • 先解决n-1问题,再将最后一步完善,e.g.汉诺塔问题
      • 与多重循环不同,该方法第一个调用的function参数往往时n0总规模
      • 与分治不同,分治往往偏向于均分,而且多了一步综合,不过分治与递归又可以相互补充

附注

  1. atof()函数,将浮点串转变为浮点数
  2. cin.peek()函数,提前预知输入而非读取
  3. 浮点数的比较引入eps

二分

  • 简介
    • 对一个待求系统(通常为有序系统),每次都均分为两半,通过判断“砍掉”其中“无用”的一半,对剩下的一半用同样的方法处理,直到得出结论。
  • 作用
    • 二分查找
      • 不仅限于查找某一个具体的数,还可以查找符合某种要求的数(通常满足一定的大小关系)
    • 二分法求方程根

分治

  • 基本思想
    • 将一个问题拆分成两个或两个以上规模更小的问题,然后将小问题分别解决或只解决部分问题,最后综合处理一次。
  • 一般模式:分划,局部处理,综合处理(分治 | 归并
    • 常常与递归思想结合
  • 作用:使规模缩小,提高算法效率(想想:不断地递归并分治,使得规模不断二分)
  • 应用举例:基于分治策略的快速排序和归并排序

附注

  1. " x & 1 " 表达式判别x奇偶性
  2. 快速幂算法

动态规划

  • 背景
    • 问题具有最优子结构
      • 问题的最优解所包含的子问题的解也是最优的
    • 问题具有无后效性
      • 当前的若干个状态值一旦确定,则此后过程的演变就只和这若干个状态的值有关,和之前是采取何种手段或经过哪条路径演变到当前的这若干个状态,没有关系。
    • 单纯的递归会导致大量子问题 重复计算
  • 思路方法
    1. 原问题分解为子问题
      • 一些问题的求解归结于它的子问题的求解,且子问题与原问题类似,只是规模减小。
      • 子问题一旦解决即被保存(通常存入一个多维数组)。
    2. 确定状态
      • “状态”简介:在用动态规划解题时,我们往往将和子问题相 关的各个变量的一组取值,称之为一个“状态”。一个“状态”对应于一个或多个子问题, 所谓某个“状态”下的“”,就是这个“状态”所对应的子问题的解
      • 状态空间与时间复杂度:整个问题的时间复杂度是状态数目乘以计算每个状态所需时间。(在数字三角形里每个“状态”只需要经过一次,且在每个状态上作计算所花的时间都是和N无关的常数。)
      • 用动态规划解题,经常碰到的情况是,K个整型变量能 构成一个状态(如数字三角形中的行号和列号这两个变量 构成“状态”)。如果这K个整型变量的取值范围分别是 N1, N2, ……Nk,那么,我们就可以用一个K维的数组 array[N1] [N2]……[Nk]来存储各个状态的“值”。这个 “值”未必就是一个整数或浮点数,可能是需要一个结构 才能表示的,那么array就可以是一个结构数组。一个 “状态”下的“值”通常会是一个或多个子问题的解。
    3. 确定一些初始状态(边界状态)的值
    4. 确定状态转移方程
      • 将第一步 分解 得到的原问题与子问题的关系用数学符号语言表述出来,即实现状态之间的转移关系。
  • 动规程序写法
    • 记忆递归型
      • 递归函数+记忆数组
    • “人人为我”递推型
      • 1,2,3,…,n-1 => n
      • 递推到“n”时“n”仍未被求出,前面已被求出的状态值用于求“n”的状态值
    • “我为人人”递推型
      • n => k (k>n)
      • 递推到“n”时“n”已经被求出,n将用于求后面的状态值
递归写法 递推写法
难度 直观简便 较复杂,可能需要结合图形理解
内存占用 较大,有爆栈风险 小,还可以用滚动数组进一步节省空间

动规中递归法向递推法转化的一般方法:

  • 递归函数有n个参数,就定义一个n维的数组,数组的下标是递归函数参数的取值范围,数组元素的值 是递归函数的返回值,这样就可以从边界值开始, 逐步填充数组,相当于计算递归函数值的逆过程。

常见分解(状态转移)方法归纳

  • 多分类讨论, 想想解决原问题等同于解决什么和什么。有时候要经过多层分解才能够得到与原问题结构相同的子问题。
  • “n=(n-1)+1”型与"n=1+(n-1)"型(与 递归先走一步 思想又异曲同工之妙,n为问题规模)
  • "F(i,j,k)=F(i-1,j,k)+F(i,j-1,k)+F(i,j,k-1)"型(这里拿 三维 的情况举例,其他维度的状态转移方程与此大同小异)
  • "F(m,n)=A, A=G( F(m-1,n),F(m,n-1) )"型,间接递归

附注

  1. 数字三角形题目启示录:
    空间优化滚动数组(通过覆盖今后无用的旧有数据空间的方法来压缩空间),降维,关注不必要的存储空间以及运行过程中变得可以丢弃的数据。②递归化递推:逆向思维。

深度优先搜索

关键词:回溯 标记(判重) 剪枝 状态

  • 简介
    • 从某个起点开始每走一步就做一个标记,然后下一步随便选择一个没有走过的节点,走不通则回退到上一步重新选择。这种走法总是试图“走的更远”。
  • 重要概念
    • (连通图,非连通图,子图,极大连通子图…)
    • 图上节点(或者某抽象的状态:e.g.譬如每个棋局也可以看作是一个图节点)
    • 图上:节点之间的联系
    • 图路径(枝)
  • 图的表示
    • 邻接矩阵L[i][j] 用一个二维数组(元素可以是一个结构,存储诸如 连通与否 路径长度 权值 方向 等内容)表示节点i与节点j之间的联系。遍历复杂度:O(n^2),n表示节点 。
    • 邻接表S[k] 用一个一维数组(元素可以是一个结构,存储 邻接节点 以及诸如 路径长度 权值 方向 等内容)表示所有与节点k有关的边的信息。遍历复杂度:O(n+e),n表示节点数,e表示边数。
      • 当e特别大而接近n^2时,邻接表就失去了优势变得和邻接矩阵差不多了。
    • 其他表示方法:具体问题具体分析。
  • 剪枝
    • 可行性剪枝:每搜索一个节点后发现不满足题目要求则直接回溯防止沿着这条路径继续错误地走下去。
    • 最优性剪枝:每搜索一个节点后都对当前路径的最优性进行检验,若当前以及可以判明不是最优路径或者说接着走下去一定不是最优路径则直接回溯。
      • 与整体最优解比较来剪枝
      • 保存中间结果用于最优性剪枝 :如果到达某个状态A时,发现前面曾经也到达过A,且前面那次到达A所花代价更少,则剪枝。这要求保存到达状态A的到目前为止的最少代价。 ( 对每个节点都开辟存储空间来存放以该节点为终点的当前最优解,每次搜索到一个节点就将该最优值与新值比较,原值更优则剪枝,新值更优则更新该最优值)
      • 注意:“最优”的决定要素可能不止一项,还可能是多元因素,如:ROADS问题中有费用和路程两个因素。剪枝时要考虑控制单一变量。

广度优先搜索

  • 简介
    • 从节点开始层次遍历(用队列)整个图。
  • 特点
    • 搜到的路径一定是最短的。
    • 占用空间较大,尤其是目标节点层次很高时。
    • 如果题目要求路径则每次入队的元素都要包含有指向父节点的“指针”。要注意,STL中的queue容器中元素一旦出队则会导致对象的丢失,因此需要自己动手编写一个保证父节点不会丢失的队列。
  • 一般模式
    广度优先搜索算法如下:(用QUEUE)
    1. 把初始节点S0放入Open表中
    2. 如果Open表为空,则问题无解,失败退出
    3. 把Open表的第一个节点取出放入Closed表,并记该节点为n
    4. 考察节点n是否为目标节点。若是,则 得到问题的解,成功退出
    5. 若节点n不可扩展,则转第(2)步
    6. 扩展节点n,将其不在Closed表和
      Open表中的子节点(判重)放入Open表的尾 部,如有必要为每一个子节点设置指向父节点的指针 (或记录节点的层次),然后转第(2)步。

DSP vs BSP

深搜 广搜
适用范围 几乎任何问题 状态表示比较简单,求最优策略的问题
优点 空间占用较少 找到的解一定路径最短
缺点 运气不好时耗时长,甚至无法在多项式时间内求解,需要剪枝优化 目标节点层次高时搜索过程将产生许多无用节点,搜索效率低且占用空间大,需要优化存储方式
结构 栈(递归) 队列

根据题目要求凭借自己的经验和对两个搜索的熟练程度做出选择

贪心

  • 简介
    • 只考虑当前最优,通过局部最优达到整体最优的策略
    • 贪心算法需要证明其正确性
  • 贪心检验:替换法
    • 假设最优序列为{ai},贪心得到的序列为{bi},则只需要证明{ai}={bi}即可证明贪心得到的是整体最优。
    • 必要时先将ai按照bi(贪心策略得到的序列)的方式排好序
    • 然后从i=1开始逐个考虑能否将ai替换为bi而不影响ai后面序列的选择。如果对于所有i均可替换,则{ai}={bi}。
    • 数学归纳法

附注
一类常见的应用贪心策略的题:区间题
解决思路:关注端点,合理排序,顺序处理