算法篇-2-分治思想-棋盘覆盖&归并排序&Strasssen矩阵乘法&循环赛安排
2016-12-27 21:33
387 查看
本系列所有代码https://github.com/YIWANFENG/Algorithm-github
人们在大量实践中发现,再用分治法时,最好使子问题规模大致相同。即将一个问题分为大小相等的k个子问题的处理方法是比较有效的。(PS:以上为抄书)
根据以上可得一般分治算法设计模式
Divide-and-conquer(P) {
If (|P|<=n0)abhoc(P);//分治法中基本函数abhoc求解
Divide P intosmaller subinstances P1,P2,...Pk;
For(int i=1;i<=k; ++i) {
Rul[i] = Divide-and-conquer(Pi);
}
Retuernmerge(Rul[1], Rul[2], Rul[3]...);//合并结果的算法
}
PS:是不是很类似于递归?:-D
线性代数里一般方法:
矩阵A、B、C (n*n方阵)
C(i,j) =∑ A(i,k)*B(k,j) (从k=1至k=n求和)
而分治法,将每一矩阵都分块成4个大小相等的子矩阵,每个子矩阵都是n/2*n/2的方阵,由此C=AB
分治降阶,子矩阵阶数大于2时,可将其分块,直到子矩阵的阶为2。计算2个n阶方阵的乘积转化为计算8个n/2阶方阵的乘积和4个n/2阶方阵的加法。
时间复杂度:T(n) =O(1) n =2 ,T(n) = 8T(n/2)+O(n^2) n>2
So,T(n) = O(n^3)不比原始定义方法直接计算更有效
所以改进计算方法(减少乘法)为
时间复杂度:T(n) =O(1) n =2 ,T(n) = 7T(n/2)+O(n^2) n>2
所以看出这段改进后代码中还含有大于2阶的矩阵乘法,所以调用自身,递归实现分治。
示例代码为:
//PS:大段代码是矩阵复制
在2^k*2^k个方格组成的棋盘中,有一个选中的特殊方格,用L形骨牌覆盖棋盘所有,不可互相重叠,不可覆盖特殊方格。
当K>0时,将2^k*2^k棋盘分割为4个2^(k-1)*2^(k-1)子棋盘,特殊方格位于其中之一,将其余三个正常棋盘转化为特殊棋盘,用一L形骨牌覆盖其余三个,从而将原问题转化为4个较小规模的同类问题,递归使用这种分割即可。
代码示例:
归并排序有一种改进版--自然归并排序,思想是先扫描一遍要排序的数组,然后的出来已经有序的数组下标,依据这排好序的数组下标来进行归并排序。代码详见github@YIWANFENG
(每次合并子数组前都需要重复执行扫描数组来获得当前数组的有序数组下标,无法有效利用之前的扫描。缺点还有每次排完序需要将辅助数组拷贝回原数组,有点浪费时间。)
用一次线性扫描找到排好序的子数组,记录他们在原数组中的位置。然后与上一合并排序一样,合并相邻的两个有序子数组,构成更大的有序数组,不断重复,直至整个数组有序。
首先分析,如果该序列已经线性排列,排在第k位的元素就是要找的元素。
模仿快排,再输入数组进行递归划分,但是只需要处理划分出的子数组之一,因为只要整体大致有序,并不需要完全有序。
数组被a[ start :end ] 划分为两个子数组a[start : i ] 与 a[ i+1: end],其中a[start:i]中每个元素都不大于a[i+1:end] 的每个元素。若k <= j (j = i - start +1)则可知所求元素在a[start:i]
否则在a[i+1:end]中,并且为该数组中第k-j小的元素。
上述方法最坏情况下时间复杂度为(n^2),但随机下一般为O(n)
下面讨论一最坏情况下可以在O(n)的选择算法(此法分析不全)
努力去找一种划分,可以使新数组长度为原数组*x of (0,1)。
(1)将n个元素划分为【n/5】个组,每组5个元素或更少,将其排序取出每组中位mid[n/5]。
(2)递归调用Select找出这mid[n/5]个元素的中位数,依次元素作为划分基准。
只要等于基准元素的元素不太多,那么两个分组就不会差太远。
依照分治,将运动员分为2部分,那么n个选手的安排可用n/2个选手的安排确定,即排列n/2,另外的n/2选手抄袭另外的n/2选手的安排。当只有两个选手时即可指定返回(即如下图对称照抄,原因是前面的人安排好了,那么换为另一批人套入后也应安排合适)。
PS:若n%2==1,比赛进行n天,其他条件不变,则可多添加一虚拟人参赛,拍赛完毕后与虚拟人比赛的运动员比赛日休息即可。
分治思想
分治法基本思想是将一个规模为n的问题分解为k个规模较小的子问题,这些问题互相独立且与原问题相同。递归解决这些问子问题,然后各子问题结果合并得到原问题解。人们在大量实践中发现,再用分治法时,最好使子问题规模大致相同。即将一个问题分为大小相等的k个子问题的处理方法是比较有效的。(PS:以上为抄书)
根据以上可得一般分治算法设计模式
Divide-and-conquer(P) {
If (|P|<=n0)abhoc(P);//分治法中基本函数abhoc求解
Divide P intosmaller subinstances P1,P2,...Pk;
For(int i=1;i<=k; ++i) {
Rul[i] = Divide-and-conquer(Pi);
}
Retuernmerge(Rul[1], Rul[2], Rul[3]...);//合并结果的算法
}
PS:是不是很类似于递归?:-D
Strasssen矩阵乘法
以分治法分析Strasssen矩阵乘法线性代数里一般方法:
矩阵A、B、C (n*n方阵)
C(i,j) =∑ A(i,k)*B(k,j) (从k=1至k=n求和)
而分治法,将每一矩阵都分块成4个大小相等的子矩阵,每个子矩阵都是n/2*n/2的方阵,由此C=AB
分治降阶,子矩阵阶数大于2时,可将其分块,直到子矩阵的阶为2。计算2个n阶方阵的乘积转化为计算8个n/2阶方阵的乘积和4个n/2阶方阵的加法。
时间复杂度:T(n) =O(1) n =2 ,T(n) = 8T(n/2)+O(n^2) n>2
So,T(n) = O(n^3)不比原始定义方法直接计算更有效
所以改进计算方法(减少乘法)为
时间复杂度:T(n) =O(1) n =2 ,T(n) = 7T(n/2)+O(n^2) n>2
所以看出这段改进后代码中还含有大于2阶的矩阵乘法,所以调用自身,递归实现分治。
示例代码为:
//PS:大段代码是矩阵复制
template<class T_> void MatrixMulti(T_ *A,T_ *B, T_ *C , int row, int col) { ///任意矩阵相乘 ///input B , C ///output A for(int i=0; i<row; ++i) { for(int j=0;j<col; ++j) { *(A+i*col+j) = 0; for(int k=0; k<col; ++k) { *(A+i*col+j)+= (*(B+col*i+k))*(*(C+row*k+j)); } } } } template<class T_> void MatrixAdd(T_ *A, T_*B, T_ *C , int row, int col,int type) { //type=1,执行+ /// type=-1,执行- for(int i=0; i<row; ++i) { for(int j=0;j<col; ++j) { *(A+i*col+j) = *(B+i*col+j) +(*(C+i*col+j))*type; } } } template<class T_> void Strassen(T_ *A, T_*B, T_ *C , int n) { ///计算2的幂次阶矩阵乘法 ///input B , C ///output A if(n==2) { MatrixMulti<T_>(A,B, C, n, n); return; } if(n>2) { Strassen(A, B, C,n/2); //矩阵分块 T_* a11 = newT_[n/2*n/2]; for(int i=0;i<n/2; ++i) memcpy(a11+i*n/2, B+n*i, sizeof(T_)*n/2); //Show<T_>(a11,n/2,n/2); T_* a12 = newT_[n/2*n/2]; for(int i=0;i<n/2; ++i) memcpy(a12+i*n/2, B+n/2+n*i,sizeof(T_)*n/2); //Show<T_>(a12,n/2,n/2); T_* a21 = newT_[n/2*n/2]; for(int i=0;i<n/2; ++i) memcpy(a21+i*n/2, B+n/2*n+n*i,sizeof(T_)*n/2); //Show<T_>(a21,n/2,n/2); T_* a22 = newT_[n/2*n/2]; for(int i=0;i<n/2; ++i) memcpy(a22+i*n/2, B+n/2*(n+1)+n*i,sizeof(T_)*n/2); //Show<T_>(a22,n/2,n/2); T_* b11 = newT_[n/2*n/2]; for(int i=0;i<n/2; ++i) memcpy(b11+i*n/2, C+n*i, sizeof(T_)*n/2); //Show<T_>(b11,n/2,n/2); T_* b12 = newT_[n/2*n/2]; for(int i=0;i<n/2; ++i) memcpy(b12+i*n/2, C+n/2+n*i,sizeof(T_)*n/2); //Show<T_>(b12,n/2,n/2); T_* b21 = newT_[n/2*n/2]; for(int i=0;i<n/2; ++i) memcpy(b21+i*n/2, C+n/2*n+n*i,sizeof(T_)*n/2); //Show<T_>(b21,n/2,n/2); T_* b22 = newT_[n/2*n/2]; for(int i=0;i<n/2; ++i) memcpy(b22+i*n/2, C+n/2*(n+1)+n*i,sizeof(T_)*n/2); //Show<T_>(b22,n/2,n/2); T_*c11= newT_[n/2*n/2]; T_*c12= newT_[n/2*n/2]; T_*c21= newT_[n/2*n/2]; T_*c22= newT_[n/2*n/2]; T_*tmp1 = newT_[n/2*n/2]; T_*tmp2 = newT_[n/2*n/2]; T_*m1 = newT_[n/2*n/2]; T_*m2 = newT_[n/2*n/2]; T_*m3 = newT_[n/2*n/2]; T_*m4 = newT_[n/2*n/2]; T_*m5 = newT_[n/2*n/2]; T_*m6 = newT_[n/2*n/2]; T_*m7 = newT_[n/2*n/2]; //计算 MatrixAdd<T_>(tmp1,b12, b22, n/2, n/2, -1); //MatrixMulti<T_>(m1,a11, tmp1, n/2, n/2); Strassen<T_>(m1,a11, tmp1, n/2); MatrixAdd<T_>(tmp1,a11, a12, n/2, n/2, 1); //MatrixMulti<T_>(m2,tmp1, b22, n/2, n/2); Strassen<T_>(m2,tmp1, b22, n/2); MatrixAdd<T_>(tmp1,a21, a22, n/2, n/2, 1); //MatrixMulti<T_>(m3,tmp1, b11, n/2, n/2); Strassen<T_>(m3,tmp1, b11, n/2); MatrixAdd<T_>(tmp1,b21, b11, n/2, n/2, -1); //MatrixMulti<T_>(m4,a22, tmp1, n/2, n/2); Strassen<T_>(m4,a22, tmp1, n/2); MatrixAdd<T_>(tmp1,a11, a22, n/2, n/2, 1); MatrixAdd<T_>(tmp2,b11, b22, n/2, n/2, 1); //MatrixMulti<T_>(m5,tmp1, tmp2, n/2, n/2); Strassen<T_>(m5,tmp1, tmp2, n/2); MatrixAdd<T_>(tmp1,a12, a22, n/2, n/2, -1); MatrixAdd<T_>(tmp2,b21, b22, n/2, n/2, 1); //MatrixMulti<T_>(m6,tmp1, tmp2, n/2, n/2); Strassen<T_>(m6,tmp1, tmp2, n/2); MatrixAdd<T_>(tmp1,a11, a21, n/2, n/2, -1); MatrixAdd<T_>(tmp2,b11, b12, n/2, n/2, 1); //MatrixMulti<T_>(m7,tmp1, tmp2, n/2, n/2); Strassen<T_>(m7,tmp1, tmp2, n/2); MatrixAdd<T_>(tmp1,m5, m4, n/2, n/2, 1); MatrixAdd<T_>(tmp2,m6, m2, n/2, n/2, -1); MatrixAdd<T_>(c11,tmp1, tmp2, n/2, n/2, 1); MatrixAdd<T_>(c12,m1, m2, n/2, n/2, 1); MatrixAdd<T_>(c21,m3, m4, n/2, n/2, 1); MatrixAdd<T_>(tmp1,m5, m1, n/2, n/2, 1); MatrixAdd<T_>(tmp2,m7, m3, n/2, n/2, 1); MatrixAdd<T_>(c22,tmp1, tmp2, n/2, n/2, -1); //矩阵合并,数据拷回 for(int i=0;i<n/2; ++i) memcpy(A+n*i, c11+i*n/2, sizeof(T_)*n/2); //Show<T_>(c11,n/2,n/2); //Show<T_>(A,n,n); for(int i=0;i<n/2; ++i) memcpy(A+n/2+n*i, c12+i*n/2,sizeof(T_)*n/2); //Show<T_>(c12,n/2,n/2); //Show<T_>(A,n,n); for(int i=0;i<n/2; ++i) memcpy(A+n/2*n+n*i, c21+i*n/2,sizeof(T_)*n/2); //Show<T_>(c21,n/2,n/2); //Show<T_>(A,n,n); for(int i=0;i<n/2; ++i) memcpy(A+n/2*(n+1)+n*i, c22+i*n/2,sizeof(T_)*n/2); //Show<T_>(c22,n/2,n/2); //Show<T_>(A,n,n); //内存回收 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 []tmp1; delete []tmp2; } }
棋盘覆盖
分治分析:在2^k*2^k个方格组成的棋盘中,有一个选中的特殊方格,用L形骨牌覆盖棋盘所有,不可互相重叠,不可覆盖特殊方格。
当K>0时,将2^k*2^k棋盘分割为4个2^(k-1)*2^(k-1)子棋盘,特殊方格位于其中之一,将其余三个正常棋盘转化为特殊棋盘,用一L形骨牌覆盖其余三个,从而将原问题转化为4个较小规模的同类问题,递归使用这种分割即可。
代码示例:
void FillChessBoard(inttr, int tc, int dr, int dc, int size) { /// tr,tc 左上角行列号 /// dr, dc 特殊方格行列号 /// size = 2^k if(size == 1) return; int t = tile++;//L形骨牌号 int s = size/2;//分割棋盘 //填充左上角子棋盘 if(dr<tr+s && dc<tc+s) FillChessBoard(tr,tc,dr,dc,s);//特殊方格在此 else { Board[tr+s-1][tc+s-1]= t;//骨牌填充右下角 FillChessBoard(tr,tc,tr+s-1,tc+s-1,s);//骨牌填充左下角 } //填充右上角子棋盘 if(dr<tr+s && dc>=tc+s) FillChessBoard(tr,tc+s,dr,dc,s);//特殊方格在此 else { Board[tr+s-1][tc+s]= t;//骨牌填充左下角 FillChessBoard(tr,tc+s,tr+s-1,tc+s,s);//骨牌填充左下角 } //填充左下角 if(dr>=tr+s && dc<tc+s) FillChessBoard(tr+s,tc,dr,dc,s);//特殊方格在此 else { Board[tr+s][tc+s-1]= t;//骨牌填充右上角 FillChessBoard(tr+s,tc,tr+s,tc+s-1,s); } //填充右下角 if(dr>=tr+s && dc>=tc+s) FillChessBoard(tr+s,tc+s,dr,dc,s);//特殊方格在此 else { Board[tr+s][tc+s]= t;//骨牌填充右上角 FillChessBoard(tr+s,tc+s,tr+s,tc+s,s); } }
归并排序
将待排元素分成大小大致相同的两个子集合,分别对两个子集合进行排序,最终将两个排好序的子集合并成所要求的排好序的集合。递归只是将待排序列一分为二,直至排序集合只剩下一个元素为止,然后不断的合并,那么我们可以先将数组中相邻的两个元素两两配对。用自己合并算法将他们排序,构成n/2组长度为2的排好序的数组,再将其合并为4的排好序的数组,如此继续直至整个数组有序。template<class T_> void Merge(T_ c[],T_d[],int l, int m, int r) { //合并c[l:m] 和 c[m+1:r]到d[l:r] int i=l,j=m+1,k=l; while(i<=m && j<=r) { if(c[i] <=c[j]) d[k++] = c[i++]; else d[k++] = c[j++]; } if(i>m) { for(intq=j;q<=r;q++) d[k++] = c[q]; } else { for(int q=i;q<=m; q++) d[k++] = c[q]; } } template<class T_> void MergePass(T_ x[],T_ y[],int s ,int n) { int i=0; while(i<=n-2*s) { //合并大小为s的相邻两端子数组 Merge(x,y,i,i+s-1,i+2*s-1); i=i+2*s; } //剩下元素个数少于2s if(i+s<n) Merge(x,y,i,i+s-1,n-1); else { for(intj=i;j<=n-1;++j) { y[j] = x[j]; } } } template<class T_> void MergeSort(T_a[],int n) { T_ *b = new T_ ; int s = 1; while(s<n) { MergePass(a,b,s,n); //合并到数组b s += s; MergePass(b,a,s,n); //合并到数组a s += s; } delete[] b; }
归并排序有一种改进版--自然归并排序,思想是先扫描一遍要排序的数组,然后的出来已经有序的数组下标,依据这排好序的数组下标来进行归并排序。代码详见github@YIWANFENG
(每次合并子数组前都需要重复执行扫描数组来获得当前数组的有序数组下标,无法有效利用之前的扫描。缺点还有每次排完序需要将辅助数组拷贝回原数组,有点浪费时间。)
用一次线性扫描找到排好序的子数组,记录他们在原数组中的位置。然后与上一合并排序一样,合并相邻的两个有序子数组,构成更大的有序数组,不断重复,直至整个数组有序。
线性时间选择
找出序列A中第K小的元素。首先分析,如果该序列已经线性排列,排在第k位的元素就是要找的元素。
模仿快排,再输入数组进行递归划分,但是只需要处理划分出的子数组之一,因为只要整体大致有序,并不需要完全有序。
数组被a[ start :end ] 划分为两个子数组a[start : i ] 与 a[ i+1: end],其中a[start:i]中每个元素都不大于a[i+1:end] 的每个元素。若k <= j (j = i - start +1)则可知所求元素在a[start:i]
否则在a[i+1:end]中,并且为该数组中第k-j小的元素。
上述方法最坏情况下时间复杂度为(n^2),但随机下一般为O(n)
template<class T_> void Swap(T_ &a,T_&b) { T_ it = a; a = b; b = it; } template<class T_> int Partition(T_ a[],ints,int e) { int i=s, j=e+1; T_ x = a[s]; //将 < x 的所有元素移到左边 //将 > x 的所有元素移到右边 while(true) { while(a[++i]<x&&i<e); while(a[--j]>x); if(i>=j) break; Swap<T_>(a[i],a[j]); } a[s] = a[j]; a[j] = x; return j;//返回中间位置(严格来说这并不是中间数) } template<class T_> intRandomizedPartition(T_ a[], int s,int e) { srand(time(NULL)); int i = 0; i = rand()%(e-s)+s; Swap<T_>(a[i],a[s]); return Partition<T_>(a,s,e); } template<class T_> T_ RandomizedSelect(T_a[],int s,int e,int k) { if(s==e) return a[s]; int i = RandomizedPartition<T_>(a,s,e); //获得中间某值元素位置 int j = i-s+1; if(k<=j) { //分治,不过每次只计算一边 returnRandomizedSelect<T_>(a,s,i,k); } else { returnRandomizedSelect<T_>(a,i+1,e,k-j); } }
下面讨论一最坏情况下可以在O(n)的选择算法(此法分析不全)
努力去找一种划分,可以使新数组长度为原数组*x of (0,1)。
(1)将n个元素划分为【n/5】个组,每组5个元素或更少,将其排序取出每组中位mid[n/5]。
(2)递归调用Select找出这mid[n/5]个元素的中位数,依次元素作为划分基准。
只要等于基准元素的元素不太多,那么两个分组就不会差太远。
template<class T_> bool cmp(T_ a,T_ b) { if(a<=b) return true; return false; } template<class T_> int Location(T_ a[],ints,int e,T_ x) { int i=s, j=e+1; //将 < x 的所有元素移到左边 //将 > x 的所有元素移到右边 while(true) { while(a[++i]<x&&i<e); while(a[--j]>x); if(i>=j) break; Swap<T_>(a[i],a[j]); } a[s] = a[j]; a[j] = x; return j;//返回中间位置(严格来说这并不是中间数) } template<class U_> U_ Select_T(U_[], int s,int e, int k) { if(e-s<75) { sort(a,a+e,cmp<U_>);//某简单算法将a排序 return a[s+k-1]; } for(int i=0;i<=(e-s-4)/5;++i) { //(e-s+1)/5 = 一共有多少分组 //(e-s-4)/5 =(e-s+1)/5-1 //将a[s+5*i]至a[s+5*i+4]的第三小元素与a[s+i] //Swap<U_>(a[s+i],?); for(intj_1=s+5*i;j_1<s+5*i+5;++j_1) for(int j_2=j_1+1;j_2<s+5*i+5;++j_2) if(a[j_1]>a[j_2]) Swap<U_>(a[j_1],a[j_2]); Swap<U_>(a[s+i],a[s+5*i+2]); } //交换位置,找到中位数的中位数 U_ x = Select_T<U_>(a,s,s+(e-s-4)/5,(e-s-4)/10); int i = Location<U_>(a,s,e,x);//x在a[s]-a[e]的位置 int j=i-s+1; if(k<=j) returnSelect_T<U_>(a,s,i,k); else returnSelect_T<U_>(a,i+1,e,k-j); }
循环赛安排
N=2^k个运动员,每一个要与其他选手都比赛一次,每选手一天一次,比赛持续n-1天。依照分治,将运动员分为2部分,那么n个选手的安排可用n/2个选手的安排确定,即排列n/2,另外的n/2选手抄袭另外的n/2选手的安排。当只有两个选手时即可指定返回(即如下图对称照抄,原因是前面的人安排好了,那么换为另一批人套入后也应安排合适)。
void timetable(int m,intt,int a[] ) { //m块的大小, t块的编号,a包存数组 if(m==2) { a[t][1]=a[t+1][0]; a[t+1][1]=a[t][0]; return ; } timetable(m/2,t,a); timetable(m/2,t+m/2,a); int sub_m = m/2; for(int i=0;i<sub_m;++i) { //row for(intj=0;j<sub_m;++j) { //column //下句费了好大劲才发现有错,注意t实际表示的是行,所以列不使用t a[t+i+sub_m][j+sub_m]=a[t+i][j];//右下角 a[t+i][j+sub_m]=a[t+i+sub_m][j];//右上角 } } }
PS:若n%2==1,比赛进行n天,其他条件不变,则可多添加一虚拟人参赛,拍赛完毕后与虚拟人比赛的运动员比赛日休息即可。
相关文章推荐
- 棋盘覆盖(分治-递归)
- 棋盘覆盖--递归分治java实现
- <菜鸟学算法-A排序(分治的思想:堆排序)>
- 棋盘覆盖--递归分治java实现
- 递归分治解决棋盘覆盖问题
- 棋盘覆盖问题(分治)(C语言)
- 棋盘覆盖-分治
- <菜鸟学算法-A排序(分治的思想:归并排序)>
- 循环赛赛程安排---递归思想
- 棋盘覆盖问题【分治】
- 递归与分治之 棋盘覆盖
- 棋盘覆盖----分治和递归
- 用二维数组模拟棋盘覆盖----采用分治法
- <菜鸟学算法-A排序(分治的思想:快速排序)>
- 棋盘覆盖问题 (分治)
- 棋盘覆盖问题 - 分治法
- 分治策略之棋盘覆盖问题(ChessBoard)
- 递归与分治之棋盘覆盖
- 棋盘覆盖问题【分治】
- 棋盘覆盖和Hall's marriage theorem