一. Divide-and-Conquer原理
简而言之, 分治算法就是一个问题的规模较大时不好解决, 但规模较小时又很好解, 那么我们就将大问题化成小问题, 依次求解小问题再合并成大问题的解, 当然, 不是所有问题都可以这么做.
设计过程分为三个阶段
1.Divide: 整个问题划分为多个子问题
注意:分解的这组子问题 未必一定是相同的子问题,即$p_i 和p_j $可以是分别完成不同任务的子问题
2.Conquer:求解各子问题(递归调用正设计的算法), 注意边界条件
3.Combine:合并子问题的解, 形成原始问题的解
Divide-and-Conquer 算法的分析
1.分析各阶段的复杂性
- Divide 阶段的时间复杂性: D(n)
- Conquer 阶段的时间复杂性: aT (n/b)
- Combine 阶段的时间复杂性: C(n)
2.建立递归方程
- 设输入大小为 n, T(n) 为时间复杂性
- 当 n<c 时,T(n)= O(1)
3.求解递归方程得到问题的复杂度
例1: 最大最小值问题
输入:一个数组A
输出: 数组中的最大值和最小值
分析:
通常,直接扫描需2 n -2 次比较操作,下面给出一个复杂度为3n/2-2的算法
例2: 整数乘法
输入:n 位二进制整数 X 和 Y
输出:X 和 Y 的乘积
通常,计算X*Y 时间复杂性为 ,我们给出一个复杂性为 的算法。
例3: 快排
输入: 一个数组A
输出:一个数据排好序(升序)的数组A’
划分算法: 分界,并返回分解点的坐标
证明快排的正确性: 即证明循环不变量恒成立
循环不变量:数据或数据结构的关键性质, 依赖于具体的算法和算法特点
证明分三个阶段
(1)初始 阶段 :循环开始前循环不变量成立
(2)循环 阶段 :循环体每执行一次 循环不变量成立
(3)终止 阶段 :算法结束后,循环不变量保证算法正确
初始阶段 : j=p
算法迭代前: i =p 1, j=p, 条件 1 和 2 为真 . 算法第 1 行(即 )使得条件 3为真
保持阶段:
设 j=k时循环不变量成立.往证j=k+1时不变量成立.
终止阶段
算法结束时, j=r, 产生三个集合:
- 所有小于等于x的元素构成的集合.
- 所有大于x的元素构成的集合.
- 由元素x构成的集合.
算法结束时
最后一个步骤将A[r]与A[i+1]互换.
算法性能分析
最好情况:
最坏情况:
平均情况:
证明略
定理. 随机排序算法的期望时间复杂性为O(nlogn)
问题的下界
问题的下届即理论上算法最好的时间复杂度
如果一个算法的时间复杂度与问题的下界相同,那么则说该算法是最优的
下面说明了排序算法的下界:
减治算法
减治方法:仅通过 求解某一个子问题 的解得到原始问题的解
分治方法:递归 求解每一个子问题 ,然后通过合并各个子问题的解最后得到原始问题的解
例:中位数与次序统计问题
输入:一个数组A
输出: 第k大的元素
先求一个中位数q,将A中元素分到3个集合中去
例:找最近的点对
输入: 以为数组上的n个点
输出:距离最近的两个点
先排序再查找的时间复杂度O(n log n)
我们用分治算法
- 边界条件: 当只有两个点的时候,直接返回这个点对
- 求 Q 中点的中位数 m
- 划分: 用 Q 中点坐标中位数 m 把 Q 划分为两个大小相等的子集合Q1, Q2
- 递归地在 Q 1 和 Q 2 中找出最接近点对(p1 ,p2 )和 (q1 ,q2)
- 合并:在 (p 1 , p 2) 、 (q 1 , q 2 )和某个 (p 3 , q 3 )之间选择最接近点对(x, y) 其中 p 3 是 Q 1 中最大点 q 3 是Q2 中最小点。
凸包算法
输入: 二维平面上n个点的坐标
输出: (一个最少的)点的集合,该集合中所有点连成圈之后能包含所有点,如下图
注意: 这个圈要求以最少的点连成
分治算法:
快速幂算法
输入: a, b,n
输出:
分治算法:
数学支持:(ab)%c = (a%c)(b%c)%c
T(n) = 2T(n/2) + O(1)
由master定理,可以得到算法复杂度为:log n
int PowerMod(int a, int n, int b)
{
int ans = 1;
a = a % b;
while(n>0) {
if(n % 2 = = 1)
ans = (ans * a) % b; //n为奇数时,折半时会少一个数,最后一次折半会执行
n = n/2;
a = (a * a) % b; // a此时已经为a%c了
}
return ans;
}
集合划分问题
问题:给出n个元素的集合,将其划分为两两不相交的m个子集,有多少种分法?
输入: n,m
输出: 分法数量k
问题分析:
我们需要将n个元素放进m个集合中, 那么显然的是:
- 当m > n时,划分是不可能的,返回0
- 当m = 1时不用划分,直接返回1
- 当m = n时不用划分,直接返回1
然后我们可以以此作为迭代基础,来进行迭代;又有如下规律:
即
所以我们可以写出代码:
setDiv(n,m)
{
if(n==m ||m=1 ) return 1;
return setDiv(n-1,m-1) + m*setDiv(n-1,m);
}