分治法作为一种常见的算法思想,其概念为:把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题,直到最后子问题可以简单的直接求解,子问题的解的合并即是原问题的解。举个例子,要算16个数的和可能一下子算不出来的,但是可以通过几次一分为二(拆分),直到分成两个数、两个数一组;再对这些数两两相加,算出每组的和后,再两两相加,直到最后只剩下了一个数,就算出16个数的和(合治)。
可以用分治法解决的问题一般有如下特征:
1>问题的规模缩小到一定的程度就可以容易地解决。此特征是大多数问题所具备的,当问题规模增大时,解决问题的复杂度不可避免地会增加。
2>问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。此特征也较为常见,是应用分治法的前提。
3>拆分出来的子问题的解,可以合并为该问题的解。这个特征在是否采用分治法的问题上往往具有决定性作用,比如棋盘覆盖、汉诺塔等,需要将子问题的解汇总,才是最终问题的解。
4>拆分出来的各个子问题是相互独立的,即子问题之间不包含公共的子问题。该特征涉及到分治法的效率,如果各子问题是不独立的,则需要重复地解公共的子问题,此时用动态规划法更好。
使用分治法的基本步骤:
1>分解,将原问题分解为若干个规模较小、相互独立、与原问题形式相同的子问题。
2>解决,若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题。
3>合并,将各个子问题的解合并为原问题的解。
以上是官方说法,依个人理解,常见的递归方式的分治步骤是:
divideAndConquer(初始递归变量){
if(持续递归条件){
本次拆分后的业务逻辑
if(某种分治策略){
divideAndConquer(一侧递归初始变量);
}else{
divideAndConquer(另一侧递归初始变量);
}
}
}
常见的循环方式的分治步骤是:
while(满足拆分的条件){
本次拆分后的业务逻辑
if(某种分治策略){
按分治策略,改变并保存拆分条件所涉及的变量
}else{
按分治策略,改变并保存拆分条件所涉及的变量
}
}
从上面两种方式可以看出,两种方式都是在改变变量。不过递归式分治是用新变量直接调用自身方法,而循环式分治是利用改变后的变量、通过循环进入下一阶段的拆分。
常见例子如下:
1>二分搜索
2>大整数乘法
3>Strassen矩阵乘法
4>棋盘覆盖
5>合并排序
6>快速排序
7>线性时间选择
8>最接近点对问题
9>循环赛日程表
10>汉诺塔
接下来将对这些例子进行实现,来探究分治思想的具体使用方法。
该问题的常见形式是在一个有序数组中寻找某个元素。在本例子中假设数组array[ ]存储升序序列,变量left表示查找范围的左边界,right表示查找范围的有边界,mid表示查找范围的中间位置,target为要查找的元素。用分治法实现过程如下:
1>初始化。令left=0,即指向array[ ]的第一个元素;right=array.length-1,即指向有序列表array[ ]的最后一个元素。
2>mid=(left+right)/ 2,即指向查找范围的中间元素。(low+high)为奇数的话,除以2并不能得到整数,此处向下取整,比如:4.5向下取整为4。
3>判定 left<=right是否成立,如果成立,转第4步,否则,说明该列表中没有指定元素,算法结束。
4>判断target 与 array[mid] 的关系。如果 target==array[mid] ,搜索成功,算法结束;如果 target >array[mid] ,令 left=middle+1,继续在数组的后半部分进行搜索;如果 target
static int binarySearch1(int arr[],int len,int target){
/*初始化左右搜索边界*/
int left=0,right=len-1;
int mid;
while(left<=right){
/*中间位置:两边界元素之和/2向下取整*/
mid=(left+right)/2;
/*arr[mid]大于target,即要寻找的元素在左半边,所以需要设定右边界为mid-1,搜索左半边*/
if(target<arr[mid]){
right=mid-1;
/*arr[mid]小于target,即要寻找的元素在右半边,所以需要设定左边界为mid+1,搜索右半边*/
}else if(target>arr[mid]){
left=mid+1;
/*搜索到对应元素*/
}else if(target==arr[mid]){
return mid;
}
}
/*搜索不到返回-1*/
return -1;
}
二分搜索递归实现代码如下:
static int binarySearch2(int array[],int left,int right,int target){
if(left<=right){
int mid=(left+right)/2;
/*搜索到对应元素*/
if(array[mid]==target){
return mid;
}else if(array[mid]<target){
/*array[mid]小于target,即要寻找的元素在右半边,所以需要设定左边界为mid+1,搜索右半边*/
return binarySearch2(array,mid+1,right,target);
}else{
/*array[mid]大于target,即要寻找的元素在左半边,所以需要设定右边界为mid-1,搜索左半边*/
return binarySearch2(array,left,mid-1,target);
}
}else{
return -1;
}
}
完整测试代码:
package divideAndConquer;
public class BinarySearch {
public static void main(String[] args) {
int[ ] array = new int[ ]{2,13,25,69,88};
int location = binarySearch1(array,array.length-1,13);
System.out.println("非递归实现,查找到的元素位置为:"+location);
location = binarySearch2(array,0,array.length-1,13);
System.out.println("递归实现,查找到的元素位置为:"+location);
}
/*非递归方式实现二分搜索*/
static int binarySearch1(int arr[],int len,int target){
/*初始化左右搜索边界*/
int left=0,right=len-1;
int mid;
while(left<=right){
/*中间位置:两边界元素之和/2向下取整*/
mid=(left+right)/2;
/*搜索左半边*/
if(target<arr[mid]){
right=mid-1;
/*搜索右半边*/
}else if(target>arr[mid]){
left=mid+1;
/*搜索到对应元素*/
}else if(target==arr[mid]){
return mid;
}
}
return -1;
}
/*递归方式实现二分搜索*/
static int binarySearch2(int array[],int left,int right,int target){
if(left<=right){
int mid=(left+right)/2;
/*搜索到对应元素*/
if(array[mid]==target){
return mid;
}else if(array[mid]<target){
/*搜索右边*/
return binarySearch2(array,mid+1,right,target);
}else{
/*搜索左边*/
return binarySearch2(array,left,mid-1,target);
}
}else{
return -1;
}
}
}
测试结果:
非递归实现,查找到的元素位置为:1
递归实现,查找到的元素位置为:1
大整数乘法也是常见的分治法使用的例子之一,在进行两个大整数相乘时,普通的思路是两个数中的每个位数上的数字逐个相乘再相加,这样使用乘法的次数较大,就会造成时间复杂度较大。常见的优化方式,是使用分治法来实现,如下:
由上图可以看出用分治法来实现大整数相乘的思路:
1>首先将要相乘的两个数X和Y分成A,B,C,D四部分。
2>此时将X和Y的乘积转化为图中的式子,把问题转化为求解式子的值。
3>递归处理,直至算出最后的结果。
依据上面思路,示例代码如下:
package divideAndConquer;
public class BigIntPow {
public static void main(String[] args) {
int num1 = 7852;
int num2 = 123;
int num1length = 4;
int num2length = 3;
long result = bigIntPow(num1,num2,num1length,num2length);
System.out.println("使用分治法,大整数相乘结果为:"+result);
}
/* num1:第一个乘数
* num2:第二个乘数
* num1length:第一个乘数的个数
* num2length:第二个乘数的个数*/
static long bigIntPow(long num1, long num2, int num1length, int num2length){
if (num1 == 0 || num2 == 0){
return 0;
}else if (num1length == 1 || num2length == 1){
return num1*num2;
}else{
int xn0 = num1length / 2, yn0 = num2length / 2;
int xn1 = num1length - xn0, yn1 = num2length - yn0;
long A = (long)(num1 / Math.pow(10, xn0));
long B = (long)(num1 % Math.pow(10, xn0));
long C = (long)(num2 / Math.pow(10, yn0));
long D = (long)(num2 % Math.pow(10, yn0));
long AC = bigIntPow(A, C, xn1, yn1);
long BD = bigIntPow(B, D, xn0, yn0);
long ABCD = bigIntPow((long)(A * Math.pow(10, xn0) - B), (long)(D - C * Math.pow(10, yn0)), xn1, yn1);
return (long)(2 * AC * Math.pow(10, (xn0 + yn0)) + ABCD + 2 * BD);
}
}
}
测试结果:
使用分治法,大整数相乘结果为:965796
矩阵乘法的Strassen算法,也是一种常见的分治思想的运用。当两个具有相同维数的矩阵相乘,需要三层for循环,其复杂度为O(n3),运算过程如下:
使用Strassen算法时,运算过程如下:
Strassen算法的时间复杂度是:
示例代码如下:
/*Strassen矩阵乘法*/
public class Strassen {
public static void main(String[] args){
int[] matrixA = new int[]{
1, 2, 3, 4,
8, 7, 6, 5,
9, 10,11,12,
13,14,15,16
};
int[] matrixB = new int[]{
17, 18, 19, 20,
21, 22, 23, 24,
25, 26, 27, 28,
29, 30, 31, 32
};
int length = 4;
int[] c = sMM(matrixA, matrixB, length);
System.out.print("计算结果为:\n");
for(int i = 0; i < c.length; i++){
System.out.print(c[i] + " ");
if((i + 1) % length == 0) //换行
System.out.println();
}
}
static int[] sMM(int[] a, int[] b, int length){
if(length == 2){
return getResult(a, b);
}else{
int tlength = length / 2;
//把a数组分为四部分,进行分治递归
int[] aa = new int[tlength * tlength];
int[] ab = new int[tlength * tlength];
int[] ac = new int[tlength * tlength];
int[] ad = new int[tlength * tlength];
//把b数组分为四部分,进行分治递归
int[] ba = new int[tlength * tlength];
int[] bb = new int[tlength * tlength];
int[] bc = new int[tlength * tlength];
int[] bd = new int[tlength * tlength];
//划分子矩阵
for(int i = 0; i < length; i++){
for(int j = 0; j < length; j++){
/*
* 划分矩阵:
* 例子:将 4 * 4 的矩阵,变为 2 * 2 的矩阵,
* 那么原矩阵左上、右上、左下、右下的四个元素分别归为新矩阵
*/
if(i < tlength){
if(j < tlength){
aa[i * tlength + j] = a[i * length + j];
ba[i * tlength + j] = b[i * length + j];
}else{
ab[i * tlength + (j - tlength)]
= a[i * length + j];
bb[i * tlength + (j - tlength)]
= b[i * length + j];
}
}else{
if(j < tlength){
//i 大于 tlength 时,需要减去 tlength,j同理
//因为 b,c,d三个子矩阵有对应了父矩阵的后半部分
ac[(i - tlength) * tlength + j]
= a[i * length + j];
bc[(i - tlength) * tlength + j]
= b[i * length + j];
}else{
ad[(i - tlength) * tlength + (j - tlength)]
= a[i * length + j];
bd[(i - tlength) * tlength + (j - tlength)]
= b[i * length + j];
}
}
}
}
//分治递归
int[] result = new int[length * length];
//temp:4个临时矩阵
int[] t1 = add(sMM(aa, ba, tlength), sMM(ab, bc, tlength));
int[] t2 = add(sMM(aa, bb, tlength), sMM(ab, bd, tlength));
int[] t3 = add(sMM(ac, ba, tlength), sMM(ad, bc, tlength));
int[] t4 = add(sMM(ac, bb, tlength), sMM(ad, bd, tlength));
//归并结果
for(int i = 0; i < length; i++){
for(int j = 0; j < length; j++){
if(i < tlength){
if(j < tlength)
result[i * length + j] = t1[i * tlength + j];
else
result[i * length + j] = t2[i * tlength + (j - tlength)];
}else{
if(j < tlength)
result[i * length + j]
= t3[(i - tlength) * tlength + j];
else
result[i * length + j]
= t4[(i - tlength) * tlength + (j - tlength)];
}
}
}
return result;
}
}
/*二维矩阵相乘,按Strassen算法计算出结果*/
public static int[] getResult(int[] a, int[] b){
int p1 = a[0] * (b[1] - b[3]);
int p2 = (a[0] + a[1]) * b[3];
int p3 = (a[2] + a[3]) * b[0];
int p4 = a[3] * (b[2] - b[0]);
int p5 = (a[0] + a[3]) * (b[0] + b[3]);
int p6 = (a[1] - a[3]) * (b[2] + b[3]);
int p7 = (a[0] - a[2]) * (b[0] + b[1]);
int c00 = p5 + p4 - p2 + p6;
int c01 = p1 + p2;
int c10 = p3 + p4;
int c11 = p5 + p1 -p3 - p7;
return new int[] {c00, c01, c10, c11};
}
/*矩阵相加*/
public static int[] add(int[] a, int[] b){
int[] c = new int[a.length];
for(int i = 0; i < a.length; i++)
c[i] = a[i] + b[i];
return c;
}
}
测试结果:
计算结果为:
250 260 270 280
578 604 630 656
986 1028 1070 1112
1354 1412 1470 1528
该问题是先给出一个n*n的棋盘,里面有个位置放特殊的棋子,如下:
然后用一些特殊形状的格子,覆盖特殊棋子之外的其他位置,特殊格子形状如下:
在解决该问题时,需要将原始的棋盘划分成四个元素个数相同的棋盘,再根据特殊元素是否在划分后的区域进行不同的处理;然后递归地执行这个过程,直到最后划分的区域为包含四个元素单位。不难看出,这里面也体现了分–治的思想,示例代码如下:
package divideAndConquer;
public class ChessBoard {
/*L型骨牌的编号,也就是L型骨牌的批次,不同的批次代表不同阶段的处理*/
static int tile=1;
/*表示棋盘*/
static int[][] board = new int[4][4];
public static void main(String[] args){
chessBoard(0,0,2,1,4);
System.out.println("棋盘覆盖后的结果:");
for(int i = 0; i <4; i++){
for(int j = 0; j <4; j++){
System.out.print(board[i][j]+" ");
}
System.out.println();
}
}
/*处理带有特殊棋子的棋盘.tr、tc表示棋盘的入口即左上角的行列号,dr、dc表示特殊棋子的行列位置,size表示棋盘的行数或者列数*/
static void chessBoard(int tr, int tc, int dr, int dc, int size){
if(size == 1)
return;
int tempTitle = tile++;
/*每一次化大棋盘为一半的子棋盘*/
int tempSize = size/2;
/*一:处理左上棋盘*/
//左上角子棋盘有特殊棋子
if(dr < tr+tempSize && dc< tc + tempSize){
//处理有特殊棋子的左上角子棋盘
chessBoard(tr,tc,dr,dc,tempSize);
}else{
//设左上角子棋盘的右下角为特殊棋子,用t型的骨牌覆盖
board[tr+tempSize-1][tc+tempSize-1] = tempTitle;
chessBoard(tr,tc,tr+tempSize-1,tc+tempSize-1, tempSize);
}
/*二:处理右上角棋盘*/
//右上角子棋盘有特殊棋子
if(dr < tr+tempSize && dc >=tc+tempSize){
//处理有特殊棋子的右上角子棋盘
chessBoard(tr,tc+tempSize,dr,dc,tempSize);
}else{
//设右上角子棋盘的左下角为特殊棋子,用t型的骨牌覆盖
board[tr+tempSize-1][tc+tempSize] = tempTitle;
chessBoard(tr,tc+tempSize,tr+tempSize-1,tc+tempSize,tempSize);
}
/*三:处理左下角子棋盘*/
//左下角子棋盘有特殊棋子
if(dr >=tr+tempSize && dc<tc+tempSize){
//处理有特殊棋子的左下角子棋盘
chessBoard(tr+tempSize,tc,dr,dc,tempSize);
}else{
//设左下角子棋盘的右上角为特殊棋子,用t型的骨牌覆盖
board[tr+tempSize][tc+tempSize-1] = tempTitle;
chessBoard(tr+tempSize,tc,tr+tempSize,tc+tempSize-1,tempSize);
}
/*四:处理右下角棋盘*/
//右下角子棋盘有特殊棋子
if(dr>=tr+tempSize&& dc>= tc+tempSize){
//处理有特殊棋子的右下角子棋盘
chessBoard(tr+tempSize,tc+tempSize,dr,dc,tempSize);
}else{
//设子棋盘右下角的左上角为特殊棋子,用t型的骨牌覆盖
board[tr+tempSize][tc+tempSize] = tempTitle;
chessBoard(tr+tempSize,tc+tempSize,tr+tempSize,tc+tempSize,tempSize);
}
}
}
测试结果:
棋盘覆盖后的结果:
2 2 3 3
2 1 1 3
4 0 1 5
4 4 5 5
即归并排序,是典型的一种分治思想的运用,其过程如下:
1>分:将待排序元素划分成大小大致相同的两个子序列,直到每个子序列只有1个元素。
2>治:对两个子序列进行合并排序。
3>合:将排好序的有序子序列进行合并,得到最终的有序序列。
这个过程,借用网上一张的图片来说明:
用代码实现如下:
package Sort;
public class MergeSort {
public static void main(String[] args) {
int[ ] array = new int[ ]{4,2,3,5,7,9,8};
/*拷贝一个和a所有元素相同的辅助数组*/
int[] arrTemp = array.clone();
sort(array,arrTemp,0,array.length-1);
System.out.println("排序后的结果:");
for(int i = 0;i<array.length;i++)
System.out.print(array[i]+" ");
}
/*基于递归的归并排序算法*/
static void sort (int a[], int temp[], int start,int end) {
if(end > start){
int mid = start+(end-start)/2;
/*对左右子序列递归*/
sort(temp, a,start,mid);
sort(temp, a,mid+1,end);
/*合并左右子数组*/
merge(a, temp, start,mid,end);
}
}
/*arr[low...high] 是待排序序列,其中arr[low...mid]和 a[mid+1...high]已有序*/
static void merge (int arr[],int temp[],int start,int mid,int end) {
/*左边子序列的头元素*/
int i = start;
/*右边子序列的头元素*/
int j = mid+1;
for(int k = start;k <= end;k++){
if(i>mid){
/*左边子序列元素用尽*/
arr[k] = temp[j++];
}else if(j>end){
/*右边子序列元素用尽*/
arr[k] = temp[i++];
}else if(temp[j]<temp[i]){
/*右边子序列当前元素小于左边子序列当前元素, 取右半边元素*/
arr[k] = temp[j++];
}else {
/*右边子序列当前元素大于等于左边子序列当前元素,取左半边元素*/
arr[k] = temp[i++];
}
}
}
}
测试结果:
排序后的结果:
2 3 4 5 7 8 9
归并排序是将待排列元素打散再重新聚合,快速排序是将待排列元素持续分堆,两者排序的过程中都是体现着不断拆分为子问题的痕迹,所以其实快速排序也是用分治思想来实现的。
快速排序的步骤如下:
1>先从数列中取出一个数作为基准数。
2>分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
3>再对左右区间重复第二步,直到各区间只有一个数。
快速排序实现代码如下:
package Sort;
public class QuickSort {
public static void main(String[] args) {
int[ ] array = new int[ ]{5,2,3,9,7};
quickSort(array, 0, array.length-1);
System.out.println("快速排序后的数组:");
for(int i = 0;i<array.length;i++)
System.out.print(array[i]+" ");
}
static void quickSort(int[] arr, int startIndex, int endIndex) {
/*当startIndex大于等于endIndex时,不再递归*/
if (startIndex >= endIndex) {
return;
}
/*得到基准元素位置*/
int keyIndex = divide(arr, startIndex, endIndex);
/*递归处理基准的左右两部分*/
quickSort(arr, startIndex, keyIndex - 1);
quickSort(arr, keyIndex + 1, endIndex);
}
static int divide(int[] arr, int startIndex, int endIndex) {
/*取第一个位置的元素作为基准元素*/
int key = arr[startIndex];
int left = startIndex;
int right = endIndex;
/*坑的位置,初始等于key的位置*/
int index = startIndex;
/*当right大于等于left时,执行循环*/
while (right >= left){
/*right指针从右向左进行比较*/
while (right >= left) {
if (arr[right] < key) {
/*最右边的元素覆盖原来坑中的值*/
arr[left] = arr[right];
/*坑的位置改变,变成最右边元素对应的索引*/
index = right;
/*left索引右移,因为原来的值已被right索引的值所覆盖*/
left++;
break;
}
/*最右边的元素参与比较,无论是否参与与坑中元素的交换,都左移,不再参与下一轮的比较*/
right--;
}
/*left指针从左向右进行比较*/
while (right >= left) {
if (arr[left] > key) {
arr[right] = arr[left];
index = left;
right--;
break;
}
left++;
}
}
/*将基准元素放在index位置,也就是大小元素放在其前后的中间位置*/
arr[index] = key;
return index;
}
}
测试结果:
快速排序后的数组:
2 3 5 7 9
此算法常使用的场景是:找出待排序序列中, 第k大或第k小元素(1
此算法使用步骤如下:
1>分组并取各组中位数 (将元素每5个分成一组,分别排序,并将该组中位数与该组的首元素交换,目的是使所有组的中位数都排列在数组最左侧,以便进一步查找中位数的中位数 。
2>查找中位数的中位数。
3>进行划分过程,即每轮排序后中位数对应的下标,在整个序列中的相对位置。
4>判断第k个数在划分结果的左边、右边还是恰好是划分结果本身,前两者递归处理,后者直接返回答案。
示例代码如下:
package divideAndConquer;
/*线性时间选择,求第k小的数字*/
public class KLower {
public static void main(String[] args) {
int array[]= {3,0,7,6,5,9,8,2,1,4,13,11,17,16,15,19,18,12,10,14,23,21,
27,26,25,29,28,22,20,24,33,31,37,36,35,39,38,32,30,34,43,
41,47,46,45,49,48,42,40,44,53,51,57,56,55,59,58,52,50,54,
63,61,67,66,65,69,68,62,60,64,73,71,77,76,75,79,78,72,70,74};
for(int i = 0; i < array.length; i++){
System.out.println("第"+(i+1)+"小数为: "+selectK(array, 0, array.length-1, i+1));
}
}
/*该例子中考虑的是元素大于75的情况,要把元素按5个一组进行分组*/
public static int selectK(int a[], int l, int r, int k){
int p = FindMid(a, l, r); //寻找中位数的中位数
int i = Partion(a, l, r, p);
int m = i - l + 1;
if(m == k){
return a[i];
}
if(m > k){
return selectK(a, l, i - 1, k);
}else{
return selectK(a, i + 1, r, k - m);
}
}
//递归寻找中位数的中位数
public static int FindMid(int arr[], int left, int r){
if(left == r) return left;
int i = 0;
int n = 0;
for(i = left; i < r - 5; i += 5){
bubbleSort(arr, i, i + 4);
n = i - left;
swap(arr,left+n/5, i+2);
}
//处理剩余元素
int num = r - i + 1;
if(num > 0){
bubbleSort(arr, i, i + num - 1);
n = i - left;
swap(arr,left+n/5, i+num/2);
}
n /= 5;
if(n == left) return left;
return FindMid(arr, left, left + n);
}
//进行划分过程
public static int Partion(int a[], int l, int r, int p){
swap(a,p, l);
int i = l;
int j = r;
int pivot = a[l];
while(i < j){
while(a[j] >= pivot && i < j)
j--;
a[i] = a[j];
while(a[i] <= pivot && i < j)
i++;
a[j] = a[i];
}
a[i] = pivot;
return i;
}
public static void swap(int a[], int i,int j){
int temp=a[j];
a[j] = a[i];
a[i] = temp;
}
/*冒泡排序*/
public static void bubbleSort(int a[], int l, int r){
for(int i=l; i<r; i++){
for(int j=i+1; j<=r; j++){
if(a[j]<a[i])swap(a,i,j);
}
}
}
}
测试结果:
第1小数为: 0
第2小数为: 1
第3小数为: 2
第4小数为: 3
第5小数为: 4
第6小数为: 5
第7小数为: 6
第8小数为: 7
第9小数为: 8
第10小数为: 9
第11小数为: 10
第12小数为: 11
第13小数为: 12
第14小数为: 13
第15小数为: 14
第16小数为: 15
第17小数为: 16
第18小数为: 17
第19小数为: 18
第20小数为: 19
第21小数为: 20
第22小数为: 21
第23小数为: 22
第24小数为: 23
第25小数为: 24
第26小数为: 25
第27小数为: 26
第28小数为: 27
第29小数为: 28
第30小数为: 29
第31小数为: 30
第32小数为: 31
第33小数为: 32
第34小数为: 33
第35小数为: 34
第36小数为: 35
第37小数为: 36
第38小数为: 37
第39小数为: 38
第40小数为: 39
第41小数为: 40
第42小数为: 41
第43小数为: 42
第44小数为: 43
第45小数为: 44
第46小数为: 45
第47小数为: 46
第48小数为: 47
第49小数为: 48
第50小数为: 49
第51小数为: 50
第52小数为: 51
第53小数为: 52
第54小数为: 53
第55小数为: 54
第56小数为: 55
第57小数为: 56
第58小数为: 57
第59小数为: 58
第60小数为: 59
第61小数为: 60
第62小数为: 61
第63小数为: 62
第64小数为: 63
第65小数为: 64
第66小数为: 65
第67小数为: 66
第68小数为: 67
第69小数为: 68
第70小数为: 69
第71小数为: 70
第72小数为: 71
第73小数为: 72
第74小数为: 73
第75小数为: 74
第76小数为: 75
第77小数为: 76
第78小数为: 77
第79小数为: 78
第80小数为: 79
该问题指的是:在一个平面上,有着许多的散点,如果找出欧式距离最近的两个点。从问题表面可以看出,该问题可以用暴力法求解,即求出所有点之间的距离,再统一进行比较,选出距离最近的点对。但这样做,难免时间复杂度较高,所以就需要用别的解法来计算,常用的是分治法:即按X坐标或Y坐标将元素区分成两部分,然后不断划分,直到每个子区间中只有一个元素,求出左右区间中最小点对之间的距离,这样就求出了两个区间最小值。接下来要求出最复杂的第三个最小值:两个点在不同的子区间,如何在不同区间寻找这两个点就成了最近点对问题的难点。
此问题的解决步骤,参考了网上一篇比较容易理解的文章:算法设计与分析——分治法:详解二维最近点对问题,具体步骤如下:
1>选择所有点的某一坐标(X坐标或Y坐标,此处以X轴为例),求出平均值,划分中轴线,将数据按某一坐标轴分为左右两个部分。
2>求出左半边和右半边的最小距离,选取较小值d,此步骤可以用递归实现。
3>根据上一步求出的d,求出最小点对在不同区间时的最小距离。
4>比较这三个最小距离。
上述步骤中,较难理解的部分是第3步。首先在左右区间各选择一个点时,肯定是在某个区域中选择时,这个区域的宽度如何定义。正确的做法应该是
d1和d2分别为左右区间中最小点对之间的距离,d为d1和d2的较小值。
接下来就要确定区域的高度范围了,这个范围不是固定的,需要根据每次选择的点来动态设置。假设我们在左半边选择了一个点,那这个点对应的Y轴坐标的2d之间的高度就是高度范围,因为2d之外的点与当前选择点之间的距离也肯定>d,如下:
接下来就是算法实现了,示例代码如下:
package divideAndConquer;
import java.util.Random;
public class ChooseClosestPoint {
public static void main(String[] args) {
double[][] points = {{1,1},{5,4},{2,8},{2,4},{3,2},{5,2},{1,4},{6,5}};
double result = getByDivideConquer(points);
System.out.println("最小点对之间的距离是: "+result);
}
/*求出最小的两个点对之间的距离*/
public static double getByDivideConquer(double[][] points) {
if(points.length==1|points.length==0) {
return 0;
}
/*预处理数据*/
double[][] sortedPoints = quickSort(points);
/*求最小点对*/
int [] minIndex = getByDivideConquer(sortedPoints,0,sortedPoints.length-1);
/*求最小点对之间的距离*/
double minDist = getDistance(sortedPoints,minIndex[0],minIndex[1]);
return minDist;
}
/*求出最小的两个点对的索引*/
private static int[] getByDivideConquer(double[][] points, int head, int tail) {
/*初始化最小距离*/
double minDist=getDistance(points,head,head+1);
/*初始化最小点对序号*/
int [] minIndex=new int[] {head,head+1};
/*点数小于等于4时,就直接遍历求最值*/
if(tail-head+1<=4) {
for(int i=head;i<=tail-1;i++) {
for(int j=i+1;j<=tail;j++) {
if(getDistance(points,i,j)<minDist) {
minDist=getDistance(points,i,j);
minIndex[0]=i;
minIndex[1]=j;
}
}
}
}else {
/*左边的最小点对*/
int [] minIndexLeft=getByDivideConquer(points,head,head+(tail-head)/2);
/*右边的最小点对*/
int [] minIndexRight=getByDivideConquer(points,head+(tail-head)/2+1,tail);
/*左边点对最小距离*/
double minDisLeft=getDistance(points,minIndexLeft[0],minIndexLeft[1]);
/*右边点对最小距离*/
double minDisRight=getDistance(points,minIndexRight[0],minIndexRight[1]);
/*左右点对中距离的较小者,即d*/
double minDisTwoSide=Math.min(minDisLeft, minDisRight);
/*左右距离最小值点对对应的索引*/
if(minDisLeft>=minDisRight) {
minDist=minDisRight;
minIndex=minIndexRight;
}else {
minDist=minDisLeft;
minIndex=minIndexLeft;
}
/*设置中间线,该变量为中间线的x轴坐标*/
double middleAxis=(points[head+(tail-head)/2][0]+points[head+(tail-head)/2+1][0])/2;
/*中间线右边的点*/
int i=head+(tail-head)/2+1;
/*i点没越界且和中间线的x轴距离小于d*/
while(i<=tail && (points[i][0]-middleAxis<minDisTwoSide)) {
int count=0;
/*j点没越界且符合条件的个数小于6*/
for(int j=head+(tail-head)/2;j>=head && (points[j][0]-middleAxis<minDisTwoSide) && (count<=6);j--) {
/*找出d*2d矩形的点*/
if(Math.abs(points[j][1]-points[j][1])<minDisTwoSide) {
if(getDistance(points,i,j)<minDist) {
minDist=getDistance(points,i,j);
minIndex[0]=i;
minIndex[1]=j;
}
count++;
}
}
i++;
}
}
return minIndex ;
}
/*计算两个点i和j之间的距离*/
private static double getDistance(double[][] points, int i, int j) {
double xDistance = Math.pow(points[i][0]-points[j][0], 2);
double yDistance = Math.pow(points[i][1]-points[j][1], 2);
return Math.sqrt(xDistance+yDistance);
}
/*开始数据预处理:将点按照横坐标由小到大排序*/
private static double[][] quickSort(double[][] oldData) {
/*clone出一个相同的数组进行操作*/
double[][] newData=oldData.clone();
QSRecursion(newData,0,newData.length-1);
return newData;
}
private static void QSRecursion(double[][] newData, int i, int j) {
if(i==j)
return;
int standardIndex=i+new Random().nextInt(j-i+1);
int pivot=partition(newData,i,j,standardIndex);//保存基准元素的正确位置
if(pivot!=i)
QSRecursion(newData,i,pivot-1);//左边
if(pivot!=j)
QSRecursion(newData,pivot+1,j);//右边
}
private static int partition(double[][] newData, int i, int j, int standardIndex) {
/*将序列首个元素和基准元素对换位置*/
swap(newData,i,standardIndex);
/*i为头指针*/
int left=i+1;
/*j为尾指针*/
int right=j;
while(left<right) {
while(newData[left][0]<=newData[i][0]&left<right) {
left++;
}
while(newData[right][0]>=newData[i][0]&left<right) {
right--;
}
if(left<right) {
swap(newData,right,left);//对换位置,保证左小右大
}
}
if(newData[right][0]>newData[i][0]) {
swap(newData,right-1,i);
standardIndex=right-1;
}else {
swap(newData,j,i);
standardIndex=j;
}
return standardIndex;
}
private static void swap(double[][] newData, int a, int b) {//对换位置
double[] temp=newData[a];
newData[a]=newData[b];
newData[b]=temp;
}
/*结束数据预处理:将点按照横坐标由小到大排序*/
}
测试结果:
最小点对之间的距离是: 1.0
该问题描述为:设有n=2^k个运动员要进行网球循环赛。现要设计一各满足一下要求的比赛日程表:
1>每个选手必须与其他n-1个选手各比赛一次。
2>每个选手一天只能赛一次。
3>循环赛一共进行n-1天。
按照上面的要求,可以将比赛表设计成一个n行n列的二维表,其中第 i 行第 j+1 列的元素表示和第 i 个选手在第 j 天比赛的选手号。
在具体进行代码实现时,可以先填满array的第一行,然后按照左上角数字与右下角数字相同的关系、左下角数字与右上角数字相同的关系,逐渐填满整张表就行。之所以说此问题中体现了分治思想,是因为可以不断对n进行n/2划分,直到划分为2人时,赛程就可确定,然后再逐渐合并出整个赛程安排。示例代码如下:
package divideAndConquer;
public class GameArrange {
public static void main(String[] args) {
/*参与比赛人数*/
int num = 8;
int[][] array = makeTable(num);
for (int i = 0; i < array.length; i++) {
for (int j = 0; j < array[0].length; j++) {
System.out.print(array[i][j] + " ");
}
System.out.println();
}
}
public static int[][] makeTable(int n) {
int[][] arr = new int[n][n];
for (int i = 0; i < n; i++)
arr[0][i] = i + 1;
/*采用分治算法,构造整个赛程表*/
for (int r = 1; r < n; r = r*2) {
for (int i = 0; i < n; i += 2 * r) {
/*左上角的值赋值给右下角*/
copy(arr, r, r + i, 0, i, r);
/*右上角的值赋值给左下角*/
copy(arr, r, i, 0, r + i, r);
}
}
return arr;
}
/*fromx:初始值的行号;fromy:初始值的列号;tox:目的格子的行号;toy:目的格子的列号*/
static void copy(int[][] arr, int tox, int toy, int fromx, int fromy, int r) {
for (int i = 0; i < r; i++) {
for (int j = 0; j < r; j++) {
arr[tox + i][toy + j] = arr[fromx + i][fromy + j];
}
}
}
}
测试结果:
1 2 3 4 5 6 7 8
2 1 4 3 6 5 8 7
3 4 1 2 7 8 5 6
4 3 2 1 8 7 6 5
5 6 7 8 1 2 3 4
6 5 8 7 2 1 4 3
7 8 5 6 3 4 1 2
8 7 6 5 4 3 2 1
汉诺塔问题,源于印度一个古老传说的益智玩具。相传大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。
汉诺塔图示如下:
将所有圆盘从A塔移动到C塔的过程中,不能将大圆盘放到小圆盘上,所以需要遵守一定的规则,这些规则其实就是解该问题的思路,如下:
1>如果只有一个盘, 则直接从A塔(初始塔)移动到C塔(目标塔)。
2>如果圆盘数 >= 2,就需要先将最大的圆盘之上的所有圆盘从A塔(初始塔)移动到B塔(中间塔);将最大的圆盘移动到C塔(目标塔);然后再将其他的圆盘从A塔(初始塔)移动到C塔即可。
示例代码如下:
package divideAndConquer;
public class Hanoitower {
public static void main(String[] args) {
hanoiTower(3, 'A', 'B', 'C');
}
static void hanoiTower(int num, char a, char b, char c) {
/*如果只有一个盘*/
if(num == 1){
System.out.println("第1个盘从 " + a + " -> " + c);
}else{
//如果我们有 n >= 2 情况,我们总是可以看做是两个盘 1.最下边的一个盘 2. 上面的所有盘
//1. 先把 最上面的所有盘 A->B, 移动过程会使用到 c
hanoiTower(num - 1, a, c, b);
//2. 把最下边的盘 A->C
System.out.println("第" + num + "个盘从 " + a + " -> " + c);
//3. 把B塔的所有盘 从 B->C , 移动过程使用到 a塔
hanoiTower(num - 1, b, a, c);
}
}
}
测试结果:
第1个盘从 A -> C
第2个盘从 A -> B
第1个盘从 C -> B
第3个盘从 A -> C
第1个盘从 B -> A
第2个盘从 B -> C
第1个盘从 A -> C