[复习笔记] 算法分析与设计

1. 分治算法

1.1 理论梳理

先看分治这一部分

1.1.1 适用情况

分治法所能解决的问题一般具有以下几个特征:

  1. 该问题的规模缩小到一定的程度就可以容易地解决
  2. 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。
  3. 利用该问题分解出的子问题的解可以合并为该问题的解;
  4. 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。

第一条特征是绝大多数问题都可以满足的,因为问题的计算复杂性一般是随着问题规模的增加而增加;

第二条特征是应用分治法的前提它也是大多数问题可以满足的,此特征反映了递归思想的应用;、

第三条特征是关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条特征,而不具备第三条特征,则可以考虑用贪心法或动态规划法。

第四条特征涉及到分治法的效率,如果各子问题是不独立的则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用动态规划法较好。

1.1.2 基本步骤

step1 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题;
step2 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题
step3 合并:将各个子问题的解合并为原问题的解。

1.1.3 复杂性分析

一个分治法将规模为n的问题分成k个规模为n/m的子问题去解。设分解阀值n0=1,且adhoc解规模为1的问题耗费1个单位时间。再设将原问题分解为k个子问题以及用merge将k个子问题的解合并为原问题的解需用f(n)个单位时间。用T(n)表示该分治法解规模为|P|=n的问题所需的计算时间,则有:

T(n)= k T(n/m)+f(n)

1.2 算法实例

1.2.1 快速排序

坐在马桶上看算法:快速排序:
对于序列a, 先任意找一个基准数, 然后利用上文中的哨兵方法把比基准数小的数移动到基准数左边, 把比基准数大的数移动到基准数右边, 然后对基准数左右的两个子序列重复这个过程.

复杂度: 快速排序的最差时间复杂度和冒泡排序是一样的都是O(N2),它的平均时间复杂度为O(NlogN)

1.2.2 归并排序

白话经典算法系列之五 归并排序的实现

子问题: 要将两个有序数列合并,只要从比较二个数列的第一个数,谁小就先取谁,取了后就在对应数列中删除这个数。然后再进行比较,如果有数列为空,那直接将另一个数列的数据依次取出即可。

分解与合并: 归并排序的基本思路就是将数组分成二组A,B,如果这二组组内的数据都是有序的,那么就可以很方便的将这二组数据进行排序了.

下图源自:图解排序算法(四)之归并排序

复杂度:
设数列长为N,将数列分开成小数列一共要logN步,每步都是一个合并有序数列的过程,时间复杂度可以记为O(N),故一共为O(N*logN)。

1.2.3 二分搜索

这里写图片描述

复杂度: 时间复杂度无非就是while循环的次数:
总共有n个元素,渐渐跟下去就是n,n/2,n/4,….n/2^k(接下来操作元素的剩余个数),其中k就是循环的次数
由于你n/2^k取整后>=1
即令n/2^k=1
可得k=log2n,(是以2为底,n的对数)
所以时间复杂度可以表示O(h)=O(log2n)

1.2.4 汉诺塔

  • 当n=1时,a柱子只有一个圆盘,直接移至c柱
  • 当n>1时,根据规则1和2,将a柱子n-1个圆盘移动到b柱子,然后将a剩下的一个圆盘移动到c,接着再把b上暂时放着的n-1个圆盘移动到c
void Hanoi(int n, char a, char b, char c)
{
    if(n == 1)
    {
        Move(a, c);
    }
    else
    {
        Hanoi(n-1, a, c, b); /*将a柱子n-1个圆盘移动到b柱子*/
        Move(a, c);      /*将a剩下的一个圆盘移动到c*/
        Hanoi(n-1, b, a, c); /*再把b上暂时放着的n-1个圆盘移动到c*/
    }
}

void Move(char a, char b)
{
    printf("Move 1 disk: %c ---------> %c\n", a, b);
}

2. 动态规划

2.1 理论梳理

动态规划部分
动态规划把问题的求解过程变成一个多阶段的决策过程,每一步决策都将利用之前的决策结果

2.1.1 适用情况

  1. 最优子结构:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。

  2. 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。

  3. 有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)

性质3是动态规划与分治法在适用情景上的主要差别

2.1.2 基本步骤

  1. 问题建模,找出目标函数和约束条件
  2. 找出子问题的划分方法(子问题的边界),判断是否满足动态规划的基本要素
  3. 找出递推方程,明确目标函数与子问题间的依赖关系
  4. 找出最小子问题的优化方法(初值确定),从而明确递推方程的结束条件

2.2 算法实例

常见的动态规划问题分析与求解

2.2.1 硬币找零

假设有几种硬币,如1、3、5,并且数量无限。请找出能够组成某个数目的找零所使用最少的硬币数。

题解:用待找零的数值k描述子结构/状态,记作sum[k],其值为所需的最小硬币数。对于不同的硬币面值coin[0…n],有递推方程:
sum[k] = min(sum[k-coin[0]] , sum[k-coin[1]], ...)+1
边界值: if(k <0) sum[k] = +∞

注意: K越小, 所需的硬币数未必越少, 所以不能用扣去最大面额硬币的贪心策略来缩小问题规模.

2.2.2 字符串相似度/编辑距离

对于序列S和T,它们之间距离定义为:对二者其一进行几次以下的操作(1)删去一个字符;(2)插入一个字符;(3)改变一个字符。每进行一次操作,计数增加1。将S和T变为同一个字符串的最小计数即为它们的距离。给出相应算法。

解法: 将S和T的长度分别记为len(S)和len(T),并把S和T的距离记为m[len(S)][len(T)]
将S和T的长度分别记为len(S)和len(T),并把S和T的距离记为m[len(S)][len(T)],有以下几种情况:

如果末尾字符相同,那么m[len(S)][len(T)]=m[len(S)-1][len(T)-1];

如果末尾字符不同,有以下处理方式

  • 修改S或T末尾字符使其与另一个一致来完成,m[len(S)][len(T)]=m[len(S)-1][len(T)-1]+1;

  • 在S末尾插入T末尾的字符,比较S[1…len(S)]和S[1…len(T)-1];

  • 在T末尾插入S末尾的字符,比较S[1…len(S)-1]和S[1…len(T)];

  • 删除S末尾的字符,比较S[1…len(S)-1]和S[1…len(T)];

  • 删除T末尾的字符,比较S[1…len(S)]和S[1…len(T)-1];

总结为,对于i>0,j>0的状态(i,j), 有递推方程:
m[i][j] = min( m[i-1][j-1]+(s[i]==s[j])?0:1 , m[i-1][j]+1, m[i][j-1] +1)

这里的重叠子结构是S[1…i],T[1…j]

2.2.3 最长公共子序列(LCS)

参考资料: 最长公共子序列
一个字符串S,去掉零个或者多个元素所剩下的子串称为S的子序列。最长公共子序列就是寻找两个给定序列的子序列,该子序列在两个序列中以相同的顺序出现,但是不必要是连续的。例如X = {a, Q, 1, 1}; Y = {a, 1, 1, d, f}那么,{a, 1, 1}是X和Y的最长公共子序列

递推关系:
对于字符串x, y:

  • 如果 xm=yn x m = y n ,则 LCS(X,Y)=xm+LCS(Xm1,Yn1) L C S ( X , Y ) = x m + L C S ( X m − 1 , Y n − 1 )
  • 如果 xm!=yn x m ! = y n ,则 LCS(X,Y)=max{LCS(Xm1,Y),LCS(X,Yn1)} L C S ( X , Y ) = m a x { L C S ( X m − 1 , Y ) , L C S ( X , Y n − 1 ) }

结束条件:如果i=0或j=0, 则dp[i][j] = 0

2.2.4 0-1背包问题

一个贼在偷窃一家商店时发现了n件物品,其中第i件值vi元,重wi磅。他希望偷走的东西总和越值钱越好,但是他的背包只能放下W磅。请求解如何放能偷走最大价值的物品,这里vi、wi、W都是整数。

解法:
为了找出子结构的形式,粗略地分析发现,对前k件物品形成最优解时,需要决策第k+1件是否要装入背包。但是此时剩余容量未知,不能做出决策。因此把剩余容量也考虑进来,形成的状态由已决策的物品数目和剩余容量两者构成。这样,所有状态可以放入一个n*(W+1)的矩阵c中,其值为当前包中物品总价值,i为物品索引,j为剩余容量(初值为W),这时有:

c[i][j]={c[i1][j]max{c[i1][jwi]+vi , c[i1][j]}if wi>jif wij c [ i ] [ j ] = { c [ i − 1 ] [ j ] i f   w i > j max { c [ i − 1 ] [ j − w i ] + v i   ,   c [ i − 1 ] [ j ] } i f   w i ⩽ j

3. 回溯法

3.1 理论梳理

五大常用算法之四:回溯法
算法复习笔记(回溯法,分支限界法)

3.1.1 基本框架

  1. 解空间树
  2. 深度优先搜索
  3. 可行性判断,有解则继续,无解则回溯

一般模式:

bool finished = FALSE; /* 是否获得全部解? */

/*a[]表示当前获得的部分解;k表示搜索深度;input表示用于传递的更多的参数;*/
backtrack(int a[], int k, data input)
{
    int c[MAXCANDIDATES]; /*这次搜索的候选 */
    int ncandidates; /* 候选数目 */
    int i; /* counter */
    if (is_a_solution(a,k,input))
    process_solution(a,k,input);/*对于符合条件的解进行处理,通常是输出、计数等*/
    else {
        k = k+1;
        /*根据目前状态,构造这一步可能的选择,存入c[]数组,其长度存入ncandidates*/
        construct_candidates(a,k,input,c,&ncandidates);
        for (i=0; i<ncandidates; i++) {
            a[k] = c[i];
            make_move(a,k,input);/*将采取的选择更新到原始数据结构上*/
            backtrack(a,k,input);
            unmake_move(a,k,input);
            if (finished) return; /* 如果符合终止条件就提前退出 */
        }
    }
}

这里写图片描述
这里写图片描述

3.2 算法实例

全面解析回溯法:算法框架与问题求解
已知集合:

利用回溯求所有子集:
这里写图片描述
利用回溯求全排列:
这里写图片描述

4. 分支限界法

4.1 理论梳理

4.1.1 适用情况

4.1.2 基本步骤

4.1.3 复杂性分析

4.2 算法实例

5. 贪心算法

5.1 理论梳理

5.1.1 证明方法

关于贪心算法的正确性证明
052贪心法的正确性证明

6. 时间复杂度专题

6.1 递归方程的求解

递归算法的时间复杂度终结篇
这里写图片描述

  • 15
    点赞
  • 107
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值