目录
一 算法简介
①算法定义
②算法特征
③算法编程
二 算法应用
汉诺塔
问题描述
问题解析
问答解答
快速幂
问题描述
问题解析
问题解答
三 分治思想设计排序算法
步骤解析
归并排序
归并排序的主要操作
归并排序与交换排序
归并算法的应用:逆序对问题
快速排序
快速排序的主要操作
快速排序的应用:求第k大数问题
算法拓展:减治法
附录:
分治是广为人知的算法思想。当我们遇到一个难以直接解决的大问题时,自然会想到把它划分成一些规模较小的子问题,各个击破,“分而治之(Divide and Conquer)” 分治算法的具体操作是把原问题分成 k 个较小规模的子问题,对这 k 个子问题分别求解。如果子问题不够小,那么把每个子问题再划分为规模更小的子问题。这样一直分解下去,直到问题足够小,很容易求出这些小问题的解为止。
分治法的题目,需要符合两个特征:
(1)平衡子问题:子问题的规模大致相同。能把问题划分成大小差不多相等的k个子问题,最好k=2,即分成两个规模相等的子问题。子问题规模相等的处理效率,比子问题规模不等的处理效率要高。
(2)独立子问题:子问题之间相互独立。这是区别于动态规划算法的根本特征,在动态规划算法中,子问题是相互联系的,而不是相互独立的。
需要说明的是,分治法不仅能够让问题变得更容易理解和解决,而且常常能大大伏化算法的复杂度,如把 O(n)的复杂度优化到 O(log2n)。这是因为局部的优化有利于全局;一个子问题的解决,其影响力扩大了 k 倍,即扩大到了全局。
一个简单的例子:在一个有序的数列中查找一个数。简单的办法是从头找到尾,复杂度是O(n)。如果用分治法,即“折半查找”,最多只需要logn次就能找到。这个方法是二分法,二分法也是分治法。
分治法的思想,几乎就是递归的过程,用递归程序实现分治法是很自然的。
用分治法建立模型时,解题步骤分为三步:
(1)分解(Divide):把问题分解成独立的子问题;
(2)解决(Conquer):递归解决子问题;
(3)合并(Combine):把子问题的结果合并成原问题的解。
分治法的经典应用,有汉诺塔、快速排序、归并排序等。
汉诺塔是一个古老的数学问题:有 3根杆子 A,B;C。A 杆上有 N 个(N>1)穿孔圆盘,盘的尺寸由下到上依次变小。要求按下列规则将所有圆盘移至C杆:每次只能移动一个圆盘;大盘不能叠在小盘上面(提示,可将圆盘临时置于 B杆,也可将从A 杆移出的圆盘重新移回 A 杆,但都必须遵循上述两条规则)。问:如何移动?最少要移动多少次?
输入: 输入两个正整数,一个是 N(N15),表示要移动的盘子数;一个是M,表示在最少移动步骤的第 M 步。
输出:共输出两行。第1行输出格式为:#No:a->b,表示第 M 步具体移动方法,其中 No表示第 M 步移动的盘子的编号(N 个盘子从上到下依次编号为1~N),表示第M 步是将No号盘子从a 杆移动到6杆(a 和b的取值均为(A、B、C))。第2行输出一个整数,表示最少移动步数。
举例:输入 3 2 输出 #2:A->B \n 7
汉诺塔的经典解法是分治法。汉诺塔的逻辑很简单: 把 n 个盘子的问题分治成两个子问题。设有x,y、2 3 根杆子,初始的 n 个盘子在 x杆上。
(1) 把 x杆的 n-1个小盘移动到 y (把这 n-1 个小盘看做一个整体),然后把第 n个大盘移动到 z杆;
(2)_把y杆上的 n-1 个小盘移动到z杆
分析代码复杂度。每次分治,分成两部分,以两倍递增:一分二,二分四。。。。复杂度为O(2的n次方),在汉诺塔中,分治法不能降低复杂度
#include
int sum = 0, m;
void hanoi(char x,char y,char z,int n){ //三个柱子x、y、zif(n==1) { //最小问题sum++;if(sum==m) printf("#%d:%d->%d",n,x,z);}else { //分治hanoi(x,z,y,n-1); //(1)先把x的n-1个小盘移到y,然后把第n个大盘移到zsum++;if(sum==m) printf("#%d:%d->%d",n,x,z);hanoi(y,x,z,n-1); //(2)把y的n-1个小盘移到z}
}
int main(){int n; scanf("%d %d",&n,&m);hanoi('A','B','C',n);printf("%d",sum);return 0;
}
幂运算a的n次方。快速幂就是高效的算出a的n次方。当n很大时,如n等于10的九次方爆破里会超时
先算 a的平方,然后再继续算平方(a2),再继续平(a2)),一直算到n 次幂,总共只需要算 O(logn)次.这就是分治法。另外由于a的n次方极大,一般会取模再输出。
#include
using namespace std;
typedef long long ll; //注意要用long long,用int会出错
ll fastPow(ll a, ll n,ll m){ //an mod mif(n == 0) return 1; //特判 a0 = 1if(n == 1) return a;ll temp = fastPow(a, n/2,m); //分治if(n%2 == 1) return temp * temp * a % m; //奇数个a。也可以这样写: if(n &1) else return temp * temp % m ; //偶数个a
}
int main(){ll a,n,m; cin >> a >> n>> m;cout << fastPow(a,n,m);return 0;
}

(1)分解。把原来无序的数列,分成两部分;对每个部分,再继续分解成更小的两部分……在归并排序中,只是简单地把数列分成两半。在快速排序中,是把序列分成左右两部分,左部分的元素都小于右部分的元素;分解操作是快速排序的核心操作。
(2)解决。分解到最后不能再分解,排序。
(3)合并。把每次分开的两个部分合并到一起。归并排序的核心操作是合并,其过程类似于交换排序。快速排序并不需要合并操作,因为在分解过程中,左右部分已经是有序的。
下面给出了归并排序的操作步骤。初始数列经过 3 次归并之后,得到一个从小到大的有序数列。请根据这个例子,自己先分析它是如何实现分治法的分解、解决、合并3 个步骤的

(1)分解。把初始序列分成长度相同的左右两个子序列,然后把每个子序列再分成更小的两个子序列,直到子序列只包含一个数。这个过程用递归实现,上图第 1行是初始序列,每个数是一个子序列,可以看成递归到达的最底层。
(2) 求解子问题,对子序列排序。最底层的子序列只包含一个数,其实不用排序。