算法基础知识——分治策略
目录:
- 基础知识
- 算法依赖因素
- 输入规模、运行时间
- 渐近记号(O、θ、Ω)
- 分治策略
- 分解、解决、合并
- 代入法、递归树法、主方法
- 经典问题
- 最大子数组问题
- Strassen矩阵乘法问题
- 应用实例
- n的阶乘【清华大学】
- 递归和动态规划-汉诺塔II【2016校招真题在线编程】
- 全排列【北京大学】
- 杨辉三角【西北工业大学】
- Fibonacci【上海交通大学】
- 二叉树【北京大学】
- 2的幂次方【上海交通大学】
一、基础知识
1、一个算法的依赖因素:
- 待排序项数;
- 这些项已被稍微排序的程度;
- 关于项值的可能限制;
- 计算机的体系结构;
- 存储设备的种类(主存、磁盘、磁带等)。
2、输入规模:输入中的项数。
3、运行时间:执行的基本操作数或步数。
4、渐进记号:
- θ(g(n)) :
- 定义:存在正常量c1、c2和n0,使得对所有n ≥ n0,有0 ≤ c1g(n) ≤ f(n) ≤ c2g(n),其中f(n)在一个常量因子内等于g(n),g(n)是f(n)的一个渐近紧确界(asymptotically tight bound)。
- 每个成员f(n) ∈ θ(g(n))均渐近非负。
- θ记号渐近地给出了一个函数的上界和下界。
- O(g(n)):
- 定义:存在正常量c和n0,使得所有n ≥ n0,有0 ≤ f(n) ≤ cg(n),g(n)为渐近上界。
- θ(g(n)) 包含于 O(g(n))。
- O记号为函数给出一个在常量因子内的上界。
- o记号(小og(n))中,界0 ≤ f(n) ≤ cg(n)对所有常量c>0成立
- Ω(g(n)):
- 定义:存在正常量c和n0,使得所有n ≥ n0,有0 ≤ cg(n) ≤ f(n),g(n)为渐近下界。
- Ω记号为函数给出一个在常量因子内的下界。
- ω记号(小ωg(n))中,界0 ≤ cg(n) ≤ f(n)对所有常量c>0成立
二、分治策略
1、递归步骤:
- 分解(Divide):将问题划分为一些子问题,子问题的形式与原问题一样,只是规模更小;
- 解决(Conquer):递归地求解出子问题。如果子问题的规模足够小,则停止递归,直接求解;
- 合并(Combine):将子问题的解组合成原问题的解。
2、递归情况与基本情况:
- 递归情况(recursive case):当子问题足够大,需要递归求解时,称之为递归情况。
- 基本情况(base case):当子问题足够小,不再需要递归时,即递归已经“触底”,称之为基本情况。
3、求解递归式的方法:
- 代入法:
- 定义:猜测一个界,然后用数学归纳法证明这个界是正确的。
- 步骤:
- 猜测解的形式;
- 用数学归纳法求出解中的常数,并证明解释正确的。
- 示例:T(n) = 2T(⌊n / 2⌋) + n
- 猜测解的形式为:T(n) = O(nlogn)
- 设T(n) ≤ knlogn,假设此上界对所有正数m < n成立,当m = ⌊n / 2⌋时,有T(⌊n / 2⌋) ≤ k⌊n / 2⌋log(⌊n / 2⌋⌋) ,将其代入递归式,得:T(n) ≤ 2k⌊n / 2⌋log(⌊n / 2⌋) + n ≤ knlog(n / 2) + n = knlogn - knlog2 + n = knlogn - kn + n = knlogn - (k - 1)n ≤ knlogn,其中n ≥ 1时,最后一步成立。
- 递归树法:
- 定义:将递归式转换成一棵树,其结点表示不同层次的递归调用产生的代价。然后采用边界和技术来求解递归式。
- 步骤:
- 每个结点表示一个单一子问题的代价,子问题对应某次递归函数调用;
- 将树中的每层中的代价求和,得到每层代价,然后将所有层的代价求和,得到所有层次的递归调用的总代价。
- 主方法:T(n) = aT(n / b) + f(n)
- 定义:将规模为n的问题分解为a个子问题,每个子问题的规模是原问题规模的n / b,分解和合并步骤总共花费时间为f(n)。
- 主定理:
- 示例:
- T(n) = 9T(n / 3) + n
- a = 9,b = 3,f(n) = O(n的(log3的9 - ε)次方),其中ε = 1,f(n) = n,应用情况1,n的(logb的a)次方 = n的(log3的9)次方 = θ(n²),则解为T(n) = θ(n²)。
- T(n) = T(2n / 3) + 1
- a = 1,b = 3/2,f(n) = 1,n的(logb的a)次方 = n的(log3/2的1)次方 = n的0次方 = 1,f(n) = θ(n的(logb的a)次方),应用情况2,则T(n) = θ(logn)
- T(n) = 3T(n / 4) + nlogn
- a = 3,b = 4,f(n) = nlogn,n的(logb的a)次方 = n的(log4的3)次方 = n的0.793次方,f(n) = Ω(n的(log4的3 + ε)次方),其中ε ≈ 0.2,
- T(n) = 9T(n / 3) + n
三、经典问题
1、寻找数组A[low, high]的最大子数组问题:
(1)暴力法:O(n²)
(2)分治法思路:
- 任何连续子数组完全位于子数组A[low, mid]中,,low ≤ i ≤ j ≤ mid;
- 任何连续子数组完全位于子数组A[mid + 1, high]中,mid < i ≤ j ≤ high;
- 跨越了中点,low ≤ i ≤ mid < j ≤ high。
(3)分治法时间复杂度:θ(nlogn)。
2、矩阵乘法的Strassen乘法:
(1)思路:
- 分解(Divide):
- 设矩阵C = A * B,将一个n * n的矩阵A、B划分为4个(n / 2) * (n / 2)的子矩阵A11、A12、A21、A22、B11、B12、B21、B22,该步骤花费时间θ(1);
- 解决(Conquer):
- 创建10个(n / 2) * (n / 2)的子矩阵S1、S2、…、S10,每个矩阵保存上面创建的子矩阵的和或差,需要进行10次(n / 2) * (n / 2)矩阵的加减法,该步骤花费时间θ(n²);
- S1 = B12 - B22
- S2 = A11 + A12
- S3 = A21 + A22
- S4 = B21 - B11
- S5 = A11 + A22
- S6 = B11 + B22
- S7 = A12 - A22
- S8 = B21 + B22
- S9 = A11 - A21
- S10 = B11 + B12
- 根据A、B子矩阵和S1、…、S10,递归地计算7个矩阵积P1、P2、…、P7,花费时间7T(n / 2);
- P1 = A11 * S1 = A11 * B12 - A11 * B22
- P2 = S2 * B22 = A11 * B22 + A12 * B22
- P3 = S3 * B11 = A21 * B11 + A22 * B11
- P4 = A22 * S4 = A22 * B21 - A22 * B11
- P5 = S5 * S6 = A11 * B11 + A11 * B22 + A22 * B11 + A22 * B22
- P6 = S7 * S8 = A12 * B21 + A12 * B22 - A22 * B21 - A22 * B22
- P7 = S9 * S10 = A11 * B11 +A11 * B12 - A21 *B11 - A21 * B12
- 创建10个(n / 2) * (n / 2)的子矩阵S1、S2、…、S10,每个矩阵保存上面创建的子矩阵的和或差,需要进行10次(n / 2) * (n / 2)矩阵的加减法,该步骤花费时间θ(n²);
- 合并(Combine):
- 通过Pi矩阵的不同组合进行加减运算,计算出结果矩阵C的子矩阵C11、C12、C21、C22,进行8次(n / 2) * (n / 2)矩阵的加减法,花费时间θ(n²)。
- C11 = P5 + P4 - P2 + P6
- C12 = P1 + P2
- C21 = P3 + P4
- C22 = P5 + P1 - P3 - P7
- 通过Pi矩阵的不同组合进行加减运算,计算出结果矩阵C的子矩阵C11、C12、C21、C22,进行8次(n / 2) * (n / 2)矩阵的加减法,花费时间θ(n²)。
(2)时间复杂度:θ(n的log7,即n的2.81次方)。
(3)局限性:
- Strassen算法在递归层次中生成的子矩阵要消耗空间。矩阵乘法一般意义上还是选择的是朴素的方法(三个for循环,θ(n³)),只有当矩阵变稠密,而且矩阵的阶数>20左右,才会考虑使用Strassen算法。
- 递归过程中生成的子矩阵消耗存储空间。
- 隐藏在Strassen算法运行时间θ(n的log7,即n的2.81次方)中的常数因子比朴素方法的过程θ(n³)的常数因子大。
四、应用实例
1、题目描述:输入一个整数n,输出n的阶乘(每组测试用例可能包含多组数据,请注意处理)【清华大学】
- 输入格式:一个整数n(1<=n<=20)
- 输出格式:n的阶乘
- 样例输入:
- 3
- 样例输出:
- 6
示例代码:
#include <stdio.h>
long long factorial(int n){
if(n == 1 || n == 0){
return 1;
}else{
return n * factorial(n - 1);
}
}
int main(){
int inputNumber;
while(scanf("%d", &inputNumber) != EOF){
printf("%lld\n", factorial(inputNumber));
}
return 0;
}
2、题目描述:有一个int数组arr其中只含有1、2和3,分别代表所有圆盘目前的状态,1代表左柱,2代表中柱,3代表右柱,arr[i]的值代表第i+1个圆盘的位置。比如,arr=[3,3,2,1],代表第1个圆盘在右柱上、第2个圆盘在右柱上、第3个圆盘在中柱上、第4个圆盘在左柱上。如果arr代表的状态是最优移动轨迹过程中出现的状态,返回arr这种状态是最优移动轨迹中的第几个状态。如果arr代表的状态不是最优移动轨迹过程中出现的状态,则返回-1。给定一个int数组arr及数组的大小n,含义如题所述,请返回一个int,代表所求的结果。
int chkStep(vector<int> arr, int n) {
// write code here
}【2016校招真题在线编程】
- 输入格式:一个int数组arr及数组的大小n
- 输出格式:返回arr这种状态是最优移动轨迹中的第几个状态。如果arr代表的状态不是最优移动轨迹过程中出现的状态,则返回-1。
- 样例输入:
- [3,3]
- 样例输出:
- 3
示例代码:
class Hanoi {
public:
int chkStep(vector<int> arr, int n) {
if(arr.empty()|| n <= 0 || arr.size() != n){
return -1;
}
return hanoi(arr, 1, 2, 3, n - 1);//从最大下标开始计算
}
int hanoi(vector<int> arr, int from, int middleware, int to, int loc){
if(loc == -1){
return 0;
}
if(arr[loc] == middleware){ //在中间的杆,则肯定不是最优移动轨迹
return -1;
}else if(arr[loc] == from){ //在最左的杆,返回子问题
return hanoi(arr, from, to, middleware, loc - 1);
}else{ //在最右的杆
int tmp = hanoi(arr, middleware, from, to, loc - 1); //子问题移动到最终杆所需步数
if(tmp == -1){
return -1;
}else{
return (1 << loc) + tmp; //前面的子问题完成(2的loc次方 - 1) + 本杆的1次移动(1) + 子问题移动到最终杆所需步数
}
}
}
};
附注:
(1)汉诺塔问题:在一块铜板上有三根杆,最左边的杆自上而下、由小到大顺序串着由64个圆盘构成的塔。目的是将最左边杆上的圆盘全部移动到右边的杆上,条件是一次只能移动一个圆盘,并且不允许大圆盘放在小圆盘的上面。
- 1个圆盘,移动1次
- 2个圆盘,移动3次
- 3个圆盘,移动7次
- ……
- n个圆盘,移动2的n次方 - 1次
(2)C语言中,左移右移优先级小于加减运算符,因此需要写return (1 << loc) + tmp;
C语言运算符优先级:单算移关与,异或逻条赋(逗)。
- 括号类:
- () (括号)、 [] (数组下标) .、 (对象) 、 ->(指针)
- 单目运算符:
- -(负号)、+(正号)、 ~(按位取反)、 ! (逻辑非)、 ++(自增) 、--(自减)、 &(取地址) 、 *(取值)、 sizeof(长度运算)
- 算术运算符:
- * / %
- + -
- 移位运算符:
- <<(位左移)、>>(位右移)
- 关系运算符:
- > < ≥ ≤
- != ==
- 与:
- &(按位与)
- 异:
- ^(按位异或)
- 或:
- |(按位或)
- 逻辑运算符:
- &&(逻辑与)
- ||(逻辑或)
- 条件运算符:
- ?:
- 赋值运算符:
- =、 +=、 -=、 *=、 /=、 %=、 >>=、 <<=、 &=、 ^=、 |=
- 逗号运算符:
- ,
3、题目描述:给定一个由不同的小写字母组成的字符串,输出这个字符串的所有全排列。 我们假设对于小写字母有'a' < 'b' < ... < 'y' < 'z',而且给定的字符串中的字母已经按照从小到大的顺序排列。【北京大学】
- 输入格式:输入只有一行,是一个由不同的小写字母组成的字符串,已知字符串的长度在1到6之间。
- 输出格式:输出这个字符串的所有排列方式,每行一个排列。要求字母序比较小的排列在前面。字母序如下定义:已知S = s1s2...sk , T = t1t2...tk,则S < T 等价于,存在p (1 <= p <= k),使得s1 = t1, s2 = t2, ..., sp - 1 = tp - 1, sp < tp成立。每组样例输出结束后要再输出一个回车。
- 样例输入:
- abc
- 样例输出:
- abc
- acb
- bac
- bca
- cab
- cba
示例代码:
#include <stdio.h>
#include <string.h>
#include <algorithm>
#define MAX_SIZE 7
using namespace std;
int mark[MAX_SIZE];
char result[MAX_SIZE];
void printChar(char inputStr[], int len, int size){
if(size == len){
printf("%s\n", result);
return;
}
for(int i = 0; i < len; i++){
if(mark[i] == 0){
result[size] = inputStr[i];
mark[i] = 1;
printChar(inputStr, len, size + 1);
mark[i] = 0;
}
}
}
int main(){
char inputStr[MAX_SIZE];
while(scanf("%s", &inputStr) != EOF){
int len = strlen(inputStr);
sort(inputStr, inputStr + len);
printChar(inputStr, len, 0);
printf("\n");
}
return 0;
}
4、题目描述:输入n值,使用递归函数,求杨辉三角形中各个位置上的值。【西北工业大学】
- 输入格式:一个大于等于2的整型数n
- 输出格式:题目可能有多组不同的测试数据,对于每组输入数据,按题目的要求输出相应输入n的杨辉三角形。
- 样例输入:
- 6
- 样例输出:
- 1 1
- 1 2 1
- 1 3 3 1
- 1 4 6 4 1
- 1 5 10 10 5 1
示例代码:
#include <stdio.h>
int yanghuiTriangle(int i, int j){
if(j == 0 || i + 1 == j){
return 1;
}
return yanghuiTriangle(i - 1, j) + yanghuiTriangle(i - 1, j - 1);
}
int main(){
int n;
scanf("%d", &n);
for(int i = 0; i < n - 1; i++){
for(int j = 0; j <= i + 1; j++){
printf("%d ", yanghuiTriangle(i, j));
}
printf("\n");
}
return 0;
}
5、题目描述:The Fibonacci Numbers{0,1,1,2,3,5,8,13,21,34,55...} are defined by the recurrence:F0=0 F1=1 Fn=Fn-1+Fn-2,n>=2 Write a program to calculate the Fibonacci Numbers.【上海交通大学】
- 输入格式:Each case contains a number n and you are expected to calculate Fn.(0<=n<=30) 。
- 输出格式:For each case, print a number Fn on a separate line,which means the nth Fibonacci Number.
- 样例输入:
- 1
- 样例输出:
- 1
示例代码:
#include <stdio.h>
int Fibonacci(int count){
if(count == 0){
return 0;
}else if(count == 1){
return 1;
}
return Fibonacci(count - 1) + Fibonacci(count - 2);
}
int main(){
int n;
while(scanf("%d", &n) != EOF){
printf("%d\n", Fibonacci(n));
}
return 0;
}
6、题目描述:
1
/ \
2 3
/ \ / \
4 5 6 7
/\ /\ /\ /\
如上图所示,由正整数 1, 2, 3, ...组成了一棵无限大的二叉树。从某一个结点到根结点(编号是1的结点)都有一条唯一的路径,比如从5到根结点的路径是(5, 2, 1),从4到根结点的路径是(4, 2, 1),从根结点1到根结点的路径上只包含一个结点1,因此路径就是(1)。对于两个结点x和y,假设他们到根结点的路径分别是(x1, x2, ... ,1)和(y1, y2,...,1),那么必然存在两个正整数i和j,使得从xi 和yj 开始,有xi = yj,xi + 1 = yj + 1,xi + 2 = yj + 2,...
现在的问题就是,给定x和y,要求他们的公共父节点,即xi(也就是 yj)。【北京大学】
- 输入格式:输入包含多组数据,每组数据包含两个正整数x和y(1≤x, y≤2^31-1)。
- 输出格式:对应每一组数据,输出一个正整数xi,即它们的首个公共父节点。
- 样例输入:
- 10 4
- 样例输出:
- 2
示例代码:
#include <stdio.h>
int FindFather(int a, int b){
if(a == 1 || b == 1){
return 1;
}else if(a == b){
return a;
}
int result = a > b ? FindFather(a / 2, b) : FindFather(a, b / 2);
return result;
}
int main(){
int a, b;
while(scanf("%d%d", &a, &b) != EOF){
printf("%d\n", FindFather(a, b));
}
return 0;
}
7、题目描述:Every positive number can be presented by the exponential form.For example, 137 = 2^7 + 2^3 + 2^0。 Let's present a^b by the form a(b).Then 137 is presented by 2(7)+2(3)+2(0). Since 7 = 2^2 + 2 + 2^0 and 3 = 2 + 2^0 , 137 is finally presented by 2(2(2)+2 +2(0))+2(2+2(0))+2(0). Given a positive number n,your task is to present n with the exponential form which only contains the digits 0 and 2.【上海交通大学】
- 输入格式:For each case, the input file contains a positive integer n (n<=20000).
- 输出格式:For each case, you should output the exponential form of n an a single line.Note that,there should not be any additional white spaces in the line.
- 样例输入:
- 1315
- 样例输出:
- 2(2(2+2(0))+2)+2(2(2+2(0)))+2(2(2)+2(0))+2+2(0)
#include <stdio.h>
void Deal(int number){
int pos = 14;//14位二进制能表示的最大整数为2的15次方 - 1 = 32767
while(number){
if(number>>pos){
if(pos == 0){
printf("2(0)");
}else if(pos == 1){
printf("2");
}else{
printf("2(");
Deal(pos);
printf(")");
}
number -= 1<<pos;
if(number > 0){
printf("+");
}
}
--pos;
}
}
int main(){
int number;
while(scanf("%d", &number) != EOF){
Deal(number);
printf("\n");
}
return 0;
}
参考文献:
[1]Thomas.H.Cormen Charles E. Leiseron、Ronald L. Rivest Clifford Srein. 算法导论(第3版). [M]北京:机械工业出版社,2013.01;
[2]杨泽邦、赵霖. 计算机考研——机试指南(第2版). [M]北京:电子工业出版社,2019.11;