分治策略Divide and Conquer
来源:互联网 发布:扫题出答案软件下载 编辑:程序博客网 时间:2024/05/18 04:29
在计算机科学中,分治法是一种很重要的算法。字面上的解释是“分而治之”,通常是递归算法,就是 把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换。
任何一个可以用计算机求解的问题所需的计算时间都与其规模有关。问题的规模越小,越容易直接求解,解题所需的计算时间也越少。例如,对于n个元素的排序问题,当n=1时,不需任何计算。n=2时,只要作一次比较即可排好序。n=3时只要作3次比较即可…而当n较大时,问题就不那么容易处理了。要想直接解决一个规模较大的问题,有时是相当困难的。
分治策略的基本思想
分治算法是 将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。如果原问题可分割成
熟悉的例子
二分查找: 给定已按升序排好序的
算法 1
binarysearch(T,x)
输入:排好序的数组T ;数x
输出:j
1.l⟵1;r⟵n
2.while l≤r do
3.m⟵⌊l+r2⌋
4.if T[m]=x then return m
5.else if T[m]≥x then r⟵m−1
6.else l⟵m+1
7.return 0
分析:
1、该问题的规模缩小到一定的程度就可以容易地解决;
2、该问题可以分解为若干个规模较小的相同问题;
3、分解出的子问题的解可以合并为原问题的解;
4、分解出的各个子问题是相互独立的。
很显然此问题分解出的子问题相互独立,即在
#include <iostream>using namespace std;// 查找成功返回value索引,查找失败返回-1template <class T>int binary_search(T array[],const T& value,int left,int right){ while (right >= left) { int m = (left + right) / 2; if (value == array[m]) return m; if (value < array[m]) right = m - 1; else left = m + 1; } return -1;}int main(){ int array[] = {0,1,2,3,4,5,6,7,8,9}; cout << "0 in array position: " << binary_search(array,0,0,9) << endl; cout << "9 in array position: " << binary_search(array,9,0,9) << endl; cout << "2 in array position: " << binary_search(array,2,0,9) << endl; cout << "6 in array position: " << binary_search(array,6,0,9) << endl; cout << "10 in array position: " << binary_search(array,10,0,9) << endl; return 0;}
算法复杂度分析:
每执行一次算法的while循环, 待搜索数组的大小减少一半。因此,在最坏情况下,while循环被执行了O(logn) 次。循环体内运算需要O(1) 时间,因此整个算法在最坏情况下的计算时间复杂性为O(logn) 。
上述算法就是用分治算法,它们的共同特点是:对于一个规模为n的问题,若该问题可以容易地解决(比如说规模n较小)则直接解决,否则将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式相同,递归地解这些子问题,然后将各子问题的解合并得到原问题的解。
分治算法的一般性描述
对这k个子问题分别求解。如果子问题的规模仍然不够小,则再划分为k个子问题,如此递归的进行下去,直到问题规模足够小,很容易求出其解为止。将求出的小规模的问题的解合并为一个更大规模的问题的解,自底向上逐步求出原来问题的解。分治算法divide-and-conquer的伪码描述如下:
算法2:
divide−and−conquer(P)
1.if(|P|≤c) S(P) // 解决小规模的问题
2. divide P into smaller subinstancesP1,P2,…,Pk //分解问题
3. for i=1 to k do
4.yi←divide−and−conquer(Pi) //递归的解各子问题
5.ReturnMerge(y1,y2,y3,…yk−1,yk) //将各子问题的解合并为原问题的解
人们从大量实践中发现,在用分治法设计算法时,最好使子问题的规模大致相同。即将一个问题分成大小相等的
一个分治法将规模为
分治算法的分析技术
定义1:
无穷数列1,1,2,3,5,8,13,21,34,55,……,称为Fibonacci数列。它可以递归地定义为:
实现:
#include <iostream>using namespace std;// fibonacci implement by recursivelong fibonacci_recursive(long n){ if (n <= 1 ) return 1; return fibonacci_recursive(n - 1) + fibonacci_recursive(n - 2);}// fibonacci implement by looplong fibonacci_loop(long n){ if (n == 0 || n == 1) return 1; long f1 = 1; long f2 = 1; long result = 0; for (long i = 1; i < n ; ++ i) { result = f1 + f2; f1 = f2; f2 = result; } return result;}int main(){ cout << "fibonacci implement by recursive: " << endl; for (long i = 0; i <= 20; ++ i) cout << fibonacci_recursive(i) << " " ; cout << endl << endl; cout << "fibonacci implement by loop: " << endl; for (long i = 0; i <= 20; ++ i) cout << fibonacci_loop(i) << " " ; cout << endl; return 0;}
分治法所能解决的问题一般具有以下几个特征:
- 1) 该问题的规模缩小到一定的程度就可以容易地解决
- 2) 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。
- 3) 利用该问题分解出的子问题的解可以合并为该问题的解;
- 4) 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。
第一条特征是绝大多数问题都可以满足的,因为问题的计算复杂性一般是随着问题规模的增加而增加;
第二条特征是应用分治法的前提它也是大多数问题可以满足的,此特征反映了递归思想的应用;
第三条特征是关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条特征,而不具备第三条特征,则可以考虑用贪心法或动态规划法。
第四条特征涉及到分治法的效率,如果各子问题是不独立的则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用动态规划法较好。
提高分治算法的途经
从所周知,直接或间接地调用自身的算法称为递归算法。用函数自身给出定义的函数称为递归函数。它优点是结构清晰,可读性强,而且容易用数学归纳法来证明算法的正确性,因此它为设计算法、调试程序带来很大方便。可是递归算法的运行效率较低,无论是耗费的计算时间还是占用的存储空间都比非递归算法要多。
解决方法:在递归算法中消除递归调用,使其转化为非递归算法。
- 1、采用一个用户定义的栈来模拟系统的递归调用工作栈。该方法通用性强,但本质上还是递归,只不过人工做了本来由编译器做的事情,优化效果不明显。
- 2、用递推来实现递归函数。
- 3、通过变换能将一些递归转化为尾递归,从而迭代求出结果。
后两种方法在时空复杂度上均有较大改善,但其适用范围有限。
大整数的乘法:请设计一个有效的算法,可以进行两个n位大整数的乘法运算。设X ,Y 是两个n位二进制数,
解:以每为乘1次作为1次基本运算,则普通乘法的时间复杂度是
下面考虑分治算法将X 和Y都分成相等的两段,每段
时间复杂度的递推方程如下:
为了降低时间复杂度,必须减少乘法的次数
细节问题:两个
解得:
较大的改进.更快的方法?? 如果将大整数分成更多段,用更复杂的方式把它们组合起来,将有可能得到更优的算法。Strassen矩阵乘法.矩阵乘法问题
对于两个n*n的矩阵A,B,求其乘积.传统方法:
算法复杂度分析
为了降低时间复杂度,必须减少乘法的次数。
算法复杂度分析
由主定理得:
更快的方法??
Hopcroft和Kerr已经证明(1971),计算2个2×2矩阵的乘积,7次乘法是必要的。因此,要想进一步改进矩阵乘法的时间复杂性,就不能再基于计算2×2矩阵的7次乘法这样的方法了。或许应当研究3×3或5×5矩阵的更好算法。
在Strassen之后又有许多算法改进了矩阵乘法的计算时间复杂性。目前最好的计算时间上界是 O(n^2.376)
是否能找到O(n^2)的算法?
伪码如下
Strassen (N,MatrixA,MatrixB,MatrixResult) //splitting input Matrixes, into 4 submatrices each. for i <-0 to N/2 for j <-0 to N/2 A11[i][j] <- MatrixA[i][j]; //a矩阵块 A12[i][j] <- MatrixA[i][j+N/2];//b矩阵块 A21[i][j] <- MatrixA[i+N/2][j]; //c矩阵块 A22[i][j] <- MatrixA[i+N/2][j+N/2];//d矩阵块 B11[i][j] <- MatrixB[i][j]; //e 矩阵块 B12[i][j] <- MatrixB[i][j+N/2]; //f 矩阵块 B21[i][j] <- MatrixB[i+N/2][j]; //g 矩阵块 B22[i][j] <- MatrixB[i+N/2][j+N/2];//h矩阵块 //here we calculate M1..M7 matrices . //递归求M1 HalfSize <- N/2 AResult <- A11+A22 BResult <- B11+B22 Strassen( HalfSize, AResult, BResult, M1 ); //M1=(A11+A22)*(B11+B22) p5=(a+d)*(e+h) //递归求M2 AResult <- A21+A22 Strassen(HalfSize, AResult, B11, M2); //M2=(A21+A22)B11 p3=(c+d)*e //递归求M3 BResult <- B12 - B22 Strassen(HalfSize, A11, BResult, M3); //M3=A11(B12-B22) p1=a*(f-h) //递归求M4 BResult <- B21 - B11 Strassen(HalfSize, A22, BResult, M4); //M4=A22(B21-B11) p4=d*(g-e) //递归求M5 AResult <- A11+A12 Strassen(HalfSize, AResult, B22, M5); //M5=(A11+A12)B22 p2=(a+b)*h //递归求M6 AResult <- A21-A11 BResult <- B11+B12 Strassen( HalfSize, AResult, BResult, M6); //M6=(A21-A11)(B11+B12) p7=(c-a)(e+f) //递归求M7 AResult <- A12-A22 BResult <- B21+B22 Strassen(HalfSize, AResult, BResult, M7); //M7=(A12-A22)(B21+B22) p6=(b-d)*(g+h) //计算结果子矩阵 C11 <- M1 + M4 - M5 + M7; C12 <- M3 + M5; C21 <- M2 + M4; C22 <- M1 + M3 - M2 + M6; //at this point , we have calculated the c11..c22 matrices, and now we are going to //put them together and make a unit matrix which would describe our resulting Matrix. for i<-0 to N/2 for j<-0 to N/2 MatrixResult[i][j]<-C11[i][j]; MatrixResult[i][j+N/2]<-C12[i][j]; MatrixResult[i +N/2][j]<-C21[i][j]; MatrixResult[i +N/2][j+N/2]<-C22[i][j];
完成测试代码
Strassen.h
#ifndef STRASSEN_HH#define STRASSEN_HHtemplate<typename T>class Strassen_class{public: void ADD(T** MatrixA, T** MatrixB, T** MatrixResult, int MatrixSize ); void SUB(T** MatrixA, T** MatrixB, T** MatrixResult, int MatrixSize ); void MUL( T** MatrixA, T** MatrixB, T** MatrixResult, int MatrixSize );//朴素算法实现 void FillMatrix( T** MatrixA, T** MatrixB, int length);//A,B矩阵赋值 void PrintMatrix(T **MatrixA,int MatrixSize);//打印矩阵 void Strassen(int N, T **MatrixA, T **MatrixB, T **MatrixC);//Strassen算法实现};template<typename T>void Strassen_class<T>::ADD(T** MatrixA, T** MatrixB, T** MatrixResult, int MatrixSize ){ for ( int i = 0; i < MatrixSize; i++) { for ( int j = 0; j < MatrixSize; j++) { MatrixResult[i][j] = MatrixA[i][j] + MatrixB[i][j]; } }}template<typename T>void Strassen_class<T>::SUB(T** MatrixA, T** MatrixB, T** MatrixResult, int MatrixSize ){ for ( int i = 0; i < MatrixSize; i++) { for ( int j = 0; j < MatrixSize; j++) { MatrixResult[i][j] = MatrixA[i][j] - MatrixB[i][j]; } }}template<typename T>void Strassen_class<T>::MUL( T** MatrixA, T** MatrixB, T** MatrixResult, int MatrixSize ){ for (int i=0;i<MatrixSize ;i++) { for (int j=0;j<MatrixSize ;j++) { MatrixResult[i][j]=0; for (int k=0;k<MatrixSize ;k++) { MatrixResult[i][j]=MatrixResult[i][j]+MatrixA[i][k]*MatrixB[k][j]; } } }}/**c++使用二维数组,申请动态内存方法*申请*int **A;*A = new int *[desired_array_row];*for ( int i = 0; i < desired_array_row; i++)* A[i] = new int [desired_column_size];*释放*for ( int i = 0; i < your_array_row; i++)* delete [] A[i];*delete[] A;*/template<typename T>void Strassen_class<T>::Strassen(int N, T **MatrixA, T **MatrixB, T **MatrixC){ int HalfSize = N/2; int newSize = N/2; if ( N <= 64 ) //分治门槛,小于这个值时不再进行递归计算,而是采用常规矩阵计算方法 { MUL(MatrixA,MatrixB,MatrixC,N); } else { T** A11; T** A12; T** A21; T** A22; T** B11; T** B12; T** B21; T** B22; T** C11; T** C12; T** C21; T** C22; T** M1; T** M2; T** M3; T** M4; T** M5; T** M6; T** M7; T** AResult; T** BResult; //making a 1 diminsional pointer based array. A11 = new T *[newSize]; A12 = new T *[newSize]; A21 = new T *[newSize]; A22 = new T *[newSize]; B11 = new T *[newSize]; B12 = new T *[newSize]; B21 = new T *[newSize]; B22 = new T *[newSize]; C11 = new T *[newSize]; C12 = new T *[newSize]; C21 = new T *[newSize]; C22 = new T *[newSize]; M1 = new T *[newSize]; M2 = new T *[newSize]; M3 = new T *[newSize]; M4 = new T *[newSize]; M5 = new T *[newSize]; M6 = new T *[newSize]; M7 = new T *[newSize]; AResult = new T *[newSize]; BResult = new T *[newSize]; int newLength = newSize; //making that 1 diminsional pointer based array , a 2D pointer based array for ( int i = 0; i < newSize; i++) { A11[i] = new T[newLength]; A12[i] = new T[newLength]; A21[i] = new T[newLength]; A22[i] = new T[newLength]; B11[i] = new T[newLength]; B12[i] = new T[newLength]; B21[i] = new T[newLength]; B22[i] = new T[newLength]; C11[i] = new T[newLength]; C12[i] = new T[newLength]; C21[i] = new T[newLength]; C22[i] = new T[newLength]; M1[i] = new T[newLength]; M2[i] = new T[newLength]; M3[i] = new T[newLength]; M4[i] = new T[newLength]; M5[i] = new T[newLength]; M6[i] = new T[newLength]; M7[i] = new T[newLength]; AResult[i] = new T[newLength]; BResult[i] = new T[newLength]; } //splitting input Matrixes, into 4 submatrices each. for (int i = 0; i < N / 2; i++) { for (int j = 0; j < N / 2; j++) { A11[i][j] = MatrixA[i][j]; A12[i][j] = MatrixA[i][j + N / 2]; A21[i][j] = MatrixA[i + N / 2][j]; A22[i][j] = MatrixA[i + N / 2][j + N / 2]; B11[i][j] = MatrixB[i][j]; B12[i][j] = MatrixB[i][j + N / 2]; B21[i][j] = MatrixB[i + N / 2][j]; B22[i][j] = MatrixB[i + N / 2][j + N / 2]; } } //here we calculate M1..M7 matrices . //M1[][] ADD( A11,A22,AResult, HalfSize); ADD( B11,B22,BResult, HalfSize); //p5=(a+d)*(e+h) Strassen( HalfSize, AResult, BResult, M1 ); //now that we need to multiply //this , we use the strassen itself. //M2[][] ADD( A21,A22,AResult, HalfSize); //M2=(A21+A22)B11,p3=(c+d)*e Strassen(HalfSize, AResult, B11, M2); //Mul(AResult,B11,M2); //M3[][] SUB( B12,B22,BResult, HalfSize); //M3=A11(B12-B22),p1=a*(f-h) Strassen(HalfSize, A11, BResult, M3); //Mul(A11,BResult,M3); //M4[][] SUB( B21, B11, BResult, HalfSize); //M4=A22(B21-B11),p4=d*(g-e) Strassen(HalfSize, A22, BResult, M4); //Mul(A22,BResult,M4); //M5[][] ADD( A11, A12, AResult, HalfSize); //M5=(A11+A12)B22 ,p2=(a+b)*h Strassen(HalfSize, AResult, B22, M5); //Mul(AResult,B22,M5); //M6[][] SUB( A21, A11, AResult, HalfSize); ADD( B11, B12, BResult, HalfSize); //M6=(A21-A11)(B11+B12),p7=(c-a)(e+f) Strassen( HalfSize, AResult, BResult, M6); //Mul(AResult,BResult,M6); //M7[][] SUB(A12, A22, AResult, HalfSize); ADD(B21, B22, BResult, HalfSize); //M7=(A12-A22)(B21+B22),p6=(b-d)*(g+h) Strassen(HalfSize, AResult, BResult, M7); //Mul(AResult,BResult,M7); //C11 = M1 + M4 - M5 + M7; ADD( M1, M4, AResult, HalfSize); SUB( M7, M5, BResult, HalfSize); ADD( AResult, BResult, C11, HalfSize); //C12 = M3 + M5; ADD( M3, M5, C12, HalfSize); //C21 = M2 + M4; ADD( M2, M4, C21, HalfSize); //C22 = M1 + M3 - M2 + M6; ADD( M1, M3, AResult, HalfSize); SUB( M6, M2, BResult, HalfSize); ADD( AResult, BResult, C22, HalfSize); //at this point , we have calculated the c11..c22 matrices, and now we are going to //put them together and make a unit matrix which would describe our resulting Matrix. //组合小矩阵到一个大矩阵 for (int i = 0; i < N/2 ; i++) { for (int j = 0 ; j < N/2 ; j++) { MatrixC[i][j] = C11[i][j]; MatrixC[i][j + N / 2] = C12[i][j]; MatrixC[i + N / 2][j] = C21[i][j]; MatrixC[i + N / 2][j + N / 2] = C22[i][j]; } } // 释放矩阵内存空间 for (int i = 0; i < newLength; i++) { delete[] A11[i];delete[] A12[i];delete[] A21[i]; delete[] A22[i]; delete[] B11[i];delete[] B12[i];delete[] B21[i]; delete[] B22[i]; delete[] C11[i];delete[] C12[i];delete[] C21[i]; delete[] C22[i]; delete[] M1[i];delete[] M2[i];delete[] M3[i];delete[] M4[i]; delete[] M5[i];delete[] M6[i];delete[] M7[i]; delete[] AResult[i];delete[] BResult[i] ; } delete[] A11;delete[] A12;delete[] A21;delete[] A22; delete[] B11;delete[] B12;delete[] B21;delete[] B22; delete[] C11;delete[] C12;delete[] C21;delete[] C22; delete[] M1;delete[] M2;delete[] M3;delete[] M4;delete[] M5; delete[] M6;delete[] M7; delete[] AResult; delete[] BResult ; }//end of else}template<typename T>void Strassen_class<T>::FillMatrix( T** MatrixA, T** MatrixB, int length){ for(int row = 0; row<length; row++) { for(int column = 0; column<length; column++) { MatrixB[row][column] = (MatrixA[row][column] = rand() %5); //matrix2[row][column] = rand() % 2;//ba hazfe in khat 50% afzayeshe soorat khahim dasht } }}template<typename T>void Strassen_class<T>::PrintMatrix(T **MatrixA,int MatrixSize){ cout<<endl; for(int row = 0; row<MatrixSize; row++) { for(int column = 0; column<MatrixSize; column++) { cout<<MatrixA[row][column]<<"\t"; if ((column+1)%((MatrixSize)) == 0) cout<<endl; } } cout<<endl;}#endif //Strassen.h
Strassen.cpp
#include <iostream>#include <ctime>#include <Windows.h>using namespace std;#include "Strassen.h"int main(){ Strassen_class<int> stra;//定义Strassen_class类对象 int MatrixSize = 0; int** MatrixA; //存放矩阵A int** MatrixB; //存放矩阵B int** MatrixC; //存放结果矩阵 clock_t startTime_For_Normal_Multipilication ; clock_t endTime_For_Normal_Multipilication ; clock_t startTime_For_Strassen ; clock_t endTime_For_Strassen ; srand(time(0)); cout<<"\n请输入矩阵大小(必须是2的幂指数值(例如:32,64,512,..): "; cin>>MatrixSize; int N = MatrixSize;//for readiblity. //申请内存 MatrixA = new int *[MatrixSize]; MatrixB = new int *[MatrixSize]; MatrixC = new int *[MatrixSize]; for (int i = 0; i < MatrixSize; i++) { MatrixA[i] = new int [MatrixSize]; MatrixB[i] = new int [MatrixSize]; MatrixC[i] = new int [MatrixSize]; } stra.FillMatrix(MatrixA,MatrixB,MatrixSize); //矩阵赋值 //*******************conventional multiplication test cout<<"朴素矩阵算法开始时钟: "<< (startTime_For_Normal_Multipilication = clock()); stra.MUL(MatrixA,MatrixB,MatrixC,MatrixSize);//朴素矩阵相乘算法 T(n) = O(n^3) cout<<"\n朴素矩阵算法结束时钟: "<< (endTime_For_Normal_Multipilication = clock()); cout<<"\n矩阵运算结果... \n"; stra.PrintMatrix(MatrixC,MatrixSize); //*******************Strassen multiplication test cout<<"\nStrassen算法开始时钟: "<< (startTime_For_Strassen = clock()); stra.Strassen( N, MatrixA, MatrixB, MatrixC ); //strassen矩阵相乘算法 cout<<"\nStrassen算法结束时钟: "<<(endTime_For_Strassen = clock()); cout<<"\n矩阵运算结果... \n"; stra.PrintMatrix(MatrixC,MatrixSize); cout<<"矩阵大小 "<<MatrixSize; cout<<"\n朴素矩阵算法: "; cout<<(endTime_For_Normal_Multipilication-startTime_For_Normal_Multipilication); cout<<" Clocks.."<<(endTime_For_Normal_Multipilication- \\ startTime_For_Normal_Multipilication)/CLOCKS_PER_SEC<<" Sec"; cout<<"\nStrassen算法:"<<(endTime_For_Strassen-startTime_For_Strassen)<<" Clocks.." cout<<(endTime_For_Strassen-startTime_For_Strassen)/CLOCKS_PER_SEC<<" Sec\n"; system("Pause"); return 0;}
典型实例
归并排序
将待排序序列R[0…n-1]看成是n个长度为1的有序序列,将相邻的有序表成对归并,得到n/2个长度为2的有序表;将这些有序序列再次归并,得到n/4个长度为4的有序序列;如此反复进行下去,最后得到一个长度为n的有序序列。归并排序其实要做两件事:“分解”——将序列每次折半划分。“合并”——将划分后的序列段两两合并后排序。
实现
#include <iostream>#include <vector>#include <iterator>#include <algorithm>#include <cstdio>using namespace std;template <class T>void merge(vector<T>& arr,int start ,int middle,int end){ int n1 = middle - start + 1; int n2 = end - middle; vector<T> left(n1); vector<T> right(n2); int i,j,k; for (i = 0;i < n1; ++ i) left[i] = arr[start + i]; for (j = 0;j < n2; ++ j) right[j] = arr[middle + j + 1]; i = j = 0; k = start; while (i < n1 && j < n2) { if (left[i] < right[j]) arr[k ++] = left[i ++]; else arr[k ++] = right[j ++]; } while (i < n1) arr[k ++] = left[i ++ ]; while (j < n2) arr[k ++] = right[j ++];}template <class T>void sort(vector<T>& arr,int start,int end){ // getchar(); if (start < end) { int middle = (start + end) / 2; sort(arr,start,middle); sort(arr,middle + 1,end); merge(arr,start,middle,end); }}int main(){ const int length = 20; vector<int> arr(length); for (size_t i = 0;i < arr.size(); ++ i) arr[i] = i; random_shuffle(arr.begin(),arr.end()); copy(arr.begin(),arr.end(),ostream_iterator<int>(cout, " ")); cout << endl; sort(arr,0,length - 1); copy(arr.begin(),arr.end(),ostream_iterator<int>(cout, " ")); cout << endl; return 0;}
整数划分问题
将正整数n表示成一系列正整数之和:
例如:正整数6有如下11种不同的划分,所以p(6) = 11:
6;5+1;4+2,4+1+1;3+3,3+2+1,3+1+1+1;2+2+2,2+2+1+1,2+1+1+1+1;1+1+1+1+1+1。
前面的几个例子中,问题本身都具有比较明显的递归关系,因而容易用递归函数直接求解。
在本例中,如果设p(n)为正整数n的划分数,则难以找到递归关系,因此考虑增加一个自变量:在正整数n的所有不同划分中,将最大加数n1不大于m的划分个数记作q(n,m)。可以建立q(n,m)的如下递归关系。
- (1) q(n,1)=1,n >= 1;当最大加数n1不大于1时,任何正整数n只有一种划分形式,即n = 1 + 1 + 1 + … +1.
- (2) q(n,m) = q(n,n),m >= n; 最大加数n1实际上不能大于n。因此,q(1,m)=1。
- (3) q(n,n)=1 + q(n,n-1); 正整数n的划分由n1=n的划分和n1 ≤ n-1的划分组成。
- (4) q(n,m)=q(n,m-1)+q(n-m,m),n > m >1;正整数n的最大加数n1不大于m的划分由 n1 = m的划分和n1 ≤ m-1 的划分组成。
前面的一个例子中,问题本身都具有比较明显的递归关系,因而容易用递归函数直接求解。
在本例中,如果设p(n)为正整数n的划分数,则难以找到递归关系,因此考虑增加一个自变量:将最大加数n1不大于m的划分个数记作
正整数n的划分数
实现:
#include <iostream>using namespace std;//int __int_partition(int n,int m){ if (n < 1 || m < 1) return 0; if (n == 1 || m == 1) return 1; if (n < m) return __int_partition(n,n); if (n == m) return __int_partition(n,m - 1) + 1; return __int_partition(n,m - 1) + __int_partition(n - m,m);}int integer_partition(int n){ return __int_partition(n,n);}int main(){ for (int i = 1; i < 7; ++ i) { cout << "integer_patition(" << i << ") = " << integer_partition(i) << endl; } return 0;}
阶乘函数
阶乘函数可递归地定义为:
边界条件与递归方程是递归函数的二个要素,递归函数只有具备了这两个要素,才能在有限次计算后得出结果。
实现:
#include <iostream>using namespace std;// factorial implement by recursivelong factorial_recursive(long n){ if (n == 0) return 1; return n * factorial_recursive(n-1);}// factorial implement by looplong factorial_loop(long n){ long result = 1; for (int i = n; i > 0; -- i) result *= i; return result;}int main(){ for (int i = 0; i < 10; i ++ ) { cout << i << "!" << " = " << factorial_recursive(i) << "," << factorial_loop(i) << endl; } return 0;}
排列问题
设计一个递归算法生成
- 当n=1时,perm(R)=(r),其中r是集合R中唯一的元素;
- 当n>1时,perm(R)由(r1)perm(R1),(r2)perm(R2),…,(rn)perm(Rn)构成。
#include <iostream>#include <vector>#include <iterator>using namespace std;/* 使用递归实现* 递归产生所有前缀是list[0:k-1],且后缀是list[k,m]的全排列的所有排列.调用算法perm(list,0,n-1)则产生list[0:n-1]的全排列*/template <class T>void perm_recursion(T list[],int k,int m){ // 产生list[k:m]的所有排列 if (k == m) { for (int i = 0; i <= m; i ++) cout << list[i] << " "; cout << endl; } else { // 还有多个元素,递归产生排列 for (int i = k; i <= m; ++ i) { swap(list[k],list[i]); perm_recursion(list,k+1,m); swap(list[k],list[i]); } }}// 非递归实现(可参照STL next_permutation源码)template <class T>void perm_loop(T list[],int len){ int i,j; vector<int> v_temp(len); // 初始排列 for(i = 0; i < len ; i ++) v_temp[i] = i; while (true) { for (i = 0; i < len; i ++ ) cout << list[v_temp[i]] << " "; cout << endl; // 从后向前查找,看有没有后面的数大于前面的数的情况,若有则停在后一个数的位置。 for(i = len - 1;i > 0 && v_temp[i] < v_temp[i-1] ; i--); if (i == 0) break; // 从后查到i,查找大于 v_temp[i-1]的最小的数,记入j for(j = len - 1 ; j > i && v_temp[j] < v_temp[i-1] ; j--); // 交换 v_temp[i-1] 和 v_temp[j] swap(v_temp[i-1],v_temp[j]); // 倒置v_temp[i]到v_temp[n-1] for(i = i,j = len - 1 ; i < j;i ++,j --) { swap(v_temp[i],v_temp[j]); } }}int main(){ int list[] = {0,1,2}; cout << "permutation implement by recursion: " << endl; perm_recursion(list,0,2); cout << endl; cout << "permutation implement by loop: " << endl; perm_loop(list,3); cout << endl; return 0;}
Hanoi塔问题
设a,b,c是3个塔座。开始时,在塔座a上有一叠共n个圆盘,这些圆盘自下而上,由大到小地叠在一起。各圆盘从小到大编号为1,2,…,n,现要求将塔座a上的这一叠圆盘移到塔座b上,并仍按同样顺序叠置。在移动圆盘时应遵守以下移动规则:
- 规则1:每次只能移动1个圆盘;
- 规则2:任何时刻都不允许将较大的圆盘压在较小的圆盘之上;
- 规则3:在满足移动规则1和2的前提下,可将圆盘移至a,b,c中任一塔座上。
#include <iostream>using namespace std;void __move(char t1,char t2){ cout << t1 << " -> " << t2 << endl;}// 把n个圆盘,从t1塔移至t2塔通过t3塔void hanoi(int n,char t1,char t2,char t3){ if (n > 0) { hanoi(n-1,t1,t3,t2); __move(t1,t2); hanoi(n-1,t3,t2,t1); }}int main(){ cout << "hanoi(1,'a','b','c'): " << endl; hanoi(1,'a','b','c'); cout << endl; cout << "hanoi(1,'a','b','c'): " << endl; hanoi(2,'a','b','c'); cout << endl; cout << "hanoi(3,'a','b','c'): " << endl; hanoi(3,'a','b','c'); cout << endl; return 0;}
棋盘覆盖
在一个
分析
当k>0时,将
特殊方格必位于4个较小子棋盘之一中,其余3个子棋盘中无特殊方格。为了将这3个无特殊方格的子棋盘转化为特殊棋盘,可以用一个L型骨牌覆盖这3个较小棋盘的会合处,从而将原问题转化为4个较小规模的棋盘覆盖问题。递归地使用这种分割,直至棋盘简化为棋盘1×1。
算法复杂度
总共要
实现
#include <iostream>#include <vector>#include <cmath>#include <iterator>using namespace std;void __chessboard_cover(vector<vector<int> >& cheb, int tx,int ty, int dx,int dy, int size, int& tile);/* 棋盘覆盖主函数* cheb: 棋盘数组 dx: 特殊方格的横坐标 dy: 特殊方格的纵坐标*/void chessboard_cover(vector<vector<int> >& cheb,int dx,int dy){ int tile = 1; __chessboard_cover(cheb,0,0,dx,dy,cheb.size(),tile);}/* 棋盘覆盖辅助函数* cheb: 棋盘数组 tx: 起始横坐标 ty: 起始纵坐标* dx: 特殊方格的横坐标 dy: 特殊方格的横坐标 size: 棋盘大小 tile: 骨牌编号*/void __chessboard_cover(vector<vector<int> >& cheb, int tx,int ty, int dx,int dy, int size, int& tile){ if (size == 1) return ; int t = tile ++ ; // L骨牌号 int s = size / 2; // 分割棋盘 // 覆盖左上角子棋盘 if (dx < tx + s && dy < ty + s) { // 特殊方格在此子棋盘中 __chessboard_cover(cheb,tx,ty,dx,dy,s,tile); } else { // 此棋盘中无特殊方格,用t号骨牌覆盖下角方格 cheb[tx + s - 1][ty + s - 1] = t; // 覆盖其余方格 __chessboard_cover(cheb,tx,ty,tx + s - 1, ty + s - 1,s,tile); } // 覆盖右上角子棋盘 if (dx >= tx + s && dy < ty + s) { // 特殊方格在此棋盘中 __chessboard_cover(cheb,tx + s,ty,dx,dy,s,tile); } else { // 用t号L型骨牌覆盖左下角 cheb[tx + s][ty + s - 1] = t; __chessboard_cover(cheb,tx + s,ty,tx + s,ty + s - 1,s,tile); } // 覆盖左下角子棋盘 if (dx < tx + s && dy >= ty + s) { // 特殊方格在此棋盘中 __chessboard_cover(cheb,tx,ty + s,dx,dy,s,tile); } else { // 用t号L型骨牌覆盖右上角 cheb[tx + s - 1][ty + s] = t; __chessboard_cover(cheb,tx,ty + s,tx + s - 1,ty + s,s,tile); } // 覆盖右下角子棋盘 if (dx >= tx + s && dy >= ty + s) { // 特殊方格在此棋盘中 __chessboard_cover(cheb,tx + s,ty + s,dx,dy,s,tile); } else { // 用t号L型骨牌覆盖左上角 cheb[tx + s][ty + s] = t; __chessboard_cover(cheb,tx + s,ty + s,tx + s,ty + s,s,tile); }}int main(){ int k = 2; int size = pow (2,k); vector<vector<int> > cheb(size); for (size_t i= 0 ;i < cheb.size(); ++i) { cheb[i].resize(size); } for (int i = 0; i < size; ++ i) { for (int j = 0;j < size; ++ j) { int dx = i; int dy = j; cout << "dx = " << dx << " , dy = " << dy << endl; cheb[dx][dy] = 0; chessboard_cover(cheb,dx,dy); for (size_t i = 0;i < cheb.size(); ++ i) { copy(cheb[i].begin(),cheb[i].end(),ostream_iterator<int>(cout," ")); cout << endl; } cout << endl; } } return 0;}
循环赛日程表
题目表述:
设有
- (1)每个选手必须与其他
n−1 个选手各赛一次; - (2)每个选手一天只能赛一次;
- (3)循环赛一共进行
n−1 天。
按分治策略,将所有的选手分为两半,n个选手的比赛日程表就可以通过为n/2个选手设计的比赛日程表来决定。递归地用对选手进行分割,直到只剩下2个选手时,比赛日程表的制定就变得很简单。这时只要让这2个选手进行比赛就可以了。
实现
#include <iostream>#include <vector>#include <cmath>#include <iterator>#include <iomanip>using namespace std;void __table(vector<vector<int> >& arr,int start,int end);void round_match_table(vector<vector<int> >& arr){ int count = arr.size(); for (int i = 0;i < count;++ i) { arr[0][i] = i + 1; } __table(arr,0,count-1);}void __table(vector<vector<int> >& arr,int start,int end){ if (end - start + 1 == 2) { arr[1][start] = arr[0][end]; arr[1][end] = arr[0][start]; return ; } int half = (end - start + 1) / 2; // 左上角 __table(arr,start,start + half -1 ); // 右上角 __table(arr,start + half,end); // 左下角 for (int i = 0;i < half; ++ i) { for (int j = start; j <= end; ++ j) { arr[i + half][j-half] = arr[i][j]; } } // 右下角(其实左下角和右下角可以在上一个循环中实现的, // 但是为了算法结构清晰,因此分为两个循环) for (int i = 0;i < half; ++ i) { for (int j = start; j < end; ++j) { arr[i + half][j + half] = arr[i][j]; } }}int main(){ int k = 4; int size = pow(2,k); vector<vector<int> > arr(size); for (int i = 0; i < size; ++ i) { arr[i].resize(size); } round_match_table(arr); for (int i = 0;i < size; ++ i) { for (int j = 0;j < size; ++ j) { cout << std::setw(3) << arr[i][j]; } cout << endl; } return 0;}
线性时间选择
给定线性序集中n个元素和一个整数k,
// 在数组a的p到r区间内找到第k小的元素template<class Type>Type RandomizedSelect(Type a[],int p,int r,int k){ if (p == r) return a[p]; // 如果p,r相等,第n小都是a[p] // 数组a[p:r]被随机分成两个部分,a[p:i]和a[i+1:r], // 使得a[p:i]中的元素都小于a[i+1:r]中的元素。 int i = RandomizedPartition(a,p,r); j = i - p + 1; if (k <= j) return RandomizedSelect(a,p,i,k); else return RandomizedSelect(a,i+1,r,k-j);}
在最坏情况下,算法
如果能在线性时间内找到一个划分基准,使得按这个基准所划分出的2个子数组的长度都至多为原数组长度的ε倍(0<ε<1是某个正常数),那么就可以在最坏情况下用O(n)时间完成选择任务。
例如,若
步骤
- 第一步,将n个输入元素划分成én/5ù个组,每组5个元素,只可能有一个组不是5个元素。用任意一种排序算法,将每组中的元素排好序,并取出每组的中位数,共én/5ù个。
- 第二步,递归调用select来找出这én/5ù个元素的中位数。如果én/5ù是偶数,就找它的2个中位数中较大的一个。以这个元素作为划分基准。
分析
伪码描述如下:
Type Select(Type a[], int p, int r, int k){if (r - p < 75) { // 问题的规模足够小,用某个简单排序算法对数组a[p:r]排序; return a[p + k - 1]; }for (int i = 0; i <= ( r - p - 4 ) / 5 ; i ++ ) { 将a[p + 5 * i]至a[p + 5 * i + 4]的第3小元素与a[p+i]交换位置;} // 找中位数的中位数,r - p - 4即上面所说的n - 5Type x = Select(a, p, p + (r - p - 4 ) / 5, (r - p - 4) / 10);// 数据n根据x划分开来int i = Partition(a,p,r,x); j = i - p + 1;if (k <= j) return Select(a,p,i,k);else return Select(a,i+1,r,k-j);}
算法复杂度分析
解得时间复杂度是
实现
#include <iostream>#include <vector>#include <algorithm>#include <iterator>using namespace std;/* 线性时间查找* arr: 数据存储数组 start:开始查找点 end: 结束查找点 n: 查找第n小(n = 1,2,3,...,end-start+1)*/template <class T>T linear_time_select(vector<T>& arr,int start,int end,int n){ if (end - start < 75) { sort (arr.begin() + start,arr.begin() + end + 1); return arr[start + n - 1]; } for (int i = 0;i < (end - start - 4) / 5; ++ i) { sort (arr.begin() + start + 5 * i,arr.begin() + start + 5 * i + 5); swap (*(arr.begin() + start + 5 * i + 2),*(arr.begin() + start + i)); } // 找到中位数的中位数 T median = linear_time_select(arr,start,start + (end-start-4)/5-1,(end-start-4)/10+1); // 数据 arr 根据 median 划分开来 int middle = __partition_by_median(arr,start,end,median); int distance = middle-start+1; // 中位数的位置与start的距离 if (n <= distance) return linear_time_select(arr,start,middle,n);// 在前半截 else return linear_time_select(arr,middle + 1,end,n - distance);// 在后半截}// 将arr按照值median划分开来,并返回界限的位置template <class T>int __partition_by_median(vector<T> &arr,int start,int end,T median){ while (true) { while (true) { if (start == end) return start; else if (arr[start] < median) ++ start; else break; } while (true) { if (start == end) return end; else if (arr[end] > median) { -- end; } else break; } swap(arr[start],arr[end]); }}int main(){ vector<int> arr; const int c = 2000; for (int i = 0;i < c; ++ i) { arr.push_back(i); } // 随机排列 random_shuffle(arr.begin(),arr.end()); for (int i = 1; i < c+1; ++ i) { cout << "find the " << i << " element,position is " << linear_time_select(arr,0,c-1,i) << endl; } return 0;}
参考资料
- Donald E.Knuth 著,苏运霖 译,《计算机程序设计艺术,第1卷基本算法》,国防工业出版社,2002年
- Donald E.Knuth 著,苏运霖 译,《计算机程序设计艺术,第2卷半数值算法》,国防工业出版社,2002年
- Donald E.Knuth 著,苏运霖 译,《计算机程序设计艺术,第3卷排序与查找》,国防工业出版社,2002年
- Thomas H. Cormen, Charles E.Leiserson, etc., Introduction to Algorithms(3rd edition), McGraw-Hill Book Company,2009
- Jon Kleinberg, Ėva Tardos, Algorithm Design, Addison Wesley, 2005.
- Sartaj Sahni ,《数据结构算法与应用:C++语言描述》 ,汪诗林等译,机械工业出版社,2000.
- 屈婉玲,刘田,张立昂,王捍贫,算法设计与分析,清华大学出版社,2011年版,2013年重印.
- 张铭,赵海燕,王腾蛟,《数据结构与算法实验教程》,高等教育出版社,2011年 1月
- 分治策略Divide and Conquer
- Divide & Conquer 分治策略
- 分治 Divide and Conquer
- 分治算法(divide and conquer)
- 分治法(divide-and-conquer)
- 分治算法Divide-and-Conquer概述
- 分治算法 Divide and Conquer Algorithm
- 算法思想系列--分治策略(Divide-Conquer-Combine)(待补充)
- 新鲜出炉 --分治排序 (Divide-and-conquer)
- 分治 Divide and Conquer 局部最小值 local optimal 棋盘问题
- 漫谈算法(四)分治算法 Divide and Conquer Algorithm
- 分治算法(Divide-and-Conquer Algorithm)一
- 分治算法(Divide-and-Conquer Algorithm)二
- 漫谈算法(四)分治算法 Divide and Conquer Algorithm
- Introduction of Algorithm - Divide and Conquer(分治法)
- 算法第二周: Divide and Conquer-分治算法
- 分治法——树的遍历(Divide and Conquer
- 分治法——主定理(Divide and Conquer
- 【计算机组成原理】简介
- Velocity教程
- &&与||到底返回什么~,&&or&
- dfs+避免重复-幸运的袋子(网易题)
- CentOS 7 上systemctl 的用法
- 分治策略Divide and Conquer
- log4j问题
- java常用的快捷方法!
- SpringBoot应用部署到Tomcat中无法启动问题
- 轻量级分布式 RPC 框架
- Java 里面<T>T 是什么意思
- GridView实现省市县三级联动
- Android App的设计架构:MVC,MVP,MVVM与架构经验谈
- 空间直线最小二乘拟合