算法策略
贪心算法
一个贪心算法总是做出当前最好的选择,也就是说,它期望通过局部最优选择从而得到全局最优的解决方案。
- 做出选择 不可反悔
- 可能得不到最优解
- 贪心策略决定算法好坏
确定贪心策略 选择当前看上去最好的方案
一步一步得到局部最优解
将所有局部最优解合并称为一个原来问题的最优解
- 冒泡排序就使用了贪心算法
原则
贪心原则
- 所谓贪心选择性质是指原问题的整体最优解可以通过一系列局部最优的选择得到
最优子结构
- 当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质
背包问题
选择一个最好的贪心策略
物品可分割称为背包问题 物品不可分割称为0-1背包问题
- 会议安排问题
- 最短路径问题
- 哈夫曼编码
- 最小生成树
递推法
由当前问题的逐步解决从而得到整个问题的解,只是依赖的是信息间本身的递推关系
递归法
解规模为N的问题,设法将它分解成规模较小的问题,然后从这些小问题的解方便地构造出大问题的解,并且这些规模较小的问题也能采用同样的分解和综合方法
特别地,当规模N=1时,能直接得解
写递归代码最关键的是写出递推公式,找到终止条件。实现递归代码要注意的两个问题:调用栈溢出与重复计算
暴力递归有着十分明显的缺陷,有剪枝与优化这两种方法:
- 利用预设条件减少搜索路径,优化最优组合搜索方案
- 利用重叠子问题,避免重叠子问题的计算,如使用备忘录
重叠子问题的处理模式:
当目标为 $x$,求最优解 $f(x)$
数值函数 $g(t)$。如果求最小值,那么 $g$ 是 min,如果求最大值,那么 $g$ 就是 max
假定当前得到子问题的参数为 $c$,得到后续子问题的函数是 $s$,那么这个函数就是 $s(x, c)$
函数 $v(x)$,该函数可以聚合当前参数 $c$ 和当前子问题的结果
叠加函数 $h(x)$ 定义了每一步如何与子问题进行叠加
$$f(x)=h(g(v(f(s(x,c)),c)))$$
枚举法
对所有可能的解逐一尝试,从而找出问题的真正解
这就要求所解的问题可能的解是有限的、固定的,不会产生组合爆炸、容易枚举的
分治法
其本质就是将一个大规模的问题分解为若干个规模较小的相同子问题,分而治之, 递归地求解问题
每层递归应用:
- 分解当前问题为子问题
- 递归解决分解的子问题
- 合并子问题的解
对于递归的问题,当不需要再继续递归分解子问题时,就是一个基本情况,此时可直接得出解,否则就是递归情况
递归式则可以用来描述复杂度,如归并排序的递归式
O(1) 若 n = 12T(n/2) + O(n) 若 n > 1
分治算法要素
- 原问题可以分解为若干个规模较小的相同子问题
- 子问题相互独立
- 子问题解可以合并为原问题的解
典型算法
- 二分查找
- 归并排序
- 快速排序
- 大整数乘法
动态规划
贪心算法根据局部最优解得到全局次优解,为了得到全局最优解,可以在贪心算法中引入回溯的过程,通过暴力的方式递归枚举出所有组合找到最优解,递归枚举的过程中,有些大问题是由小问题的解组成的,这部分就可以进行优化,从而引入动态规划
通过子问题的最优解,推导出最终问题的最优解,把这些子问题之间的转移称为状态转移,并把用于刻画这些状态转移的表达式称为状态转移方程
动态规划的思想类似于分治法,不过动态规划是从最小子问题求起 将小问题的解存储起来,求解大问题时 如果需要用到小问题的解 直接使用即可 无需再重复计算
- 待备忘的自顶向下法:当对一个子问题进行求解时,会判断这个子问题是否已经求过解,如果是直接返回,否则进行计算并放入缓存
- 自底向上法:求解一个问题时,其依赖的子问题都已经被求解完毕。一种做法是根据问题写出状态转移方程,根据状态转移方程求解每一阶段的解
状态转移方程,为了写出它,需要按照套路找出以下项目:
- 初始化状态:由于动态规划是根据已经计算好的子问题推广到更大问题上去的,因此需要一个“原点”作为计算的开端
- 状态:找出子问题与原问题之间会发生变化的变量,这个变量就是状态转移方程中的参数
- 决策:改变状态,让状态不断逼近初始化状态的操作。这个决策,就是状态转移方程状态转移的方向
$$f(x)=\left{\begin{array}{c}d(x),x\in V_I\g({v(f(s(x,c)),c)}),c\in values(x)\end{array}\right.$$
- 如果一个 $x$ 不用再划分子问题,就可以直接返回
- 否则需要从可选组合 $values$ 中取出用于划分子问题的备选值,$values$ 可能会随着 $x$ 变化而变化
- 对每一个备选值 $c$ ,通过函数 $s(x,c)$ 求得当前备选值的子问题的 $x$, $c$。然后,通过 $f(s(x,c))$ 得到这个子问题的结果
- 接下来通过子问题 $v(f(s(x,c)),c)$ 的结果和当前备选值 $c$,来求得当前问题的解
- 最后通过最优化函数 $g(t)$ 进行求解最优值
要素
- 最优子结构:问题的最优解包含其子问题的最优解,子问题之间相互独立
- 无后效性:在推导后面阶段的状态的时候,只关心前面阶段的状态值,不关心这个状态是怎么一步一步推导出来的,同时某阶段状态一旦确定,就不受之后阶段的决策影响
- 子问题重叠:存在重复计算现象。不是必要条件 但是是动态规划的优势
典型算法
- 兔子序列
- 最长公共子串
回溯法
回溯法是一种选优搜索法,按照选优条件深度优先搜索,以达到目标。当搜索到某一步时,发现原先选择并不是最优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术称为回溯法,而满足回溯条件的某个状态称为“回溯点”。
要素
- 解空间
- 由所有可能解组成的空间
- 将这些解按一定结构组织起来:解空间树
- 隐约束
- 不满足隐元素的分支无需搜索 直接剪掉 也称为剪枝函数
典型算法
- 0-1背包
- 最大图
- 地图着色
- n皇后
分支限界法
- 广度优先
回溯法找出所有解
分支限界法找出一个接
回溯法深度优先分支限界法广度优先
回溯法搜索一次生成一个孩子节点分支限界法一次生成所有节点
线性规划网络流
解决
- 确定决策变量
- 哪些变量对模板有影响
- 确定目标函数
- 含有决策变量的线性函数
- 找出约束条件
- 将对决策变量的约束表示为方程或者不等式
- 求最优解
算法思想
- 灵光一现的算法,通常比较独特且不易想到,但却能以非常简洁的代码实现并且达到出色的性能表现
数据结构决定程序
使用更合适的数据结构能减少代码量
正确的程序
- 断言
- 测试
性能分析
- 问题定义
- 系统结构
- 算法与数据结构
- 代码调优
- 系统软件
- 硬件
估算
使用小实验获取关键参数
任何事都应尽量简单 但不宜过于简单
算法设计
- 保存状态 避免重复计算
- 动态规划
- 对信息进行预处理至数据结构中
- 分治算法
- 扫描算法
- 还是动态规划
代码调优
- 不成熟的优化是大量编程灾害的根源
- 需要有一个度量工具
- 注意代码调优是一把双刃剑
一些优化方案:
- 缓存
- 等价的代数表达式
- 内联函数避免函数调用开销
- 循环展开
- 修改数据结构
节省空间
- 调整数据结构
- 使用计算代替存储
- 稀疏数据结构
- 关键字索引
- 数据压缩
- 动态废品
- 垃圾回收
自描述数据
- KV对
- 注释文档
- 源代码描述
技巧
- 明白用户的真实需求
- 评估成本与收益
- 正确评估问题的难度
- 正确的工具