算法练习:分治法

分治法

  • 在计算机科学中,分治法是一种很重要的算法。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。
  • 精髓
    分–将问题分解为规模更小的子问题;
    治–将这些规模更小的子问题逐个击破;
    合–将已解决的子问题合并,最终得出“母”问题的解;
  • 可解决问题的特征
  1. 该问题的规模缩小到一定的程度就可以容易地解决
  2. 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。
  3. 利用该问题分解出的子问题的解可以合并为该问题的解;
  4. 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题。
  • 生活实例
    在一堆硬币中有一个是假币,重量较真币轻,若两两相比费时费力。采用分治法,将硬币分成两堆相比,将轻的那一堆再分成两堆相比……
  • 经典应用
    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)将另一序列剩下的所有元素直接复制到合并序列尾。
图片来源https://mp.weixin.qq.com/s/vn3KiV-ez79FmbZ36SX9lg

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)重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了。
图片来源https://mp.weixin.qq.com/s/vn3KiV-ez79FmbZ36SX9lg

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;
	}

练习题

  1. 找一对数
    给定一个整数数组 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("没有这两个数");
    }
}
  1. 农夫和奶牛
    农夫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;
	}
}
发布了41 篇原创文章 · 获赞 1 · 访问量 1461

猜你喜欢

转载自blog.csdn.net/qq_44467578/article/details/104124956