先看分治这一部分
分治法所能解决的问题一般具有以下几个特征:
第一条特征是绝大多数问题都可以满足的,因为问题的计算复杂性一般是随着问题规模的增加而增加;
第二条特征是应用分治法的前提它也是大多数问题可以满足的,此特征反映了递归思想的应用;、
第三条特征是关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条特征,而不具备第三条特征,则可以考虑用贪心法或动态规划法。
第四条特征涉及到分治法的效率,如果各子问题是不独立的则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用动态规划法较好。
step1 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题;
step2 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题
step3 合并:将各个子问题的解合并为原问题的解。
一个分治法将规模为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)
坐在马桶上看算法:快速排序:
对于序列a, 先任意找一个基准数, 然后利用上文中的哨兵方法把比基准数小的数移动到基准数左边, 把比基准数大的数移动到基准数右边, 然后对基准数左右的两个子序列重复这个过程.
复杂度: 快速排序的最差时间复杂度和冒泡排序是一样的都是O(N2),它的平均时间复杂度为O(NlogN)
白话经典算法系列之五 归并排序的实现
子问题: 要将两个有序数列合并,只要从比较二个数列的第一个数,谁小就先取谁,取了后就在对应数列中删除这个数。然后再进行比较,如果有数列为空,那直接将另一个数列的数据依次取出即可。
分解与合并: 归并排序的基本思路就是将数组分成二组A,B,如果这二组组内的数据都是有序的,那么就可以很方便的将这二组数据进行排序了.
下图源自:图解排序算法(四)之归并排序
复杂度:
设数列长为N,将数列分开成小数列一共要logN步,每步都是一个合并有序数列的过程,时间复杂度可以记为O(N),故一共为O(N*logN)。
复杂度: 时间复杂度无非就是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)
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);
}
动态规划部分
动态规划把问题的求解过程变成一个多阶段的决策过程,每一步决策都将利用之前的决策结果
最优子结构:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。
无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。
有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)
性质3是动态规划与分治法在适用情景上的主要差别
常见的动态规划问题分析与求解
假设有几种硬币,如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越小, 所需的硬币数未必越少, 所以不能用扣去最大面额硬币的贪心策略来缩小问题规模.
对于序列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]
参考资料: 最长公共子序列
一个字符串S,去掉零个或者多个元素所剩下的子串称为S的子序列。最长公共子序列就是寻找两个给定序列的子序列,该子序列在两个序列中以相同的顺序出现,但是不必要是连续的。例如X = {a, Q, 1, 1}; Y = {a, 1, 1, d, f}那么,{a, 1, 1}是X和Y的最长公共子序列
递推关系:
对于字符串x, y:
结束条件:如果i=0或j=0, 则dp[i][j] = 0
一个贼在偷窃一家商店时发现了n件物品,其中第i件值vi元,重wi磅。他希望偷走的东西总和越值钱越好,但是他的背包只能放下W磅。请求解如何放能偷走最大价值的物品,这里vi、wi、W都是整数。
解法:
为了找出子结构的形式,粗略地分析发现,对前k件物品形成最优解时,需要决策第k+1件是否要装入背包。但是此时剩余容量未知,不能做出决策。因此把剩余容量也考虑进来,形成的状态由已决策的物品数目和剩余容量两者构成。这样,所有状态可以放入一个n*(W+1)的矩阵c中,其值为当前包中物品总价值,i为物品索引,j为剩余容量(初值为W),这时有:
c[i][j]={c[i−1][j]max{c[i−1][j−wi]+vi , c[i−1][j]}if wi>jif wi⩽j 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
五大常用算法之四:回溯法
算法复习笔记(回溯法,分支限界法)
一般模式:
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/*将采取的选择更新到原始数据结构上*/
backtrack(a,k,input);
unmake_move(a,k,input);
if (finished) return; /* 如果符合终止条件就提前退出 */
}
}
}
全面解析回溯法:算法框架与问题求解
已知集合:
利用回溯求所有子集:
利用回溯求全排列:
关于贪心算法的正确性证明
052贪心法的正确性证明