九章算法 强化 Lecture 4 扫描线二分法双端队列

扫描线

例题 LintCode 391. Number of Airplanes in the Sky

Given an list interval, which are taking off and landing time of the flight. How many airplanes are there at most at the same time in the sky?

Example 1:
Input: [(1, 10), (2, 3), (5, 8), (4, 7)]
Output: 3
Explanation:
The first airplane takes off at 1 and lands at 10.
The second ariplane takes off at 2 and lands at 3.
The third ariplane takes off at 5 and lands at 8.
The forth ariplane takes off at 4 and lands at 7.
During 5 to 6, there are three airplanes in the sky.

Solution

每当遇到起飞的时间点时,空中飞机个数就加一,每当遇到降落的时间点时,空中飞机个数就减一。因此将所有的时间点进行排序,降落的和起飞的,起飞时间点标记为正的,降落时间点标记为负的。题目说明了当一个时间点同时有降落起飞时,先计算降落再计算起飞,于是自定义的comparator需要先比较时间再比较flag。
在这里插入图片描述

/**
 * Definition of Interval:
 * public classs Interval {
 *     int start, end;
 *     Interval(int start, int end) {
 *         this.start = start;
 *         this.end = end;
 *     }
 * }
 */
class Point {
    int val;
    int flag;
    
    public Point(int v, int f) {
        val = v;
        flag = f;
    }
}

public class Solution {
    /**
     * @param airplanes: An interval array
     * @return: Count of airplanes are in the sky.
     */
    public Comparator<Point> comparator = new Comparator<Point>() {
        public int compare(Point a, Point b) {
            if (a.val != b.val) {
                return a.val - b.val;
            } else {
                return a.flag - b. flag;
            }
        }
    };
     
    public int countOfAirplanes(List<Interval> airplanes) {
        // write your code here
        if (airplanes == null || airplanes.size() == 0) return 0;
        int n = airplanes.size();
        ArrayList<Point> points = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            points.add(new Point(airplanes.get(i).start, 1));
            points.add(new Point(airplanes.get(i).end, -1));
        }
        Collections.sort(points, comparator);
        
        int res = 0;
        int count = 0;
        for (Point p : points) {
            if (p.flag == 1) {
                count++;
            } else {
                count--;
            }
            res = Math.max(count, res);
        }
        return res;
    }
}

例题 253. Meeting Rooms II

Given an array of meeting time intervals consisting of start and end times [[s1,e1],[s2,e2],…] (si < ei), find the minimum number of conference rooms required.

Example 1:
Input: [[0, 30],[5, 10],[15, 20]]
Output: 2

Example 2:
Input: [[7,10],[2,4]]
Output: 1
NOTE: input types have been changed on April 15, 2019. Please reset to default code definition to get new method signature.

Solution

和上面一道题的解法完全一致

class Solution {
    class Event {
        int time;
        int flag;
        
        public Event(int t, int f) {
            time = t;
            flag = f;
        }
    }
    
    Comparator<Event> comparator = new Comparator<>() {
        public int compare(Event a, Event b) {
            if (a.time == b. time) {
                return a.flag - b.flag;
            } else {
                return a.time - b.time;
            }
        }
    };
    
    public int minMeetingRooms(int[][] intervals) {
        if (intervals == null || intervals.length == 0) {
            return 0;
        }
        
        ArrayList<Event> list = new ArrayList<>();
        for (int i = 0; i < intervals.length; i++) {
            list.add(new Event(intervals[i][0], 1));
            list.add(new Event(intervals[i][1], -1));
        }
        Collections.sort(list, comparator);
        
        int res = 0;
        int count = 0;
        for (Event e : list) {
            if (e.flag == 1) {
                count++;
            } else {
                count--;
            }
            res = Math.max(res, count);
        }
        return res;
    }
}

例题 LintCode 131 The Skyline Problem

Description
Given N buildings in a x-axis,each building is a rectangle and can be represented by a triple (start, end, height),where start is the start position on x-axis, end is the end position on x-axis and height is the height of the building. Buildings may overlap if you see them from far away,find the outline of them。

An outline can be represented by a triple, (start, end, height), where start is the start position on x-axis of the outline, end is the end position on x-axis and height is the height of the outline.
在这里插入图片描述

Solution

和上面的题思路类似,将input中给的每个矩形,拆成两个事件,一个是正事件(一栋楼出现),一个是负事件(一栋楼消失)。
在将所有的事件进行排序,需要注意的是,事件排序完毕后,进行扫描时,天际线的高度取决于此处存在的所有楼的高度的最大值,同时,经过一个正事件,高楼加一,经过一个负事件,高楼减一。于是采用PriorityQueue,因为PriorityQueue支持快速添加,删除,取最大值。定义一个PriorityQueue用来存储扫描线当前位置经过的所有高楼。每当PriorityQueue的最大值发生变化时就说明找到了一个拐点。
edge case需要特别考虑:起始和结尾。


例题 Time Intersection

Given two sets of time sections, find the intersection between the two sets of time sections.

Solution

可以将这两个sets看作两架飞机,不断的起飞和降落,intersection 就代表空中同时又两个飞机。intersection的起始点就是空中刚开始有两个飞机,intersection的结束点就是空中刚刚没有两架飞机。
在这里插入图片描述


二分法

例题 162. Find Peak Element

A peak element is an element that is greater than its neighbors.

Given an input array nums, where nums[i] ≠ nums[i+1], find a peak element and return its index.

The array may contain multiple peaks, in that case return the index to any one of the peaks is fine.

You may imagine that nums[-1] = nums[n] = -∞.

Example 1:
Input: nums = [1,2,3,1]
Output: 2
Explanation: 3 is a peak element and your function should return the index number 2.

Example 2:
Input: nums = [1,2,1,3,5,6,4]
Output: 1 or 5
Explanation: Your function can return either index number 1 where the peak element is 2, or index number 5 where the peak element is 6.

Solution

class Solution {
    public int findPeakElement(int[] nums) {
        if (nums == null || nums.length == 0) return -1;
        
        int n = nums.length;
        int l = 0;
        int r = n - 1;
        while (l < r) {
            int mid = l + (r - l) / 2;
            if (nums[mid] > nums[mid + 1]) {
                r = mid;
            } else {
                l = mid + 1;
            }
        }
        return l;
    }
}

二分答案

求满足条件的最大值最小值,
通过猜值判断是否满足题意,来搜索可能解

  1. 找到可行解范围
  2. 猜答案
  3. 检验条件
  4. 调整搜索范围

例题 69. sqrt(x)

Implement int sqrt(int x).
Compute and return the square root of x, where x is guaranteed to be a non-negative integer.
Since the return type is an integer, the decimal digits are truncated and only the integer part of the result is returned.

Example 1:
Input: 4
Output: 2

Example 2:
Input: 8
Output: 2
Explanation: The square root of 8 is 2.82842…, and since the decimal part is truncated, 2 is returned.

Solution

先确定一个上下界,然后根据上下界二分,避免overflow,采用 long型

class Solution {
    public int mySqrt(int x) {
        if (x < 2) return x;
        int l = 1;
        int r = x/2;
        while (l + 1 < r) {
            int mid = l + (r - l) / 2;
            long temp = (long)mid * mid;
            if (temp == x) {
                return mid;
            } else if (temp > x) {
                r = mid;
            } else {
                l = mid;
            }
        }
        long t = (long)r * r;
        if (t > x) return l;
        return r;
    }
}

例题 sqrt(x) ll

要求返回一个double值,精度满足 ∣ r e s 2 − x ∣ < = 1 e − 10 |res^2 - x| <= 1e-10 res2x<=1e10,同时输入值也可以是double型。

Solution

    public double mySqrt(double x) {
        double l = 0.0;
        double r = x;
        while (l < r) {
            double mid = l + (r - l) / 2;
            if (Math.abs(mid * mid - x) < Math.pow(10, -10)) {
                return mid;
            } else {
                if (mid * mid > x) {
                    r = mid;
                } else {
                    l = mid;
                }
            }
        }
        return -1;
    }

例题 LintCode 183 Wood Cut

Given n pieces of wood with length L[i] (integer array). Cut them into small pieces to guarantee you could have equal or more than k pieces with the same length. What is the longest length you can get from the n pieces of wood? Given L & k, return the maximum length of the small pieces.

Example 1
Input:
L = [232, 124, 456]
k = 7
Output: 114
Explanation: We can cut it into 7 pieces if any piece is 114cm long, however we can’t cut it into 7 pieces if any piece is 115cm long.

Solution

public class Solution {
    /**
     * @param L: Given n pieces of wood with length L[i]
     * @param k: An integer
     * @return: The maximum length of the small pieces
     */
    public int woodCut(int[] L, int k) {
        // write your code here
        if (L == null | L.length == 0) return 0;
        
        Arrays.sort(L);
        int l = 1;
        int r = L[L.length - 1];
        int res = 0;
        
        while (l <= r) {
            int mid = l + (r - l) / 2;
            int temp = 0;
            for (int length : L) {
                temp += length/ mid;
            }
            if (temp >= k) {
                res = Math.max(res, mid);
                l = mid + 1;
            } else {
                r = mid - 1;
            }
        }
        return res;
    }
}

例题 LintCode 437 Copy Books

Given n books and the i-th book has pages[i] pages. There are k persons to copy these books.
These books list in a row and each person can claim a continous range of books. For example, one copier can copy the books from i-th to j-th continously, but he can not copy the 1st book, 2nd book and 4th book (without 3rd book).
They start copying books at the same time and they all cost 1 minute to copy 1 page of a book. What’s the best strategy to assign books so that the slowest copier can finish at earliest time?
Return the shortest time that the slowest copier spends.

Example 1:
Input: pages = [3, 2, 4], k = 2
Output: 5
Explanation:
First person spends 5 minutes to copy book 1 and book 2.
Second person spends 4 minutes to copy book 3.

Example 2:
Input: pages = [3, 2, 4], k = 3
Output: 4
Explanation: Each person copies one of the books.

Solution

直接求十分困难,转换一下问题。把问题变成给定要求完成的时间,求需要最小的人数。采用贪心的策略,每个人都尽量一直抄到时间即将超过指定时间为止。

public class Solution {
    /**
     * @param pages: an array of integers
     * @param k: An integer
     * @return: an integer
     */
    public int copyBooks(int[] pages, int k) {
        // write your code here
        int temp = 0;
        int max = 0;
        for (int t : pages) {
            temp += t;
            max = Math.max(max, t);
        }
        int l = max;
        int r = temp;
        int res = Integer.MAX_VALUE ;
        
        while (l <= r) {
            int mid = l + (r - l) / 2;
            int tmp = numCal(pages, mid);
            if (tmp <= k) {
                res = Math.min(res, mid);
                r = mid - 1;
            } else {
                l = mid + 1;
            }
        }
        return res;
    }
    
    public int numCal(int[] pages, int t) {
        int res = 0;
        int temp = 0;
        for (int p : pages) {
            if (temp + p > t) {
                res++;
                temp = p;
            } else {
                temp += p;
            }
        }
        return res + 1;
    }
}

例题 287. Find the Duplicate Number

Given an array nums containing n + 1 integers where each integer is between 1 and n (inclusive), prove that at least one duplicate number must exist. Assume that there is only one duplicate number, find the duplicate one.

Example 1:
Input: [1,3,4,2,2]
Output: 2

Example 2:
Input: [3,1,3,4,2]
Output: 3

Note:
You must not modify the array (assume the array is read only).
You must use only constant, O(1) extra space.
Your runtime complexity should be less than O(n2).
There is only one duplicate number in the array, but it could be repeated more than once.

Solution 1

使用环检测算法求出duplicates。nums中存在duplicates代表用nums构成的sequence中有一个环。
环检测算法定义两个指针,一个快指针,一个慢指针。第一次两个指针相遇时说明找到了一个环,然后以sequence起点和相遇点定义两个慢指针,然后下一次相遇的时候就是环的起点也就是duplicates。

class Solution {
    public int findDuplicate(int[] nums) {
        // Find the intersection point of the two runners.
        int tortoise = nums[0];
        int hare = nums[0];
        do {
            tortoise = nums[tortoise];
            hare = nums[nums[hare]];
        } while (tortoise != hare);

        // Find the "entrance" to the cycle.
        int ptr1 = nums[0];
        int ptr2 = tortoise;
        while (ptr1 != ptr2) {
            ptr1 = nums[ptr1];
            ptr2 = nums[ptr2];
        }

        return ptr1;
    }
}

Solution 2

可以采用二分法,但是前提条件是nums排好序了,但是如果已经排好顺序了没有必要非要用二分法。直接判断相邻两个元素是否相等更直接。
在这里插入图片描述

例题 643. Maximum Average Subarray I

Given an array consisting of n integers, find the contiguous subarray of given length k that has the maximum average value. And you need to output the maximum average value.

Example 1:
Input: [1,12,-5,-6,50,3], k = 4
Output: 12.75
Explanation: Maximum average is (12-5-6+50)/4 = 51/4 = 12.75

Note:
1 <= k <= n <= 30,000.
Elements of the given array will be in the range [-10,000, 10,000].

Solution

最简单直接的解法,Brute Force,遍历计算所有的均值,找到最大的一个。
稍微简化一些的算法是借助前缀和数组,这样可以更快的计算subarray的和。

    public double findMaxAverage(int[] nums, int k) {
        int[] sum = new int[nums.length + 1];
        int temp = 0;
        sum[0] = 0;
        for (int i = 0; i < nums.length; i++) {
            temp += nums[i];
            sum[i + 1] = temp;
        }
        double res = -Double.MAX_VALUE;
        for (int i = 0; i < sum.length-k; i++) {
            res = Math.max(res, 1.0 * (sum[i+k] - sum[i]) / k);
        }
        return res;
    }

Solution

采用二分法的解法是将问题转化为,对于给定的平均值,是否存在长度为 k 的 subarray 满足条件。

例题 644. Maximum Average Subarray II

Given an array consisting of n integers, find the contiguous subarray whose length is greater than or equal to k that has the maximum average value. And you need to output the maximum average value.

Example:
Input: [1,12,-5,-6,50,3], k = 4
Output: 12.75

Explanation:
when length is 5, maximum average value is 10.8,
when length is 6, maximum average value is 9.16667.
Thus return 12.75.

Note:
1 <= k <= n <= 10,000.
Elements of the given array will be in range [-10,000, 10,000].
The answer with the calculation error less than 10-5 will be accepted.

Solution

二分法,将问题转化为,对于给定的平均值,是否存在长度大于等于 k 的 subarray 满足条件。
假设给定的平均值是 T,目标是求是否存在 ( n u m s [ l ] + n u m s [ l + 1 ] + . . . + n u m s [ r ] ) / ( r − l + 1 ) > = T (nums[l] + nums[l+1] +...+ nums[r]) / (r - l + 1) >= T (nums[l]+nums[l+1]+...+nums[r])/(rl+1)>=T,转换一下 ( n u m s [ l ] − T ) + ( n u m s [ l + 1 ] − T ) + . . . + ( n u m s [ r ] − T ) > = 0 (nums[l] - T) + (nums[l+1] - T) +...+ (nums[r] - T) >= 0 (nums[l]T)+(nums[l+1]T)+...+(nums[r]T)>=0,定义 b [ t ] = n u m s [ t ] − T b[t] = nums[t] - T b[t]=nums[t]T
通过 b [ t ] b[t] b[t]的前缀和数组,遍历右边界( r r r)的位置,此时为了使 b 数组从 l l l r r r 的和最大,由于右边界已经固定,求出左边界位置, 左边界位置就是从 0 到 i 位置,最小的前缀和。

class Solution {
    public double findMaxAverage(int[] nums, int k) {
        double l = nums[0];
        double r = nums[0];
        for (int t : nums) {
            l = Math.min(l, t);
            r = Math.max(r, t);
        }
        while (l + 1e-5 < r) {
            double mid = l + (r - l) / 2;
            if (canFind(nums, mid, k)) {
                l = mid;
            } else {
                r = mid;
            }
        }
        return l;
    }
    
    // canFind 用来判断 nums 数组中有没有长度至少为 k 的子数组的平均值大于等于 ave
    public boolean canFind(int[] nums, double ave, int k) {
    	// rightSum 是右边界的前缀和,
    	// leftSum 是左边界的前缀和
    	// minLeftSum 是到当前左边界位置最小的的的leftSum
    	// 左右边界之间的长度是 k 
        double rightSum = 0, leftSum = 0, minLeftSum = 0;
        for (int i = 0; i < k ; i++) {
            rightSum += nums[i] - ave;
        }
        
        for (int i = k; i <= nums.length; i++) {
            if (rightSum - minLeftSum >= 0) {
                return true;
            }
            if (i < nums.length) {
                rightSum += nums[i] - ave;
                leftSum += nums[i - k] - ave;
                minLeftSum = Math.min(leftSum, minLeftSum);
            }
        }
        return false;
    }
}

Deque

双端队列,两边都可以 push 和 pop
JAVA: Deque Interface, ArrayDeque<> class

例题 239. Sliding Window Maximum

Given an array nums, there is a sliding window of size k which is moving from the very left of the array to the very right. You can only see the k numbers in the window. Each time the sliding window moves right by one position. Return the max sliding window.

Follow up:
Could you solve it in linear time?

Example:
Input: nums = [1,3,-1,-3,5,3,6,7], and k = 3
Output: [3,3,5,5,6,7]

Explanation:
Window position Max


[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7

Constraints:

  1. 1 < = n u m s . l e n g t h < = 1 0 5 1 <= nums.length <= 10^5 1<=nums.length<=105
  2. − 1 0 4 < = n u m s [ i ] < = 1 0 4 -10^4 <= nums[i] <= 10^4 104<=nums[i]<=104
  3. 1 < = k < = n u m s . l e n g t h 1 <= k <= nums.length 1<=k<=nums.length

Solution 1

采用PriorityQueue来记录sliding window里面所有的数,时间复杂度为 O ( n l o g k ) O(nlogk) O(nlogk)

class Solution {
    Comparator<Integer> comparator = new Comparator<>() {
        public int compare(Integer a, Integer b) {
            return b - a;
        }
    };
    
    public int[] maxSlidingWindow(int[] nums, int k) {
        if (nums.length < k) {
            int res = nums[0];
            for (int i : nums) {
                res = Math.max(res, i);
            }
            
            return new int[] {res};
        }
        
        int[] result = new int[nums.length - k + 1];
        PriorityQueue<Integer> queue = new PriorityQueue<>(comparator);
        int count = 0;
        for (int i = 0; i < nums.length; i++) {
            if (queue.size() < k) {
                queue.add(nums[i]);
            } else {
                result[count++] = queue.peek();
                queue.remove(nums[i - k]);
                queue.add(nums[i]);
            }
        }
        result[count] = queue.peek();
        return result;
    }
}

Solution 2

采用Deque的做法,时间复杂度为 O ( n ) O(n) O(n)
观察发现,sliding window 里面的数字,由于窗口向右走,所以窗口里面的数字中左边数字小于右边数字的左边数字是不可能成为窗口里面最大值的,因为它小于右边的值,同时先于右边的值被提出窗口中,如下面图中被圈出来的红色数字。于是我们构造一个 Deque 用来存储 window 中的数字的坐标(index)。
当加入下一个数值时,判断 deque 后面的值是否小于等于这个数,如果小于等于,需要 pollLast() 出来直到 deque 长度为0或者最后一个数大于加入的数字,然后加入这个数。
当从 deque 中删除坐标最前面的数字时,由于 deque 保证顺序和原数组是相同的,坐标从左到右递增,只需要 peekFirst() 看 deque 最前面的index是不是需要删掉数字的index。
在这里插入图片描述

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        Deque<Integer> deque = new ArrayDeque<>();
        if (nums.length < k) {
            int res = nums[0];
            for (int i : nums) {
                res = Math.max(res, i);
            }
            return new int[] {res};
        }
        
        int[] result = new int[nums.length - k + 1];
        int count = 0;
        for (int i = 0; i < k - 1; i++) {
            inQueue(deque, i, nums);
        }
        
        for (int i = k - 1; i < nums.length; i++) {
            inQueue(deque, i, nums);
            result[count++] = nums[deque.peekFirst()];
            outQueue(deque, i - k + 1);
        }
        return result;
    }
    
    public void inQueue(Deque<Integer> deque, int index, int[] nums) {
        while (!deque.isEmpty() && nums[deque.peekLast()] <= nums[index]) {
            deque.pollLast();
        }
        deque.addLast(index);
    }
    
    public void outQueue(Deque<Integer> deque, int index) {
        if (deque.peekFirst() == index) {
            deque.pollFirst();
        }
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值