分治法
- 在计算机科学中,分治法是一种很重要的算法。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。
- 精髓:
分–将问题分解为规模更小的子问题;
治–将这些规模更小的子问题逐个击破;
合–将已解决的子问题合并,最终得出“母”问题的解; - 可解决问题的特征
- 该问题的规模缩小到一定的程度就可以容易地解决
- 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。
- 利用该问题分解出的子问题的解可以合并为该问题的解;
- 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题。
- 生活实例
在一堆硬币中有一个是假币,重量较真币轻,若两两相比费时费力。采用分治法,将硬币分成两堆相比,将轻的那一堆再分成两堆相比…… - 经典应用
1. 折半搜索
折半查找法也称为二分查找法,它充分利用了元素间的次序关系,采用分治策略,可在最坏的情况下用O(log n)完成搜索任务。它的基本思想是:(这里假设数组元素呈升序排列)将n个元素分成个数大致相同的两半,取a[n/2]与欲查找的x作比较,如果x=a[n/2]则找到x,算法终止;如 果x<a[n/2],则我们只要在数组a的左半部继续搜索x;如果x>a[n/2],则我们只要在数组a的右 半部继续搜索x。
public static int binarysearch(int[] num,int x){
int L=0;
int R=num.length-1;
while(L<=R) {
int mid=L+(R-L)/2;//不要用(L+R)/2,有可能溢出int
if(x==num[mid]) return mid;
else if(x>num[mid]) L=mid+1;
else R=mid-1;
}
return -1;
}
2. 归并排序
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列。步骤如下:
1)申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
2)设定两个指针,最初位置分别为两个已经排序序列的起始位置;
3)比较两个指针所指向的元素,选择较小的元素放入到合并空间,并移动指针到下一位置;
4) 重复步骤 3 直到某一指针达到序列尾;
5)将另一序列剩下的所有元素直接复制到合并序列尾。
public static void GB(int[] arr,int start,int end,int[] help) {
if(start<end) {
int mid=start+(end-start)/2;
GB(arr,start,mid,help);
GB(arr,mid+1,end,help);
HB(arr,start,mid,end,help);
}
}
public static void HB(int[] arr, int start, int mid, int end, int[] help) {
int pa=0;
int ps=start,pe=mid+1;
while(ps<=mid&pe<=end) {
if(arr[ps]<arr[pe])
help[pa++]=arr[ps++];
else
help[pa++]=arr[pe++];
}
while(ps<=mid)
help[pa++]=arr[ps++];
while(pe<=end)
help[pa++]=arr[pe++];
for (int i = 0; i < pa; i++)
arr[start+i]=help[i];
}
3. 快速排序
快速排序算法通过多次比较和交换来实现排序,其排序流程如下:
(1)首先设定一个分界值,通过该分界值将数组分成左右两部分。
(2)将大于或等于分界值的数据集中到数组右边,小于分界值的数据集中到数组的左边。此时,左边部分中各元素都小于或等于分界值,而右边部分中各元素都大于或等于分界值。
(3)然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。
(4)重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了。
public static void KS(int[] arr,int start,int end) {
if(start>=end) return;
int k=arr[start];
//基准值如何选取最优还没有结论,但是由于基准值的不同选择,快速排序的时间复杂度会有相当的差距。
//比如我选择了每次都是第一个做基准值,如果待排序列是降序的,那这种算法无疑是非常差的。
int left=start,right=end;//左右指针
while(left!=right) {
while(right>left & arr[right]>=k)
right--;
swap(arr, left, right);
while(left<right & arr[left]<=k)
left++;
swap(arr,left,right);
}
KS(arr,start,left-1);
KS(arr,right+1,end);
}
//对值的交换要利用数组
static void swap(int[] a,int x,int y) {
int temp;
temp=a[x];
a[x]=a[y];
a[y]=temp;
}
练习题
- 找一对数
给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。
- 容易想到的暴力解法,遍历两遍数组
时间复杂度:O(n^2)
对于每个元素,我们试图通过遍历数组的其余部分来寻找它所对应的目标元素,这将耗费 O(n) 的时间。因此时间复杂度为O(n^2)
class Solution {
public int[] twoSum(int[] nums, int target) {
for(int i=0;i<nums.length-1;i++){
for(int j=i+1;j<nums.length;j++){
if(nums[i]+nums[j]==target){
return new int[]{nums[i],nums[j]};
}
}
}
throw new IllegalArgumentException("没有这两个数");
}
}
- 若使用二分法
1)将数组排序,利用库函数sort(快速排序),时间复杂度O(n * log(n))
2)对数组中每个元素二分查找是否存在target-nums[i],每次查找复杂度O(log(n)),最坏要查找n-2次,所以这部分的复杂度也是O(n * log(n))
则这个方法的总的时间复杂度依旧是O(n * log(n)) - 将之改进
1)将数组排序,利用库函数sort(快速排序),时间复杂度O(n * log(n))
2)因为数组已经有序,设置两个指针i,j,初始化i=0,j=nums.length-1
若a[i]+a[j]>m,j–;若a[i]+a[j]<m,i++;直到a[i]+a[j]=m
这部分的复杂是O(log(n))
这种方法的复杂度依旧是O(n * log(n))
class Solution {
public int[] twoSum(int[] nums, int target) {
Arrays.sort(nums);
int i=0;
int j=nums.length-1;
int sum=nums[i]+nums[j];
while(i!=j){
if(sum>target){
j--;
}
if(sum<target){
i++;
}
sum=nums[i]+nums[j];
if(sum==target){
return new int[]{i,j};
}
}
throw new IllegalArgumentException("没有这两个数");
}
}
- 农夫和奶牛
农夫john建造了一座很长的畜栏,它包括N(2<=N<=100000)个隔间,这些小隔间的位置为x0,…,xn-1(0<=xi<=1000000000,均为整数,各不相同)。
john的C(2<=C<=N)头牛每头分到一个隔间。牛都希望互相离得远点省得互相打扰。怎样才能使任意两头牛之间的最小距离尽可能的大,这个最大的最小距离是多少呢?
- 理解题意,可以想成是一条长xi(0<=xi<=1000000000)的线段上随机分布N(2<=N<=100000)个点,现在取其中C(2<=C<=N)个点,如何让两点之间的最小距离最大
- 暴力解法,枚举xi/C~1,设这个距离为D,将第一头牛放在x0,在离这个点D之后有无可取点,有则再取D之后,无则说明该距离不可取,D-1,回到x0重新尝试,一直到取完C个点,输出D。
显而易见,这种方法的复杂度是xi/C*N,最大为1000000000(10亿)。毫无疑问会超时。 - 折半搜索,枚举区间为[1~xi/C],从中间开始尝试,若可行,记住D,在后半区进行尝试;若不可行,在前半区尝试;
复杂度为log(xi/C)*N
package 蓝桥;
import java.util.Arrays;
import java.util.Scanner;
public class Main {
static int N;//可取点数
static int C;//需取点数
static int D;//最大的最小间距
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
N = sc.nextInt();
C = sc.nextInt();
int[] num = new int[N];
for (int i = 0; i < N; i++) {
num[i] = sc.nextInt();
}
Arrays.sort(num);
C(0,num[num.length-1],num);
System.out.println(D);
}
private static void C(int start, int end, int[] num) {
int mid = start + (end - start) / 2;
if (start <= end) {
//可行查右半区,不可行查左半区
if (YN(mid, num)) {
C(mid + 1, end, num);
} else {
C(start, mid - 1, num);
}
}
}
private static boolean YN(int d, int[] num) {
int i = num[0];
int count = 1;
for (int j = 1; j < num.length; j++) {
if (num[j] >= i + d) {
i = num[j];
count++;
}
//可行则保存D
if (count == C) {
D=d;
return true;
}
}
return false;
}
}