分治法。 无需赘述! 就是把规模较大的问题划分成易于解决的小问题, 这个思路和模块化编程思想较为相似!。 然后把小问题的解组合成所要的最终目的解。 在一般情况下, 分治法都和递归有一腿, 所以如果想用好分治, 请先了解一下递归(当然大神可以秒杀一切, 腾空跳读, 弱渣只有膜拜!)。
分治法模式:
divide-and-conquer(P) { if(|P|<=n0) adhoc(P); divide P into smaller subinstances P1, P2, ,,, Pk; for(i=1; i<=k; i++) y1 = divide-and-conquer(Pi); return merge(y1, ,,, yk); }
分治举例:
《1》二分查找:

1 int binarySearch(int a[], int x, int n) 2 { 3 int left = 0; int right = n-1; 4 while(left<=right) 5 { 6 int middle = (left+right)/2; 7 if(x==a[middle]) return middle; 8 if(x>a[middle]) left = middle + 1; 9 else right = middle - 1; 10 } 11 return -1; 12 }
《2》棋盘覆盖。
在一个2^k×2^k个方格组成的棋盘中,若有一个方格与其他方格不同,则称该方格为一特殊方格,且称该棋盘为一个特殊棋盘.显然特殊方格在棋盘上出现的位置有4^k种情形.因而对任何k≥0,有4^k种不同的特殊棋盘. 下图–图(1)中的特殊棋盘是当k=3时16个特殊棋盘中的一个:
图(1)
题目要求在棋盘覆盖问题中,要用下图-图(2)所示的4种不同形态的L型骨牌覆盖一个给定的特殊棋盘上除特殊方格以外的所有方格,且任何2个L型骨牌不得重叠覆盖.
图(2)
解题思路:把每个大的棋盘分成4个小棋盘。为了把其中的三个无特殊方格的子棋盘转化成特殊棋盘, 可以用一个L型骨牌覆盖这3个较小棋盘的会合处。 递归的进行分割, 直至棋盘简化成 1 * 1的规模。

1 int chessBoard(int tr, int tc, int dr, int dc, int size) 2 { 3 if(size==1) return; 4 int t = tile++; //L型骨牌号 5 s = size/2; //分割棋盘 6 //覆盖左上角子棋盘。 7 if(dr<tr+s&&dc<tc+s) 8 //特殊方格在此棋盘中。 9 chessBoard(tr, tc, dr, dc, s); 10 else 11 {//此棋盘中无特殊方格 12 // 用t号 L型骨牌覆盖右下角。 13 board[tr+s-1][tc+s-1] = t; 14 //覆盖其余方格 15 chessBoard(tr, tc, tr+s-1, tc+s-1, s); 16 } 17 //覆盖右上角子棋盘。 18 if(dr<tr+s&&dc>=tc+s) 19 chessBoard(tr, tc+s, dr, dc, s) 20 else 21 { 22 board[tr+s-1][tc+s] = t; 23 chessBoard(tr, tc+s, tr+s-1, tc+s, s); 24 } 25 //覆盖左下角子棋盘。 26 if(dr>=tr+s&&dc<tc+s) 27 chessBoard(tr+s, tc, dr, dc, s); 28 else 29 { 30 board[tr+s][tc+s-1] = t; 31 chessBoard(tr+s, tc, tr+s, tc+s-1, s); 32 } 33 //覆盖右下角子棋盘。 34 if(dr>=tr+s&&dc>=tc+s) 35 chessBoard(tr+s, tc+s, dr, dc, s); 36 else 37 { 38 board[tr+s][tc+s] = t; 39 chessBoard(tr+s, tc+s, tr+s, tc+s); 40 } 41 }
《3》 归并排序。

1 #include<iostream> 2 using namespace std; 3 4 const int maxn = 10005; 5 int A[maxn], T[maxn]; 6 7 void merge_sort(int *A, int x, int y, int* T) //x为要排序数组的下界下标, y为,, 8 { 9 if(y-x>1) 10 { 11 int m = x + (y-x)/2;//划分, 其实这里可以优化, 以中位数划分更快。 12 int p = x, q = m, i = x; 13 merge_sort(A, x, m, T);//递归 14 merge_sort(A, m, y, T); 15 while(p<m||q<y) //合并 16 { 17 if(q>=y||(p<m&&A[p]<=A[q])) 18 T[i++] = A[p++]; //从左半部分复制 19 else T[i++] = A[q++]; //从右半部分复制 20 } 21 for(i=x; i<y; i++) A[i] = T[i]; //从辅助空间复制回A数组。 22 } 23 } 24 int main() 25 { 26 int n; 27 scanf("%d", &n); 28 for(int i = 0; i<n; i++) 29 { 30 scanf("%d", &A[i]); 31 } 32 merge_sort(A, 0, n, T); 33 /*------*/ 34 return 0; 35 }
《4》快速排序。

1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 using namespace std; 5 6 int a[10] = {12, 5, 6, 23, 8658, 21, 1, 1, 25}; 7 8 void quick_sort(int b, int e, int a[]) 9 { 10 int i = b, j = e, x = a[(b+e)/2]; 11 do 12 { 13 while(a[i]<x) i++; 14 while(a[j]>x) j--; 15 if(i<=j) swap(a[i++], a[j--]); 16 }while(i<j) ; 17 if(j < e) quick_sort(i, e, a); 18 if(j > b) qiuck_sort(b, j, a); 19 } 20 21 int main() 22 { 23 quick_sort(0, 9, a); 24 for(int i = 0; i<10; i++) 25 printf("%d ", a[i]); 26 return 0; 27 }
《5》 线性时间选择。
输入有多组测试例。
对每一个测试例有2行,第一行是整数n和k(1≤k<n≤1000),第二行是n个整数。
第k小的元素。
输入样例 |
输出样例 |
5 2 3 9 4 1 6 7 3 4 59 7 23 61 55 46 |
3 23 |
分析: 对于这个问题, 最简单的方法就是,先排序, 然后输出第K个值, 然而, 这样的算法有点慢。 这里还有更快的算法。 这个算法和快速排序算法的思想差不多。 即现以第一个数为标准, 把数划分成左边的都小于该数, 右边的都大于该数。如果左边的数的个数大于 K 则再把把左边的作为整体划分即可, 否则 在右边, 缩小k变为k-左边的个数, 然后划分右边。 详见代码:

1 #include<iostream> 2 #include<cstdio> 3 using namespace std; 4 5 const int maxn = 1000 + 5; 6 int a[maxn]; 7 8 int select(int left, int right, int k) 9 { 10 //找到了第 k 小的元素。 11 if(left>=right) return a[left]; 12 int i = left; 13 int j = right + 1; 14 int pivot = a[left]; 15 while(true) //快排 16 { 17 do{ 18 i = i + 1; 19 }while(a[i]<pivot); 20 do 21 { 22 j=j-1; 23 }while(a[j]>pivot); 24 if(i>=j) break; 25 swap(a[i], a[j]); 26 } 27 if(j-left+1==k) return pivot; 28 a[left] = a[j]; //存储pivot 29 a[j] = pivot; 30 if(j-left+1<k) 31 return select(j+1, right, k-j+left-1); 32 else return select(left, j-1, k); 33 } 34 int main() 35 { 36 int n, k; 37 while(scanf("%d%d", &n, &k)!=EOF) 38 { 39 for(int i=0; i<n; i++) 40 scanf("%d", &a[i]); 41 printf("%d ", select(0, n, k+1));//注意数组从 0 开始。 42 } 43 return 0; 44 }
在最差的形况下, 这个算法的时间复杂度为n^2 . 当然, 这个算法还可以进一步的进行优化。 聪明的你也许已经想到了优化的方法,即和对快排的优化一样, 用产生随机数的方法啦进行划分, 而不是以每次的第一个数作为划分标准。
《6》最接近点对问题。
《7》 循环赛日程表。
设有n=2^k个运动员要进行网球循环赛。现要设计一个满足以下要求的比赛日程表: (1)每个选手必须与其他n-1个选手各赛一次; (2)每个选手一天只能参赛一次; (3)循环赛在n-1天内结束。
请按此要求将比赛日程表设计成有n行和n-1列的一个表。在表中的第i行,第j列处填入第i个选手在第j天所遇到的选手。其中1≤i≤n,1≤j≤n-1。8个选手的比赛日程表如下图:
算法思路:按分治策略,我们可以将所有的选手分为两半,则n个选手的比赛日程表可以通过n/2个选手的比赛日程表来决定。递归地用这种一分为二的策略对选手进行划分,直到只剩下两个选手时,比赛日程表的制定就变得很简单。这时只要让这两个选手进行比赛就可以了。如上图,所列出的正方形表是8个选手的比赛日程表。其中左上角与左下角的两小块分别为选手1至选手4和选手5至选手8前3天的比赛日程。据此,将左上角小块中的所有数字按其相对位置抄到右下角,又将左下角小块中的所有数字按其相对位置抄到右上角,这样我们就分别安排好了选手1至选手4和选手5至选手8在后4天的比赛日程。依此思想容易将这个比赛日程表推广到具有任意多个选手的情形。
算法步骤:
(1)用一个for循环输出日程表的第一行 for(int i=1;i<=N;i++) a[1][i] = i
(2)然后定义一个m值,m初始化为1,m用来控制每一次填充表格时i(i表示行)和j(j表示列)的起始填充位置。
(3)用一个for循环将问题分成几部分,对于k=3,n=8,将问题分成3大部分,第一部分为,根据已经填充的第一行,填写第二行,第二部分为,根据已经填充好的第一部分,填写第三四行,第三部分为,根据已经填充好的前四行,填写最后四行。for (ints=1;s<=k;s++) N/=2;
(4)用一个for循环对③中提到的每一部分进行划分for(intt=1;t<=N;t++)对于第一部分,将其划分为四个小的单元,即对第二行进行如下划分
同理,对第二部分(即三四行),划分为两部分,第三部分同理。
(5)最后,根据以上for循环对整体的划分和分治法的思想,进行每一个单元格的填充。填充原则是:对角线填充
for(int i=m+1;i<=2*m;i++) //i控制行
for(int j=m+1;j<=2*m;j++) //j控制列
{
a[i][j+(t-1)*m*2]= a[i-m][j+(t-1)*m*2-m];/*右下角的值等于左上角的值 */
a[i][j+(t-1)*m*2-m] =a[i-m][j+(t-1)*m*2];/*左下角的值等于右上角的值 */
}
运行过程:
(1)由初始化的第一行填充第二行
(2)由s控制的第一部分填完。然后是s++,进行第二部分的填充
(3)最后是第三部分的填充

1 #include<iostream> 2 using namespace std; 3 4 const int maxn = 10005; 5 int a[maxn][maxn]; 6 7 void table(int k, int a[][maxn]) 8 { 9 int n = 1; 10 n<<=k; 11 for(int i=1; i<=n; i++) 12 a[1][i] = i; 13 int m = 1; //用来每次填充时, 控制填充起始的位置 14 for(int s=1; s<=k; s++)//划分(分治)成 k块, 从第一块向下滚。 15 { 16 n/=2; //每次大长块中, 横分成 n 小块。 17 for(int t=1; t<=n; t++) //一小块一小块的填充 18 for(int i=m+1; i<=2*m; i++) //控制行数 19 for(int j=m+1; j<=2*m; j++)//控制列数 20 { 21 a[i][j+(t-1)*m*2]=a[i-m][j+(t-1)*m*2-m];//右下角的值等于左上角的值 22 a[i][j+(t-1)*m*2-m] = a[i-m][j+(t-1)*m*2];//左下角的值等于右上角的值 23 } 24 m*=2; //向下滚, 起始位置翻倍。 25 } 26 } 27 28 int main() 29 { 30 int k; 31 while(scanf("%d", &k)!=EOF) 32 { 33 int n=1<<k; 34 table(k, a); 35 for(int i=1; i<=n; i++) 36 { 37 for(int j=1; j<=n; j++) 38 printf("%5d", a[i][j]); 39 printf(" "); 40 } 41 } 42 return 0; 43 }
《8》输油管道问题。
第1行是一个整数n,表示油井的数量(1≤n≤10 000)。
接下来n行是油井的位置,每行两个整数x和y
(﹣10 000≤x,y≤10 000)。
各油井到主管道之间的输油管道最小长度总和。
输入样例
5
1 2
2 2
1 3
3 -2
3 3
输出样例
6
分析:很明显当 y 为中位数时最短。 求中位数, 当然可以用排序, 然而更快的方法 goto《5》哈哈!

1 int select(int left, int right, int k) 2 { 3 //ÕÒµ½ÁËµÚ k СµÄÔªËØ¡£ 4 if(left>=right) return a[left]; 5 int i = left; 6 int j = right + 1; 7 int pivot = a[left]; 8 while(true) //¿ìÅÅ 9 { 10 do{ 11 i = i + 1; 12 }while(a[i]<pivot); 13 do 14 { 15 j=j-1; 16 }while(a[j]>pivot); 17 if(i>=j) break; 18 swap(a[i], a[j]); 19 } 20 if(j-left+1==k) return pivot; 21 a[left] = a[j]; //´æ´¢pivot 22 a[j] = pivot; 23 if(j-left+1<k) 24 return select(j+1, right, k-j+left-1); 25 else return select(left, j-1, k); 26 }
他山之石可以攻玉! 本来对这几个循环理解不清, 结果无意中窥探到某大神博客中的上述图解, 恍然大悟。虽然智商深深受虐! 然, 仍感收益颇丰! 上述分治法参考书籍:
《算法设计与分析》 愿与“童稚”之人共享!