算法基础知识——分治策略

算法基础知识——分治策略

目录:

  1. 基础知识
    1. 算法依赖因素
    2. 输入规模、运行时间
    3. 渐近记号(O、θ、Ω)
  2. 分治策略
    1. 分解、解决、合并
    2. 代入法、递归树法、主方法
  3. 经典问题
    1. 最大子数组问题
    2. Strassen矩阵乘法问题
  4. 应用实例
    1. n的阶乘【清华大学】
    2. 递归和动态规划-汉诺塔II【2016校招真题在线编程】
    3. 全排列【北京大学】
    4. 杨辉三角【西北工业大学】
    5. Fibonacci【上海交通大学】
    6. 二叉树【北京大学】
    7. 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,

三、经典问题

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
  • 合并(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

(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;

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值