算法篇——回溯和分支限界

155 阅读2分钟
  1. 设计算法找出n个元素中第k小元素,过程,分析

    暴力解法:先对数组进行升序排序

    第1小,索引为0;第k小,索引为k-1

    调用java的Arrays.sort():看数组长度 小于47用插入排序; [47,286)用快速排序; 大于等于286:连续性不好用快速排序,连续性好用归并

    具体的时间复杂度和空间复杂度根据数据量的大小而定,

    public class Solution {
    
        public int findKthMin(int[] nums, int k) {
            int len = nums.length;
            Arrays.sort(nums);
            return nums[k-1];
        }
    }
    
    

    方法二:借助 partition 操作定位到最终排定以后索引为 k-1 的那个元素(特别注意:随机化切分元素)

    partition(切分)操作总能排定一个元素,还能够知道这个元素它最终所在的位置,这样每经过一次 partition(切分)操作就能缩小搜索的范围,这样的思想叫做 “减而治之”(是 “分而治之” 思想的特例)。

    时间复杂度:O(N),这里 N 是数组的长度

    空间复杂度:O(1),原地排序,没有借助额外的辅助空间。

    import java.util.Random;
    
    /**
     * @author SJ
     * @date 2020/12/3
     */
    public class FindKthMin {
        public static int findKthMin(int[] nums,int k){
            int left=0;
            int right=nums.length-1;
            int targetIndex=k-1;//第k小,下标是k-1
    
            while (true){
                int index=partition(nums,left,right);
                if (index>targetIndex)
                    right=index-1;
                else if (index<targetIndex)
                    left=index+1;
                else
                    return nums[index];
            }
        }
    
        private static int partition(int[] nums, int left, int right) {
            //在区间上随机选择一个点,pivot 中心点
            Random random=new Random();
            int randomIndex = random.nextInt(right - left + 1)+left;//随机生成一个[left,right]区间内的随机数
            swap(nums,left,randomIndex);//把随机选定的点与最左边的元素进行交换用作划分点
    
            int pivot=nums[left];
            //l和r是用于移动的指针,从后往前找比pivot小的,从前往后找比pivot大的
            int l=left;
            int r=right;
    
            while (l<r){
                while (nums[r]>=pivot&&l<r)
                    r--;
                swap(nums,r,l);
    
                while (nums[l]<=pivot&&l<r)
                    l++;
    
                swap(nums,l,r);
            }
            nums[l]=pivot;
            return l;
    
        }
    
        //交换数组上两个数的值
        private static void swap(int[] nums, int index1, int index2) {
            int temp = nums[index1];
            nums[index1] = nums[index2];
            nums[index2] = temp;
        }
    
        public static void main(String[] args) {
            int[] nums={1,2,3,5,2,4,3,0};
            int k=2;
            int kthMin = findKthMin(nums, 3);
            System.out.println(kthMin);
    
            int kthMin1 = findKthMin(nums, 2);
            System.out.println(kthMin1);
    
            int kthMin2 = findKthMin(nums, 1);
            System.out.println(kthMin2);
    
            int kthMin3 = findKthMin(nums, 4);
            System.out.println(kthMin3);
        }
    
    
    }
    
    

    结果:

    "C:\Program Files\Java\jdk1.8.0_131\bin\java.exe" 
    2
    1
    0
    2
    
    Process finished with exit code 0
    
    
  2. 求逆序对,算法和分析

    import java.util.Arrays;
    
    /**
     * @author SJ
     * @date 2020/12/3
     */
    public class ReversePair {
        public static void main(String[] args) {
            int[] nums = {7, 5, 6, 4};
            int i = mergeSort(nums, 0, nums.length - 1);
            System.out.println(i);
    
    
        }
    
        public static int mergeSort(int[] nums, int left, int right) {
            int leftCount = 0;
            int rightCount = 0;
            int aa = 0;
            if (left == right) {
                return 0;
            } else if (left < right) {
                int middle = (left + right) / 2;
                leftCount = mergeSort(nums, left, middle);
                rightCount = mergeSort(nums, middle + 1, right);
                aa = merge(nums, left, right);
            }
            return leftCount + rightCount + aa;
        }
    
        public static int[] temp = new int[10];
    
        private static int merge(int[] nums, int left, int right) {
            for (int i = left; i <= right; i++) {
                temp[i] = nums[i];
            }
            int middle = (left + right) / 2;
            //合并left到middle  和middle+1到right两个数组
    
            int l = left;
            int r = middle + 1;
            int count = 0;
    
            for (int i = left; i <= right; i++) {
                //左边的合并完了右边的害没结束,把右边的全部加进来
                if (l == middle + 1 && r <= right)
                    nums[i] = temp[r++];
                else if (r == right + 1 && l <= middle)
                    nums[i] = temp[l++];
                else if (temp[l] <= temp[r])
                    nums[i] = temp[l++];
                else {
                    nums[i] = temp[r++];
    				//	计算逆序本趟归并找到的逆序对个数
                    count += middle - l + 1;
                }
            }
    
            return count;
        }
    }
    
    

    利用合并排序的合并过程计算逆序对,时间复杂度就是合并排序的时间复杂度O(nlogn)O(nlogn),辅助空间为一个和原数组等大的数组

  3. 简述回溯法和分支限界法

    TSP问题回溯法和分治限界法的解空间树的形态,举例说明segmentfault.com/a/119000002…

    回溯法

    回溯法有点类似于暴力枚举的搜索过程,回溯法的基本思想是按照深度优先搜索的策略,从根节点出发深度搜索解空间树,当搜索到某一节点时,如果该节点可能包含问题的解,则继续向下搜索;反之回溯到其祖先节点,尝试其他路径搜索。

    如果问题只要求求得一个可行解,那么搜索到问题的一个解即可结束;如果问题所求是最优解,那么需要搜索整个解空间树,得到所有解之后择最优作为问题的解,或者在搜索到叶子节点之前已经能确定该路径不为最优解时就可以进行剪枝,节省搜索时间,那么本文的旅行售货员问题属于后者。

    import java.util.*;
    
    /**
     * @author SJ
     * @date 2020/12/3
     */
    public class TSP {
        public static int N = 4;//记录城市数量
        public static int[] cost = new int[N + 2];//记录开销
        public static List<Integer> path;//记录路径
        public static int[][] G;//记录邻接矩阵,G[i][j]代表i城市到j城市之间的花销,第0行0列不存东西
    
        public static void main(String[] args) {
            // N=4;//四个城市
            G = new int[][]{{0, 0, 0, 0, 0}, {0, -1, 30, 6, 4}, {0, 30, -1, 5, 10}, {0, 6, 5, -1, 20}, {0, 40, 10, 20, -1}};
            findMinCostPath();
            System.out.println(cost[N + 1]);
            System.out.println(Arrays.toString(path.toArray()));
    
    
        }
    
        public static void findMinCostPath() {
            // Arrays.fill(path,-1);//初始化为-1;
    
            List<Integer> visited = new ArrayList<>();
            visited.add(1);
            for (int i = 0; i <= N + 1; i++) {
                //下标为N+1的格子存放总耗费
                if (i == 0 || i == 1)
                    cost[i] = 0;
                else
                    cost[i] = Integer.MAX_VALUE;
    
            }
            List<Integer> curPath = new ArrayList<>();
            curPath.add(1);
            dfs(1, visited);
    
    
        }
    
    
        public static void dfs(int cur, List<Integer> visited) {
            if (visited.size() == N && G[cur][1] != -1) {
                int curCost = cost[cur] + G[cur][1];
                if (curCost < cost[N + 1]) {
                    cost[N + 1] = curCost;
                    path = new ArrayList<>(visited);
                    path.add(1);
    
                } else
                    return;
            }
            for (int i = 1; i <= N; i++) {
                //有边相连且未走过且比之前找到的路径花费少
                int tempCost = cost[cur] + G[cur][i];
                if (G[cur][i] != -1 && !visited.contains(i) && tempCost < cost[N + 1]) {
                    visited.add(i);
    
                    cost[i] = tempCost;
                    dfs(i, visited);
                    visited.remove(visited.size() - 1);//回溯
    
                }
            }
    
    
        }
    
    }
    
    25
    [1, 4, 2, 3, 1]
    
    Process finished with exit code 0
    
    

    分支限界

    类似于回溯法,也是一种在问题的解空间树T上搜索问题解的算法。但在一般情况下,分支限界法与回溯法的求解目标不同。

    回溯法的求解目标是找出T中满足约束条件的所有解

    分支限界法的求解目标则是找出满足约束条件的一个解,或是在满足约束条件的解中找出使某一目标函数值达到极大或极小的解,即在某种意义下的最优解

    分支限界法是利用广度优先搜索的策略或者以最小耗费(最大效益)优先的方式搜索问题的解空间树,对于解空间树中的活节点只有一次机会成为拓展节点,活节点一旦成为扩展节点,那么将一次性产生其所有儿子节点。

    对于优先队列式的分支限界法,这些儿子节点中,不可行解或者一定不能成为最优解的儿子节点会被舍弃,其余儿子节点将会按照优先级依次存入一个活节点表(队列),此后会挑出活节点表优先级最高的节点作为下次扩展节点,重复此过程,直至找到问题的最优解。

    import java.util.*;
    
    /**
     * @author SJ
     * @date 2020/12/4
     */
    public class TSP2 {
        public static int N = 4;//记录城市数量
        public static int[][] G;//记录邻接矩阵,G[i][j]代表i城市到j城市之间的花销,第0行0列不存东西
    
    
        //每个城市节点有两个属性,1.curLength 代表从出发城市走到当前城市的路径  2.lowBound 代表确定这一步走该城市之后,下面所选路径和的最小值
        //可能达不到这个最小值,但是这是一个下界
        static class City {
            public int index;//城市编号
            public int curLength;
            public int lowBound;
            public List<Integer> path;//每个节点维护一个路径
    
            public City(int index, int curLength, int lowBound, List<Integer> path) {
                this.index = index;
                this.curLength = curLength;
                this.lowBound = lowBound;
                this.path = path;
                
            }
    
            public City() {
            }
    
            @Override
            public String toString() {
                return "City{" +
                        "index=" + index +
                        ", curLength=" + curLength +
                        ", lowBound=" + lowBound +
                        ", path=" + path +
                        '}';
            }
        }
    
        public static int findMin(int[] nums) {
            int min = Integer.MAX_VALUE;
            for (int i = 1; i < nums.length; i++) {
                if (nums[i] < min && nums[i] != -1)
                    min = nums[i];
            }
            return min;
        }
    
        public static void main(String[] args) {
            //总共4个城市,为了找到一条联通所有城市的回路,我们可以虚拟出第5个城市,就是第一个城市的复刻版
            G = new int[][]{{0, 0, 0, 0, 0, 0}, {0, -1, 30, 6, 4, -1}, {0, 30, -1, 5, 10, -1}, {0, 6, 5, -1, 20, -1}, {0, 4, 10, 20, -1, -1}, {0, -1, 30, 6, 4, -1}};
            findMinCostPath();
    
    
        }
    
    
        private static void findMinCostPath() {
            // initialize();
    
            //curLength和lowBound的和越小,优先级越高
            PriorityQueue<City> citiesQue = new PriorityQueue<>(new Comparator<City>() {
                @Override
                public int compare(City o1, City o2) {
                    return (o1.curLength + o1.lowBound) - (o2.curLength + o2.lowBound);
                }
            });
    
            City firstCity = new City(1, 0, 22, new ArrayList<>());
            firstCity.path.add(1);
            citiesQue.add(firstCity);
    
            City city = findPath(citiesQue);
    
            if (city != null) {
    
                System.out.println(city.curLength);
                System.out.println(Arrays.toString(city.path.toArray()));
            }
    
    
        }
    
    
        public static City findPath(PriorityQueue<City> citiesQue) {
            while (!citiesQue.isEmpty()) {
                City curCity = citiesQue.poll();
                int size = curCity.path.size();
                if (size == N + 1) {
                    curCity.path.set(size - 1, 1);
                    return curCity;
                }
    
    
                for (int i = 2; i <= N + 1; i++) {
                    int curCost = G[i][curCity.index];
                    if (curCost != -1 && !curCity.path.contains(i)) {//curCity和i号city路是通的,且i号城市未被访问过
                        int tempCurLength = curCity.curLength + curCost;//更新起始城市到当前城市的路径
                        int tempLowBound;
                        if (curCity.path.size() == 4)
                            tempLowBound = 0;
                        else
                            tempLowBound = curCity.lowBound - findMin(G[curCity.index]);
                        List<Integer> list = new ArrayList<>(curCity.path);
                        list.add(i);
                        citiesQue.add(new City(i, tempCurLength, tempLowBound, list));
    
                    }
                }
    
    
            }
            return null;
        }
    
    }
    
    "C:\Program Files\Java\jdk1.8.0_131\bin\java.exe" ...
    25
    [1, 4, 2, 3, 1]
    
    Process finished with exit code 0