分治的几个常见问题

二分搜索

时间复杂度:O(logn)
在这里插入图片描述

二分搜索技术(从排好序的n个元素(由小到大排序)中寻找一个中寻找元素x的坐标)
输入举例(即给十个数,找里面的31的坐标):
10 321
1 1 2 3 5 7 12 31 111 321
输出举例
7
package section2.BinarySearch;
import java.util.Scanner;
public class BinarySearch {
	public static void main(String[] args) {
		Scanner cin =new Scanner(System.in);
		int total = cin.nextInt();	//数据个数
		int num = cin.nextInt();	//要找的数据
		int[] array = new int[total];
		for (int i = 0; i < array.length; i++) {
			array[i]=cin.nextInt();
		}
		System.out.println(find(total,num,array));
	}
	public static int find(int total, int num, int[] arr){	
		int left = 0; int right =total-1;
		while(left<=right){
			int middle = (left+right)/2;	//每次查找都缩小一半范围
			if(arr[middle]==num){	//找到
				return middle;
			}else if(arr[middle]>num){
				right = middle-1;	//注意-1  避免(8+9)/=8这样的死循环
			}else{
				left=middle+1;	//注意+1	避免(8+9)/=8这样的死循环
			}
		}
		return -1;	//没找到
	}
}


大整数乘法

请设计一个有效的算法,可以进行两个n位大整数的乘法运算

1 最暴力的方法:O(n^2)
2 我们采用分而治之的思想

将X和Y按如下方法分成两部分
在这里插入图片描述
那么
X = A10^(n/2) + B
Y = C
10^(n/2) + D

X*Y = (A*10^(n/2) + B)*( C*10^(n/2) + D)

    = A*C*10^n + A*D*10^(n/2) + B*C*10^(n/2) + B*D

复杂度分析
在这里插入图片描述
为了降低时间复杂度,必须减少乘法的次数上式可改写为

X*Y = (A*10^(n/2) + B)*( C*10^(n/2) + D)

       = A*C*10^n + A*D*10^(n/2) + B*C*10^(n/2) + B*D

      = A*C*10^n + ((A+B)(C+D) – A*C – B*D)*10*(n/2) + B*D

或者

     = A*C*10^n + ((A-B)(D-C) + A*C + B*D)*10*(n/2) + B*D

复杂度分析
在这里插入图片描述

package section2.Divide_and_Conquer;
public class BigDataMultiple {
	public static void main(String[] args) {
		// 大整数相乘
		long x = 12234L;
		long y = 45243L;
		String sx = String.valueOf(x);	//sx为12234
		int n = sx.length();
		long sig = sign(x) * sign(y);	//符号运算
		double s = bigdataride(x, y, n);
		System.out.println("大数相乘的计算结果为:" + s * sig);
	}
	
	public static int sign(long a) {
		return a < 0 ? -1 : 1;
	}

	public static double bigdataride(long x, long y, int n) {
		x = Math.abs(x);
		y = Math.abs(y);
		if (n == 1) {
			return x * y;
		}else{
			if (n % 2 == 1) {
				n = n - 1; // 若数字长度n为奇数,则转化为偶数
			}
			long a = x / Math.round(Math.pow(10, (n / 2)));	//x的前半段
			long b = x - a * Math.round(Math.pow(10, (n / 2)));	//x的后半段
			long c = y / Math.round(Math.pow(10, (n / 2)));	//y的前半段
			long d = y - c * Math.round(Math.pow(10, (n / 2)));	//y的后半段
			double ac = bigdataride(a, c, n / 2);// 递归计算a*c(每一次都拆分为两半)
			double bd = bigdataride(b, d, n / 2);// 计算b*d
			long aJb = a + b;
			long cJd = c + d;
			double abcd = bigdataride(aJb, cJd, n / 2);
			return (ac * Math.pow(10, n) + (abcd - ac - bd)* Math.pow(10, n / 2) + bd);	//每一轮递归都产生一个拆分好几次后得到的两个数的乘积
		}
	}
}

Strassen矩阵乘法

暴力的矩阵乘法时间复杂度为O(n的三次方)

Strassen矩阵乘法采用的方法与大整数乘法方法类似,利用分治的策略,将一个矩阵递归的划分为一半,只不过大整数乘法是只在数的长度上分为了两半,而Strassen矩阵乘法是将矩阵在长和宽上都划分为了两半,即第一次时将一个矩阵分为四个矩阵
时间复杂度变为
在这里插入图片描述
由于和大整数乘法一样改进不是很大,所以以后有时间在学,先写出暴力法矩阵乘法的代码

//暴力法矩阵乘法
package section2.Divide_and_Conquer;
public class Strassen {
	public static void main(String[] args) {
		int[][] x1 = {{1,2,3,4,5},{5,4,3,2,1},{5,4,3,2,1}};	//第一个矩阵的行数要和第二个矩阵的列数相等
		int[][] x2 = {{3,2},{1,2},{5,3},{1,2},{7,3}};	//第二个矩阵的行数要和第一个矩阵的列数相等
		int[][] result = new int[x1.length][x2[0].length];	//结果矩阵的行数等于x1的行数,结果的列数等于x2的列数
		
		for (int i = 0; i < x1.length; i++) {	//结果数组的第i行
			for (int j = 0; j < x2[0].length; j++) {	//结果数组的第j列
				int num = 0;	//记录result[i][j]的值
				for (int k = 0; k < x1[0].length; k++) {	//结果中的每个数都需要经历k次运算
					num = num + x1[i][k]*x2[k][j];
				}
				result[i][j] = num;
			}
		}
		for (int i = 0; i < result.length; i++) {	//输出结果数组
			for (int j = 0; j < result[0].length; j++) {
				System.out.print(result[i][j]+" ");
			}
			System.out.println();
		}
		
	}

}

棋盘覆盖

在这里插入图片描述

//棋盘覆盖
//问题:用4种不同形状的L型骨牌覆盖给定棋盘上除特殊方格以外的所有方格,且任何2个L型骨牌不得重叠覆盖。
//思路:应用分治法
//分治的技巧在于如何划分棋盘,使划分后的子棋盘的大小相同,并且每个子棋盘均包含一个特殊方格,从而将原问题分解为规模较小的棋盘覆盖问题。
//k>0 时,可将2^k×2^k的棋盘划分为4个2^(k-1)×2^(k-1)的子棋盘,如图4.11(a)所示。这样划分后,由于原棋盘只有一个特殊方格,所 以,
//这4个子棋盘中只有一个子棋盘包含该特殊方格,其余3个子棋盘中没有特殊方格。为了将这3个没有特殊方格的子棋盘转化为特殊棋盘,以便采用递归方法求 解,
//可以用一个L型骨牌覆盖这3个较小棋盘的会合处,从而将原问题转化为4个较小规模的棋盘覆盖问题。递归地使用这种划分策略, 直至将棋盘分割为1×1的子棋盘。
package section2.Divide_and_Conquer;
import java.util.Scanner;
public class ChessboardCoverage {
	public static void main(String[] args) {
		int size = 4; // 棋盘的长,固定为4
		int dr = 1; // 特殊棋子所在的行
		int dc = 1; // 特殊棋子所在的列
		ChessboardCoverage c = new ChessboardCoverage();
		c.chessBoard(0, 0, dr, dc, size);
		for (int i = 0; i < 4; i++) {
			for (int j = 0; j < 4; j++) {
				System.out.print(board[i][j] + "  ");
			}
			System.out.println();
		}
	}

	int title = 1; // 表示L型骨牌的编号
	static int[][] board = new int[4][4];
	// 处理带有特殊棋子的棋盘.tr、tc表示棋盘的入口即左上角的行列号,dr、dc表示特殊棋子的行列位置,size表示棋盘的行数或者列数
	public void chessBoard(int tr, int tc, int dr, int dc, int size) {
		if (size == 1) return;
		int t = title++; // 骨牌编号
		System.out.println(t);
		int s = size / 2;// 每一次化大棋盘为一半的子棋盘
		// 要处理带有特殊棋子的棋盘,第一步先处理左上棋盘(后面三步与此步重复)
		if (dr < tr + s && dc < tc + s)// 左上角子棋盘有特殊棋子
			chessBoard(tr, tc, dr, dc, s);// 处理有特殊棋子的左上角子棋盘
		else// 处理无特殊棋子的左上角子棋盘
		{
			board[tr + s - 1][tc + s - 1] = t;// 设左上角子棋盘的右下角为特殊棋子,用t型的骨牌覆盖。由于骨牌有三种,当处理过程中同一级设置的特殊棋子用相同的骨牌覆盖
			chessBoard(tr, tc, tr + s - 1, tc + s - 1, s);// 处理有用骨牌覆盖的格子作为特殊棋子的左上角子棋盘
		}
		// 第二步处理右上角棋盘
		if (dr < tr + s && dc >= tc + s)// 右上角子棋盘有特殊棋子
		{
			chessBoard(tr, tc + s, dr, dc, s);// 处理有特殊棋子的右上角子棋盘
		} else {
			board[tr + s - 1][tc + s] = t;// 设右上角子棋盘的左下角为特殊棋子,用t型的骨牌覆盖。由于骨牌有三种,当处理过程中同一级设置的特殊棋子用相同的骨牌覆盖
			chessBoard(tr, tc + s, tr + s - 1, tc + s, s);// 处理有用骨牌覆盖的格子作为特殊棋子的右上角子棋盘
		}
		// 第三步处理左下角子棋盘
		if (dr >= tr + s && dc < tc + s)// 左下角子棋盘有特殊棋子
		{
			chessBoard(tr + s, tc, dr, dc, s);// 处理有特殊棋子的左下角子棋盘
		} else {
			board[tr + s][tc + s - 1] = t;// 设左下角子棋盘的右上角为特殊棋子,用t型的骨牌覆盖。由于骨牌有三种,当处理过程中同一级设置的特殊棋子用相同的骨牌覆盖
			chessBoard(tr + s, tc, tr + s, tc + s - 1, s);// 处理有用骨牌覆盖的格子作为特殊棋子的左下角子棋盘
		}
		// 第四步处理右下角棋盘
		if (dr >= tr + s && dc >= tc + s)// 右下角子棋盘有特殊棋子
		{
			chessBoard(tr + s, tc + s, dr, dc, s);// 处理有特殊棋子的右下角子棋盘
		} else {
			board[tr + s][tc + s] = t;// 设子棋盘右下角的左上角为特殊棋子,用t型的骨牌覆盖。由于骨牌有三种,当处理过程中同一级设置的特殊棋子用相同的骨牌覆盖
			chessBoard(tr + s, tc + s, tr + s, tc + s, s);// 处理有用骨牌覆盖的格子作为特殊棋子的右下角子棋盘
		}
	}
}

合并排序

时间复杂度:nlog2n

如果a的x次方=N(a>0,且a≠1),那么数x叫做以a为底N的对数,记作x=logaN

O(lgn):
将一个数据集分成两半,然后将分开的每一半再分成两半,依此类推
O(nlgn):
将一个数据集分成两半,然后将分开的每一半再分成两半,依此类推,在此过程中同时遍历每一半数据
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

//合并排序
package section2.Divide_and_Conquer;
public class MergeSort {
	public static void main(String[] args) {
		int[] arr = {1,2,9,8,3,7,6,5,4,3,2,1};
		sort(0,11,arr);
		for (int i = 0; i < arr.length; i++) {
			System.out.print(arr[i]+" ");
		}
	}
	public static void sort(int begin,int end,int[] arr){	//往里递归时每次1/2的分割数组,往外递归的时候合并排序
		if(begin<end){
			sort(begin,(begin+end)/2,arr);	//左边归并排序,使得左子序列有序
			sort((begin+end)/2+1,end,arr);	//右边归并排序,使得右子序列有序
			merge(begin,end,arr);	//将两个有序子数组合并操作(其实真正的排序都在这里面)
		}
	}
	public static void merge(int begin,int end,int[] arr){
		int i = begin;	//左序列指针
		int j = (begin+end)/2+1;	//右序列指针
		int[] temp = new int[arr.length];	//临时数组
		int index = 0;	//临时数组指针
		while(i<=(begin+end)/2&&j<=end){	//此while排完之后左右数组中交叉大小的已经合并完毕
											//剩下的是较大的部分,不用排序了,直接接在后面,在后面的两个while中实现
			if(arr[i]>arr[j]){
				temp[index] = arr[j];
				index++;
				j++;
			}else{
				temp[index] = arr[i];
				index++;
				i++;
			}
		}
		while(i<=(begin+end)/2){	//将左边剩余元素填充进temp中
			temp[index] = arr[i];
			index++;
			i++;
		}
		while(j<=end){	//将右序列剩余元素填充进temp中
			temp[index] = arr[j];
			index++;
			j++;
		}
		//将temp中的元素全部拷贝到原数组中
		for (int k = begin; k <=end ; k++) {
			arr[k] = temp[k-begin];
		}
	}
}

快速排序

时间复杂度:nlog2n
在这里插入图片描述
在这里插入图片描述

//快速排序(极为重要)
package section2.Divide_and_Conquer;
public class QuickSort {
	public static void main(String[] args) {
		int[] arr = {1, 49, 38, 65, 97, 23, 22, 76, 1, 5, 8, 2, 0, -1, 22 };
		quickSort(arr, 0, arr.length - 1);
		System.out.println("排序后:");
		for (int i : arr) {
			System.out.print(i+" ");
		}
	}

	private static void quickSort(int[] arr, int low, int high) {
		if (low < high) {
			// 找寻基准数据的正确索引
			int index = getIndex(arr, low, high);	//每次运行之后arr都会改变
			// 进行迭代对index之前和之后的数组进行相同的操作使整个数组变成有序
			//(因为index之前的数在一次quickSort之后已经都比index处的值小,且index之后都比index大
			//所以现在分别对index前的数在排序,对index后的数在排序,index处的数不用变了)
			quickSort(arr, low, index - 1);
			quickSort(arr, index + 1, high);
		}
	}

	private static int getIndex(int[] arr, int low, int high) {
		// 基准数据
		int tmp = arr[low];
		while (low < high) {
			// 当队尾的元素大于等于基准数据时,向前挪动high指针
			while (low < high && arr[high] >= tmp) {
				high--;
			}
			// 如果队尾元素小于tmp了,需要将其赋值给low
			arr[low] = arr[high];
			// 当队首元素小于等于tmp时,向前挪动low指针
			while (low < high && arr[low] <= tmp) {
				low++;
			}
			// 当队首元素大于tmp时,需要将其赋值给high
			arr[high] = arr[low];
		}
		// 跳出循环时low和high相等,此时的low或high就是tmp的正确索引位置
		// 由原理部分可以很清楚的知道low位置的值并不是tmp,所以需要将tmp赋值给arr[low]
		arr[low] = tmp;
		return low; // 返回tmp的正确位置
	}	
}

循环赛日程表

问题描述

设有n=2k个运动员要进行网球循环赛。现要设计一个满足以下要求的比赛日程表:
每个选手必须与其他n-1个选手各赛一次;
每个选手一天只能参赛一次;
循环赛在n-1天内结束。
请按此要求将比赛日程表设计成有n行和n-1列的一个表。在表中的第i行,第j列处填入第i个选手在第j天所遇到的选手。其中1≤i≤n,1≤j≤n-1。

package section2.Divide_and_Conquer;
import java.util.Scanner;
public class RoundRobinSchedule {
	public static void main(String[] args) {
		Scanner in = new Scanner(System.in);
		int k = in.nextInt();
		int n = 1;	//参赛人数为2的k次方
		for (int i = 0; i < k; i++) {
			n = n*2;
		}
		int[][] result = table(n,k);
		for (int i = 0; i < result.length; i++) {
			for (int j = 0; j < result.length; j++) {
				System.out.print(result[i][j]+"   ");
			}
			System.out.println();
		}
	}
	public static int[][] table(int n,int k){
		int[][] table = new int[n][n];
		for (int i = 0; i < table.length; i++) {
			table[0][i] = i+1;
		}
		int m = 1;
		for (int i = 1; i <= k; i++) {	//可以等分k次,即分治k次,k:参赛人数为2的k次方
			m = m*2;	//此次处理的正方形长
			for (int z = 0; z < n/m; z++) {		//每一轮的前m行,n列都要进行相似处理,m在变,n不变(即处理多少个正方形)
				for (int j = 0; j < m/2; j++) {	//x	处理的其中一个正方形的x坐标
					for (int x = 0; x < m/2; x++) {	//y	处理的其中一个正方形的y坐标
						table[j+m/2][x+m*z] = table[j][x+m/2+m*z];	//正方形的左下角等于右上角
					}
					for (int s = m/2; s < m; s++) {	//y	处理的其中一个正方形的y坐标
						table[j+m/2][s+m*z] = table[j][s-m/2+m*z];	//正方形的右下角等于左上角
					}
				}
			}
		}
		return table;
	}
	
}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值