在計算機科學中,分治法是一種很重要的算法。字面上的解釋是“分而治之”,通常是遞歸算法,就是 把一個複雜的問題分成兩個或更多的相同或相似的子問題,再把子問題分成更小的子問題……直到最後子問題可以簡單的直接求解,原問題的解即子問題的解的合併。這個技巧是很多高效算法的基礎,如排序算法(快速排序,歸併排序),傅立葉變換。
任何一個可以用計算機求解的問題所需的計算時間都與其規模有關。問題的規模越小,越容易直接求解,解題所需的計算時間也越少。例如,對於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索引,查找失敗返回-1
template <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 recursive
long fibonacci_recursive(long n)
{
if (n <= 1 )
return 1;
return fibonacci_recursive(n - 1)
+ fibonacci_recursive(n - 2);
}
// fibonacci implement by loop
long 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_HH
template<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 recursive
long factorial_recursive(long n)
{
if (n == 0)
return 1;
return n * factorial_recursive(n-1);
}
// factorial implement by loop
long 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 - 5
Type 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月