算法篇——分治法例题(最近点对、子序列最大和)

565 阅读4分钟

内容三 最近点对

对平面上给定的N个点,给出所有点对的最短距离

即,输入是平面上的N个点,输出是N点中具有最短距离的两点

  1. 要求随机生成N个点的平面坐标,应用穷举法编程计算出所有点对的最短距离

  2. 要求随机生成N个点的平面坐标,应用分治法编程计算出所有点对的最短距离

穷举法:两层循环,枚举出平面上所有点对的距离,得到最小的,时间复杂度 O(n2)O(n^2),辅助空间O(1)O(1)

分治法:按x轴排序,先大致分成两半找,假设中心点是A(x0,y0x_0,y_0)假设两边找到的最小距离是d,那么中间交叉的部分在距离x=x0dx=x_0-d、与x=x0+dx=x_0+d 这两条垂线中间找(此时按y轴排序用穷举法)。至于为什么,详见www.360doc.com/content/19/…

触底:所划分的区域内有不大于3个点。

import java.util.*;

/**
 * @author SJ
 * @date 2020/10/14
 */
public class FindShortestDistance {

    static class Point {
        private Integer x;
        private Integer y;

        public Point(int x, int y) {
            this.x = x;
            this.y = y;
        }
        //计算两点之间距离
        public int getDistance(Point a){
            return (int) (Math.pow((this.x-a.x),2)+Math.pow((this.y-a.y),2));
        }

        @Override
        public String toString() {
            return "(" + x +
                    ", " + y +
                    ')';
        }

    }

    //生成num个随机点放在list内
    public static List<Point> generateRandomPoint(int num){
        List<Point> list=new ArrayList<>();
        int[] xs=RandomUtil.randomBetween100(num);
        int[] ys=RandomUtil.randomBetween100(num);
        for (int i = 0; i < num; i++) {
            list.add(new Point(xs[i], ys[i])) ;

        }
        return list;
    }
    //穷举法
    public static double findShortestDistance(List<Point> points){
        //将最短距离初始化为list中第0个点和第一个点之间的距离
        int distance= points.get(0).getDistance(points.get(1));
        for (int i = 0; i < points.size(); i++) {
            for (int j = i+1; j <points.size() ; j++) {
                //list内下标为i的点与下标为j的点之间的距离
                int tempDistance=points.get(i).getDistance(points.get(j));
                if (tempDistance<distance){
                    distance=tempDistance;
                }

            }
        }
        return Math.sqrt(distance);

    }
    //list排序
    public static void sortByX(List<Point> points) {
        Collections.sort(points, new Comparator<Point>() {
            @Override
            public int compare(Point o1, Point o2) {
                return o1.x.compareTo(o2.x);
            }
        });
    }
    private static void sortByY(List<Point> points) {
        Collections.sort(points, new Comparator<Point>() {
            @Override
            public int compare(Point o1, Point o2) {
                return o1.y.compareTo(o2.y);
            }
        });
    }

    //分治法
    public static double findShortestDistance2(List<Point> points,int low,int high){

        //只剩两个点
        if (high-low==1)
            return Math.sqrt(points.get(high).getDistance(points.get(low)));
        //还剩3个点
        else if (high-low==2){
            double a=Math.sqrt(points.get(low).getDistance(points.get(low+1)));
            double b=Math.sqrt(points.get(low+1).getDistance(points.get(high)));
            double c=Math.sqrt(points.get(low).getDistance(points.get(high)));
            return Math.min(Math.min(a,b),c);
        }
        else {
            int mid=(low+high)/2;
            double leftMin=findShortestDistance2(points,low,mid);
            double rightMin=findShortestDistance2(points,mid+1,high);

            double min=Math.min(leftMin,rightMin);

            //重点来了
            //1.找到横坐标与中点横坐标距离在min以内的的点,不包括min
            List<Point> candidate=new ArrayList<>();
            for (int i = low; i <=high; i++) {
                if (Math.abs(points.get(i).x-points.get(mid).x)<min)
                    candidate.add(points.get(i));
            }

            //找出这些点后根据纵坐标排序
            sortByY(candidate);
            //在这个小范围内用穷举法
            for (int i = 0; i < candidate.size(); i++) {
                for (int j = i+1; j <candidate.size(); j++) {
                    if ((candidate.get(j).y-candidate.get(i).y)>min)
                        break;
                    double temp=Math.sqrt(candidate.get(j).getDistance(candidate.get(i)));
                    if (temp<min)
                        min=temp;

                }

            }
            return min;
        }
    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.println("输入点的数目:");
        int num = scanner.nextInt();
        //生成num个随机点存在pointList中
        List<Point> pointList=generateRandomPoint(num);
        System.out.println("生成的随机点对为:");
        for (Point point : pointList) {
            System.out.print(point+" ");
        }
        System.out.println();
        //使用穷举法计算
        double shortestDistance = findShortestDistance(pointList);
        System.out.println("穷举所得最短距离为:"+shortestDistance);

        //使用分治法计算
        sortByX(pointList);
        double shortestDistance2 = findShortestDistance2(pointList, 0, pointList.size() - 1);
        System.out.println("分治法所得最短距离为:"+shortestDistance2);


    }

}

测试结果:

"C:\Program Files\Java\jdk1.8.0_131\bin\java.exe"...
输入点的数目:
10
生成的随机点对为:
(12, 1) (3, 11) (12, 8) (10, 7) (14, 6) (3, 18) (1, 8) (9, 18) (15, 14) (5, 11) 
穷举所得最短距离为:2.0
分治法所得最短距离为:2.0

Process finished with exit code 0

复杂度分析:分治法:取决于选择的排序算法,我用的是Java提供的Collections.sort(),其内部采取的是归并排序,时间复杂度是O(nlogn)O(nlogn)。递归,区间深度不超过O(logn)O(logn).总得来说时间复杂度O(nlogn)O(nlogn)

除了存对象用的List,辅助空间还有计算交叉部分的距离时用的list,下面这里。

image-20201014211908624

内容四 子序列最大和

给出一个整数序列,选出其中连续且非空的一段使得这段和最大

要求:使用分治法解决

思路:

我们来分析一下这个问题, 我们先把数组平均分成左右两部分。

此时有三种情况:

  • 最大子序列全部在数组左部分
  • 最大子序列全部在数组右部分
  • 最大子序列横跨左右数组

对于前两种情况,我们相当于将原问题转化为了规模更小的同样问题。

横跨左右数组的用枚举法:左边的选择从middle-1开始挨着盘得往左累加,把最累加最大得结果记下来;

右边的从middle+1开始也是同样的方法。最后三个部分加起来就是第三中情况的结果。

随机数还是调用那个类,范围从-25到+25

    public static int[] randomBetween100(int length){
        int[] nums=new int[length];
        for (int i = 0; i < nums.length; i++) {
            nums[i]=(int)(Math.random()*(-50)+25);
        }
        return nums;
    }
import java.util.Arrays;
import java.util.Scanner;

/**
 * @author SJ
 * @date 2020/10/14
 */
public class MaxSum {
    public static int findMaxSum(int[] nums, int left, int right) {
        //只剩一个数字
        if (right == left)
            return nums[left];
            //只剩两个值
        else if (right - left == 1) {

            return Math.max(Math.max(nums[left], nums[right]), (nums[right] + nums[left]));
        }
        int mid = (left + right) / 2;
        //左边序列最大和
        int leftMax = findMaxSum(nums, left, mid - 1);
        //右边序列最大和
        int rightMax = findMaxSum(nums, mid + 1, right);

        //找交叉项
        int rightSum = 0;
        int tempRightMax = nums[mid + 1];
        for (int i = mid + 1; i <= right; i++) {
            rightSum += nums[i];
            if (rightSum > tempRightMax)
                tempRightMax = rightSum;

        }
        int leftSum = 0;
        int tempLeftMax = nums[mid - 1];
        for (int i = mid - 1; i >= left; i--) {
            leftSum += nums[i];
            if (leftSum > tempRightMax)
                tempLeftMax = leftSum;
        }
        //跨界序列最大和
        int middleMax = tempLeftMax + nums[mid] + tempRightMax;

        return Math.max(Math.max(leftMax, rightMax), middleMax);

    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.println("输入点序列长度:");
            int length = scanner.nextInt();
            int[] nums = RandomUtil.randomBetween100(length);
            System.out.println("生成的随机序列为:" + Arrays.toString(nums));
            int max = findMaxSum(nums, 0, nums.length - 1);
            System.out.println("最大和为:" + max);
        }
    }
}

结果:

"C:\Program Files\Java\jdk1.8.0_131\bin\java.exe"...
输入点序列长度:
5
生成的随机序列为:[0, 15, -4, 19, 7]
最大和为:37
输入点序列长度:
6
生成的随机序列为:[12, 23, -7, 10, -12, -11]
最大和为:38
输入点序列长度:
7
生成的随机序列为:[2, 9, 19, 3, 23, 0, 8]
最大和为:53
输入点序列长度:
10
生成的随机序列为:[-18, 17, 24, 12, -4, 20, 4, -5, 19, -20]
最大和为:87
输入点序列长度:

时间复杂度为 O(nlgn)O(nlgn), 空间复杂度为 O(1)O(1)