二分查找 - 二分答案

本文探讨了二分查找在寻找重复数、特殊数组的特征值以及山脉数组中查找目标值等问题中的应用。讲解了左查找和右查找的二段性,以及如何在不同场景中使用它们。此外,还介绍了如何利用二分查找解决最大值最小化和最小值最大化的最优化问题,以及在寻找第k小/大元素时的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

二分答案

二分答案区间,最优化问题转换为判定性问题

「二分」的本质是二段性,并非单调性。说白了就是答案在一个区间,二分区间,直到找到最优答案。

TreeSet 的方法:
E floor​(E e) 返回此 set 中小于或等于给定元素的最大元素,如果没有这样的元素,则 null 。
E ceiling​(E e) 返回此 set 中大于或等于给定元素的最小元素,如果没有这样的元素,则 null 。

左查找 返回 set 中 ≥ 给定目标的最小元素的最小索引 。
右查找 返回 set 中 > 给定目标的最小元素的索引 。

287.寻找重复数

重复数 target 只有一个,左右查找结果相同。通过条件转化成 mid 与 target 之间的关系。
左查找二段性:cnt 是数组中 ≤ mid 的个数,如果 cnt ≤ mid,那么说明 mid < target,即 target ∈ (mid, right]。反之 cnt > mid,说明 target ∈ [left, mid]。
右查找二段性:cnt 是数组中 < mid 的个数,如果 cnt < mid,那么说明 mid ≤ target ,即 target ∈ [mid, right]。反之 cnt ≥ mid,说明 target ∈ [left, mid)。

注意 x 的个数会补齐缺失的数后还剩一个。

class Solution {
    public int findDuplicate(int[] nums) {        
        int left = 1, right = nums.length; // [left, right)
        while (left < right) {
            int mid = left + right >> 1;
            int cnt = 0;
            // 右查找写法,左查找改为 ≤,最后返回 right
            for (int x : nums) if (x < mid) cnt++;            
            if (cnt < mid) left = mid + 1;
            else right = mid;
        }
        return right - 1;
    }
}

快慢指针

把的数组当作一个 链表 来看,数组的下标就是指向元素的指针,把数组的元素也看作指针。如 0 是指针,指向 nums[0],而 nums[0] 也是指针,指向 nums[nums[0]]。

链表中的环
例:[1,2,3,4,5,2]。按照上面的循环下去就会得到这样一个路径:1 2 3 4 5 [3 4 5] [3 4 5] . . . 这样就有了一个环。

第二次为什么 newSlow 和 slow 相遇在入口
slow 在环中走了 nc - m 步,再走 m 步,就回到入口。

class Solution {
    public int findDuplicate(int[] nums) {        
        int fast = 0, slow = 0;
        while (true){
            fast = nums[nums[fast]];
            slow = nums[slow];
            if (fast == slow) break;
        }
        int res = 0;
        while(slow != res){
            res = nums[res];
            slow = nums[slow];                 
        }
        return res;
    }
}

★1608.特殊数组的特征值

方法一:排序

根据特征值 x 的定义,x 一定是在 [1, n] 范围内的一个整数。
若 i 为特征值,那么 nums 中恰好有 i 个元素大于等于 i。说明 nums[n - i] 必须大于等于 i,并且 nums[n - i - 1](如果存在)必须小于 i。
[6, 6, 6, 6] 4

class Solution {
    public int specialArray(int[] nums) {
        int n = nums.length;
        Arrays.sort(nums);
        for (int i = 1; i <= n; i++)
            if (nums[n - i] >= i && (n == i || nums[n - i - 1] < i)) 
                return i;
        
        return -1;       
    }    
}

方法二:计数数组后缀和

class Solution {
    public int specialArray(int[] nums) {
        // int max = Arrays.stream(nums).max().getAsInt(); 
        int n = nums.length;
        int[] counts = new int[n + 1];
        for (int x : nums) 
            counts[Math.min(x, n)] += 1;

        int sum = 0;
        for (int i = n; i > 0; i--) {
            sum += counts[i];
            if (sum == i) return i;
        }
        return -1;
    }
}

方法三:二分答案

二段性:

class Solution {
    public int specialArray(int[] nums) {
        int left = 1, right = nums.length + 1;
        while (left < right) {
            int mid = left + right >> 1;
            int target = check(nums, mid); 
            if (mid < target) left = mid + 1;
            else {
                right = mid;
                if (target == mid) return mid;
            } 
        }

        return -1;
        // ★ 需要后处理        
        // return check(nums, left) == left ? left : -1;
    }
    
    int check(int[] nums, int mid) {
        int cnt = 0;       
        for (int x : nums) if (x >= mid) cnt++;
        return cnt;
    }
}

1095. 山脉数组中查找目标值

class Solution {
    MountainArray arr;
    public int findInMountainArray(int target, MountainArray mountainArr) {
        arr = mountainArr;
        // !!!峰值在开区间内 (0, n - 1),这样才不会越界。
        int left = 1, right = arr.length() - 1;
        while (left < right) {
            int mid = left + right >> 1;
            if (arr.get(mid) < arr.get(mid + 1)) left = mid + 1;
            else right = mid;
        }
        if (arr.get(left) == target) return left;
        int top = left; 
        int index = binarySearch(target, 0, top, 1);
        if (index != -1) return index;
        return binarySearch(target, top + 1, arr.length(), -1); 
    }

    public int binarySearch(int target, int left, int right, int sign) {
        target *= sign;
        while (left < right) {
            int mid = left + right >> 1;
            int cur = arr.get(mid) *sign;
            if (cur < target) left = mid + 1;
            else right = mid;
        }
        // [1,5,2] 0 会找到索引 3,后处理包括越界问题。
        return left < arr.length() && arr.get(left) * sign == target ? left : -1;
    }
}

一、最小值 左查找

★875.爱吃香蕉的珂珂

适合左查找,即 >= h 的最小值

class Solution {
    public int minEatingSpeed(int[] piles, int h) {
        int max = 0;
        for (int p : piles) if (max < p) max = p;        
        int left = 1, right = max; 
        while (left < right) {
            int mid = left + right >> 1;
            int hours = 0;
            for (int p : piles) 
                hours += (p - 1) / mid + 1; // 向上取整
            if (hours > h) left = mid + 1; // hours > h,吃的少了,吃不完。说明:mid < target
            else right = mid;
        }
        return right;
    }
}

1283. 使结果不超过阈值的最小除数

lass Solution {
    public int smallestDivisor(int[] nums, int threshold) {
        int l = 1, r = Arrays.stream(nums).max().getAsInt();
        while (l < r) {
            int mid = l + r >> 1;
            int total = 0;
            for (int x: nums) 
                total += (x - 1) / mid + 1;
           
            if (total > threshold) l = mid + 1; // mid < target
            else r = mid; 
        }
        return l;
    }
}

2187.完成旅途的最少时间

bisect_left 与 bisect_right 答案不同,可能存在一个时间区间,完成的任务相同。如:[3, 6] 在 3 - 5 完成的任务都是 1 ,所以本题适合左查找。

class Solution {
    public long minimumTime(int[] time, int totalTrips) {        
        long left = 1L, right = 1L * time[0] * totalTrips;
        while (left < right) {
            long mid = left + right >> 1;
            long cnt = 0L;
            for (int x : time) cnt += mid / x;            
            if (cnt < totalTrips) left = mid + 1;
            else right = mid;
        }
        return left;
    }
}

1870.准时到达的列车最小时速

由于时速必须为正整数,因此二分的下界为 1;对于二分的上界,考虑 hours 为两位小数,因此对于最后一段路程,最小的时限为 0.01,那么最高的时速要求即为 dist[i]/0.01 ≤ 107,同时为二分时速的上界。

class Solution {
    public int minSpeedOnTime(int[] dist, double hour) {
        if (dist.length > Math.ceil(hour)) return -1;
        int left = 1, right = 10000000;
        while (left < right) {
            int mid = left + (right - left) / 2;
            // 如果以 mid 速度不可达,那么就尝试增大速度,否则就需要减了
            if (check(dist, hour, mid)) left = mid + 1; 
            else right = mid;
        }
        return left;
    }

    boolean check(int[] dist, double hour, int speed) {
        double time = 0.0;
        int n = dist.length - 1;
        // 对除了最后一个站点以外的时间进行向上取整累加
        for (int i = 0; i < n; ++i) 
            // 除法的向上取整
            time += (dist[i] + speed - 1) / speed;

        // 加上最后一个站点所需的时间
        time += (double) dist[n] / speed;
        return time > hour;
    }
}

1011.在D天内送达包裹的能力

class Solution {
    public int shipWithinDays(int[] weights, int days) {
        int left = Arrays.stream(weights).max().getAsInt();
        int right = Arrays.stream(weights).sum();
        while (left < right) {
            int mid = left + right >> 1;
            int day = 1, sum = 0;
            for (int x : weights) {
                sum += x;
                if (sum > mid) {
                    day++;
                    sum = x;
                }
            }
            if (day > days) left = mid + 1;
            else right = mid;
        }
        return left;
    }
}

1482.制作m束花所需的最少天数

class Solution {
    public int minDays(int[] bloomDay, int m, int k) {
        long n = bloomDay.length;
        if (n < (long) m * k) return -1;
        int left = Arrays.stream(bloomDay).min().getAsInt(), 
            right = Arrays.stream(bloomDay).max().getAsInt();
        while (left < right) {
            int mid = left + right >> 1;
            int cnt = 0, mack = 0;
            for (int x : bloomDay) {
                if (x <= mid) {
                    cnt++;
                    if (cnt == k) {
                        mack++;
                        cnt = 0;
                        if (mack > m) break;
                    }
                } else cnt = 0;
            }
            if (mack < m) left = mid + 1;
            else right = mid;
        }
        return right;
    }
}

1648.销售价值减少的颜色球

class Solution {
    int MOD = (int)1e9 + 7;
    int[] inventory;
    public int maxProfit(int[] inventory, int orders) {    
        this.inventory = inventory;   
        int l = 0, r = Arrays.stream(inventory).max().getAsInt();
        long res = 0;
        while(l < r) { 
            int mid = l + (r - l) / 2;
            long count = 0;
            for(int x: inventory) {
                count += Math.max(x - mid, 0);
                // if (count > orders) break;
            }
                
            if (count > orders) l = mid + 1;
            // if (check(mid, orders)) l = mid + 1;
            else r = mid;
        }
        
        for (int x: inventory) { 
            if (x > r) {// 先 取到 r + 1
                res += (x + r + 1L) * (x - r) / 2; 
                orders -= x - r; // 取球
            }
        }
        // ★优先运算的乘法需要类型提升
        res += (long) r * orders; // 剩余补齐, 特殊处理下边界
        return (int)(res % MOD);
    }

    // 统计 > amount 的个数,不包含相等的个数。
    public boolean check(int amount, int orders){
        int count = 0;
        for(int x: inventory) {
            count += Math.max(x - amount, 0);
            // ★int 需要剪枝,long 不需要
            if (count > orders) return true;
        }
        return false;
    }
}

2594. 修车的最少时间

注意到 ranks 值域很小,在 t 相同时,相同 r 值的人修的车数是一样的,所以可以直接按 r 分组计算。

class Solution {
    public long repairCars(int[] ranks, int cars) {
        int minR = ranks[0];
        var cnt = new int[101];
        for (int r : ranks) {
            minR = Math.min(minR, r);
            ++cnt[r];
        }
        long left = 0, right = (long) minR * cars * cars;
        while (left < right) { 
            long mid = left + right >> 1, s = 0;
            for (int r = minR; r <= 100; ++r)
                s += (long) Math.sqrt(mid / r) * cnt[r];
            if (s < cars) left = mid + 1;
            else right = mid;
        }
        return right;
    }
}

1631.最小体力消耗路径

class Solution {
    int m, n;
    int[] DIRS = {1, 0, -1, 0, 1};
    int[][] heights;
    public int minimumEffortPath(int[][] heights) {
        this.heights = heights;
        m = heights.length;
        n = heights[0].length;

        int left = 0, right = 1000000;
        while (left < right) {            
            int mid = left + right >> 1;
            if (check(mid)) left = mid + 1; 
            else right = mid;
        }
        return left;
    }

    boolean check(int mid) {
        Deque<int[]> q = new LinkedList<>();
        q.offer(new int[]{0, 0});
        boolean[][] vis = new boolean[m][n];
        vis[0][0] = true;

        while (!q.isEmpty()) {
            int i = q.peek()[0], j = q.poll()[1];
            if (i == m - 1 && j == n - 1) return false;
            for (int k = 0; k < 4; k++) {
                int x = i + DIRS[k], y = j + DIRS[k + 1];
                if (inArea(x, y) && !vis[x][y] && Math.abs(heights[i][j] - heights[x][y]) <= mid) {  
                    q.offer(new int[]{x, y});
                    vis[x][y] = true;
                }
            }
        }
        return true;
    }

    private boolean inArea(int x, int y) {
        return x >= 0 && x < m && y >= 0 && y < n;
    }
}

778.水位上升的泳池中游泳

public class Solution {
    int n;
    int[] DIRS = {1, 0, -1, 0, 1};
    public int swimInWater(int[][] grid) {
        n = grid.length;
        int left = grid[0][0], right = n * n - 1;
        while (left < right) {            
            int mid = left + right >> 1; // left + right 不会溢出            
            // if (bfs(grid, mid)) right = mid;  // 8 ms
            boolean[][] vis = new boolean[n][n];
            if (dfs(grid, 0, 0, vis, mid)) right = mid; // 3 ms
            else left = mid + 1;
        }
        return left;
    }

    // 使用广度优先遍历得到从 (x, y) 开始向四个方向的所有小于等于 threshold 且与 (x, y) 连通的结点
    private boolean bfs(int[][] grid, int threshold) {
        Deque<int[]> q = new LinkedList<>();
        q.offer(new int[]{0, 0});
        boolean[][] vis = new boolean[n][n];
        vis[0][0] = true;

        while (!q.isEmpty()) {
            int i = q.peek()[0], j = q.poll()[1];
            if (i == n - 1 && j == n - 1) return true;
            for (int k = 0; k < 4; k++) {
                int x = i + DIRS[k], y = j + DIRS[k + 1];
                if (inArea(x, y) && !vis[x][y] && grid[x][y] <= threshold) {  
                    q.offer(new int[]{x, y});
                    vis[x][y] = true;
                }
            }
        }
        return false;
    }

    // 使用深度优先遍历得到从 (x, y) 开始向四个方向的所有小于等于 threshold 且与 (x, y) 连通的结点
    private boolean dfs(int[][] grid, int i, int j, boolean[][] vis, int threshold) {
        vis[i][j] = true;
        for(int k = 0; k < 4; k++){
            int x = i + DIRS[k], y = j + DIRS[k + 1];
            if (i == n - 1 && j == n - 1) return true;
            if (inArea(x, y) && !vis[x][y] && grid[x][y] <= threshold) {
                if (dfs(grid, x, y, vis, threshold)) return true;
            }
        }
        return false;
    }

    private boolean inArea(int x, int y) {
        return x >= 0 && x < n && y >= 0 && y < n;
    }
}

2258. 逃离火灾

二、最大值 右查找

右查找模板,改进 模板一。闭区间

开闭区间是针对数据源而言,查找区间为闭区间。

int rightFind(int[] arr, int target) {
    int n = arr.length, res = -1;
    int left = 0, right = n - 1; // ★★★ 闭区间 ★★★
    while (left <= right) { // ★ [i,j] 当 i == j 时仍然有效,[i, i) 则无效
        int mid = left + right >> 1;
        if (arr[mid] <= target) {
            // !!!找到就直接返回,实现的是 左查找。
            // if (arr[mid] == target) return mid;
            // 在含 = 处更新答案
            // 让子弹再飞一会儿,最后实现的 右查找
            // res = mid; // 也可以不记录,最后返回 right
            left = mid + 1;
        } else right = mid - 1;
    }
    // return res; // 找到的是 <= target 最大的数的下标
    // 需要后处理
    // return res > -1 && arr[res] == target ? res : -1;
    return right > 0 && arr[right] == target ? right : -1;
}

模板二 右查找插入位置 - 1。半开区间

int rightFind(int[] arr, int target) {
    int n = arr.length;
    int left = 0, right = n; // ★ [left, right)
    while (left < right) {
        int mid = left + right >> 1;
        if (arr[mid] <= target) left = mid + 1;
        else right = mid;
    }

    // 需要后处理
    // return left;
    return left < n && arr[left-1] == target ? left - 1: -1;
}

模板三 右查找目标位置。闭区间

int rightFind(int[] arr, int target) {
    int n = arr.length;
    int left = 0, right = n - 1; 
    while (left < right) { 
        int mid = 1 + left + right >> 1; // 向上取整
        if (arr[mid] <= target) left = mid;
        else right = mid - 1;
    }
    // 需要后处理
    return left >= 0 && arr[left] == target ? left : -1;
}

1898. 可移除字符的最大数目

class Solution {
    public int maximumRemovals(String s, String p, int[] removable) {
        char[] cs = s.toCharArray(), cp = p.toCharArray();
        int n = cs.length, m = cp.length;
        int left = 0, right = removable.length + 1; [left, right)
        while (left < right) {
            int mid = left + right >> 1;
            boolean[] state = new boolean[n];
            for (int i = 0; i < mid; i++)
                state[removable[i]] = true;
            int j = 0;
            for (int i = 0; i < n && j < m; i++) {
                if (cs[i] == cp[j] && !state[i]) j++;
            }
            if (j == m) left = mid + 1;
            else right = mid;
        }
        return right - 1;
    }
}

2226.每个小孩最多能分到多少糖果

每人能拿到 i 个糖果,就可以拿到闭区间 [0, i] 内任一糖果数量。
辅助函数 check(i) ,遍历 candies 数组,用 cnt 统计每一个元素 c 最多可以分给 ⌊c/i⌋ 个小孩的总和,返回 cnt ≥ k。

二分查找的上界,最多一堆糖果的数量或 sum(candies) // k;
二分查找的下界,为 0。

class Solution {
    public int maximumCandies(int[] candies, long k) {
        // [2, 5] 11 if sum < k 会出现了 /0 的情况, 右分割需要预处理。左分割不需要
        int left = 0, right = Arrays.stream(candies).max().getAsInt() + 1;
        // long sum = Arrays.stream(candies).toMapLong().sum();
        long sum = 0;
        for (int x : candies) sum += x;
        if (sum < k) return 0; // 左分割需要预处理
        while (left < right) {           
            int mid = left + right >> 1;
            long cnt = 0;
            for (int x : candies) cnt += x / mid;
            if (cnt >= k) left = mid + 1;
            else right = mid;
        }
        return right - 1;
    }
}       
class Solution {
    public int maximumCandies(int[] candies, long k) {
        // [2, 5] 11 if sum < k 会出现了 /0 的情况, 右分割需要预处理。左分割不需要
        int left = 0, right = Arrays.stream(candies).max().getAsInt();
        while (left < right) {           
            int mid = left + right + 1 >> 1; // 向上取整 左分割
            long cnt = 0;
            for (int x : candies) cnt += x / mid;
            // mid <= target, cnt(人数) 与 mid(糖果数) 成反比,人数越多,分的越少。
            if (cnt >= k) left = mid; 
            else right = mid - 1;
        }
        return right;
    }
}                  

★2141. 同时运行 N 台电脑的最长时间

n = 2, batteries = [3,3,3] 充分利用小电池,可分段供给不同时段的电脑用。

class Solution {
    public long maxRunTime(int n, int[] batteries) {
        long tot = 0L;
        for (int b : batteries) tot += b;      
        long l = 0L, r = tot / n + 1;
        while (l < r) {
            long mid = l + r  >> 1;
            long sum = 0L;
            for (int b : batteries) 
                sum += Math.min(b, mid);
            if (mid * n <= sum) l = mid + 1;
            else r = mid;
        }
        return r - 1;
    }
}
class Solution {
    public long maxRunTime(int n, int[] batteries) {
        Arrays.sort(batteries);
        var sum = 0L;
        for (var b : batteries) sum += b;
        for (var i = batteries.length - 1; ; --i) {
            if (batteries[i] <= sum / n) {
                return sum / n;
            }
            sum -= batteries[i];
            --n;
        }
    }
}

2861. 最大合金数

class Solution {
    public int maxNumberOfAlloys(int n, int k, int budget, List<List<Integer>> composition, List<Integer> stock, List<Integer> cost) {
        int res = 0;
        int max = stock.get(0) + budget + 1;
        for (var com : composition) {
            // 合金数
            int left = 0, right = max;
            while (left < right) { // 开区间写法
                int mid = left + right >> 1;
                boolean ok = true;
                long money = 0;
                for (int i = 0; i < n; ++i) {
                    long have = stock.get(i), need = 1L * com.get(i) * mid;
                    if (have < need) {
                        money += (need - have) * cost.get(i);
                        if (money > budget) {
                            ok = false;
                            break;
                        }
                    }
                }
                if (ok) left = mid + 1;
                else right = mid;
            }
            res = Math.max(res, left - 1);
        }
        return res;
    }
}

三、最小化最大值 (最大值 最小)

2064.分配给商店的最多商品的最小值

class Solution {
    public int minimizedMaximum(int n, int[] quantities) {
        int left = 1, right = Arrays.stream(quantities).max().getAsInt(); 
        while (left < right) {
            int mid = left + right >> 1;
            int cnt = 0;
            for (int x : quantities) 
                cnt += (x + mid - 1) / mid;
         
            if (cnt > n) left = mid + 1;
            else right = mid;
        }
        return right;
    }
}

1760.袋里最少数目的球

class Solution {
    public int minimumSize(int[] nums, int maxOperations) {
        // max min
        int l = 1, r = Arrays.stream(nums).max().getAsInt();
        while (l < r) {
            int m = l + r >> 1;
            if(f(nums, m) > maxOperations) l = m + 1; 
            else r = m;
        }
        return r;
        
    }
    int f(int[] nums, int k){
        int sum = 0;
        for(int x : nums)
            // 要少分一份
            sum += (x - 1) / k;
        
        return sum;
    }
}

410.分割数组的最大值

class Solution {
    public int splitArray(int[] nums, int k) {
        int left = Arrays.stream(nums).max().getAsInt();
        int right = Arrays.stream(nums).sum();

        while (left < right) {
            int mid = left + right >> 1;
            if (check(nums, k, mid)) left = mid + 1;
            else right = mid;
        }
        return left;
    }
    boolean check(int[] nums, int k, int mid) {
        int acc = 0, cnt = 1;
        for (int x : nums) {
            if (acc + x > mid) {
                acc = x;
                cnt++;
            } else acc += x;
        }
        return cnt > k;
    }
}

★2439. 最小化数组中的最大值

class Solution {
    public int minimizeArrayValue(int[] nums) {
        int left = 0, right = 0;
        for (int x : nums) if (x > right) right = x;
        while (left < right) {
            int mid = left + right >> 1;
            if (check(nums, mid)) left = mid + 1;
            else right = mid; 
        }
        return left;
    }
    boolean check(int[] nums, int mid) {
        Long dif = 0L; // 可以容纳后面转移过的最大数
        for (int x : nums) {
            if (x - mid > dif) return true; 
            dif += mid - x;
        }
        return false;
    }
}

遍历数组逐步求平均值 ave,当前值 x,if x < ave,then x 无法使前面的值更小或是使本身增大,也不会更新答案。
if x > ave,then x 一定可以将自己的值匀传递给给前面的数,得到最优解。

class Solution {
    public int minimizeArrayValue(int[] nums) {
        int n = nums.length,res = 0;
        Long sum = 0L;
        for (int i = 0; i < n; i++) {
            sum += nums[i];
            if (nums[i] > res)
            	res = (int) Math.max(res, (sum + i) / (i + 1));
        }
        return res;
    }
}

2513. 最小化两个数组中的最大值

lcm 为 d1 和 d2 的最小公倍数
能被 d2 整除但不能被 d1 整除的数,能在 arr1 中且不能在 arr2 中;
能被 d1 整除但不能被 d2 整除的数,能在 arr2 中且不能在 arr1 中;
既不能被 d1 整除也不能被 d2 整除的数,可以在 arr1 和 arr2 中。

每个数组去掉独享的,剩余的数字只能在共享中选择。

class Solution {
    int d1, d2, u1, u2;
    long lcm;
    public int minimizeSet(int divisor1, int divisor2, int uniqueCnt1, int uniqueCnt2) {
        d1 = divisor1; d2 = divisor2;
        u1 = uniqueCnt1; u2 = uniqueCnt2;
        lcm = (long)d1 * d2 / gcd(d1, d2);
        
        long l = 0L, r = (u1 + u2) * 2;
        while (l < r){
            long mid = l + r >> 1;
            if (check(mid)) l = mid + 1;  
            else r = mid;       
        }
        return (int)l;
    }
    
    boolean check(long x){
        long need1 = Math.max(0L, u1 - x / d2 + x / lcm);
        long need2 = Math.max(0L, u2 - x / d1 + x / lcm);
        long common = x - x / d1 - x / d2 + x / lcm;
        return common < need1 + need2;
    }

    int gcd(int a, int b){
        while (b != 0){
            int tmp = a % b;
            a = b;
            b = tmp;
        }
        return a;
    }
}

2560. 打家劫舍 IV

class Solution {
    public int minCapability(int[] nums, int k) {
        int left = 0, right = 0;
        for (int x : nums) if (x > right) right = x;       
        while (left < right) {
            int mid = left + right >> 1;
            int a = 0, b = 0;
            for (int x : nums)
                if (x > mid) a = b;
                else {
                    int c = b;
                    b = Math.max(b, a + 1);
                    a = c;
                }
            if (b < k) left = mid + 1;
            else right = mid;
        }
        return left;
    }
}

2616. 最小化数对的最大差值

二分数对中的最大差值 max。
由于下标和答案无关,可以先排序。为了让匹配的数对尽量多,应尽量选相邻的元素,这样更能满足要求。

class Solution {
    public int minimizeMax(int[] nums, int p) {
        Arrays.sort(nums);
        int n = nums.length;
        int left = 0, right = nums[n-1] - nums[0];
        while (left < right) {
            int mid = left + right >> 1;
            int cnt = 0;
            for (int i = 1; i < n; i++) {
                // 相邻两对只能选一对,符合条件就选。
                if (nums[i] - nums[i-1] <= mid) {
                    cnt++; i++;
                    if (cnt >= p) break;
                }
            }
            if (cnt < p) left = mid + 1;
            else right = mid;
        }
        return left;
    }
}

1723.完成所有工作的最短时间

第一个答案,最大值可能是 jobs 的和,所以弱化了 max >= ans 剪枝效果的。二分答案限定 max 的大小,

class Solution {
    int INF = 0x3f3f3f3f, k, n;
    int[] jobs, work;
    public int minimumTimeRequired(int[] jobs, int k) {
        this.k = k;
        this.jobs = jobs;
        Arrays.sort(jobs); // 排序优化:先分配大的,再用小的填充,
        n = jobs.length;
        work = new int[k];
        return dfs(n - 1, 0);
    }

    public int dfs(int i, int max){
        if (i == -1) return max;
        int x = jobs[i];
        int res = INF; // 也可以把 res 设成全局变量。
        for (int j = 0;  j < k; j++){

            // 剪枝:排除全部分配给了前面的工人,后面的空闲。 
            if (j > 0 && work[j] == 0 && work[j - 1] == 0) break;
            // 剪枝:和上一个工人选择得到的结果是一致的
            if (j > 0 && work[j] == work[j - 1]) continue;
            work[j] += x;
            int tmp = Math.max(max, work[j]);
            // 剪枝
            if (tmp < res) res = Math.min(res, dfs(i - 1, tmp));
            work[j] -= x;
        }
        return res;
    }
}

二分答案优化

class Solution {
    int[] jobs;
    int k, n;
    public int minimumTimeRequired(int[] jobs, int k) {
        Arrays.sort(jobs);
        this.jobs = jobs;
        this.k = k;
        n = jobs.length;
        int l = jobs[n-1], r = Arrays.stream(jobs).sum();
        while (l < r) {
            int mid = (l + r) >> 1;
            if (check(mid)) r = mid;
            else l = mid + 1;
        }
        return l;
    }

    boolean check(int maxMin) {
        // 先分配费时的
        return backtrack(new int[k], n - 1, maxMin);
    }

    boolean backtrack(int[] work, int i, int maxMin) {
        if (i == -1) return true;
        int x = jobs[i];
        for (int j = 0; j < k; ++j) {
            // 剪枝:排除全部分配给了前面的工人,后面的空闲。 
            if (j > 0 && work[j] == 0 && work[j-1] == 0) break; // 1 
            if (work[j] + x <= maxMin) {
                work[j] += x;
                if (backtrack(work, i - 1, maxMin)) return true;               
                work[j] -= x; 
            }
            // if (work[j] == 0 || work[j] + x == maxMin)  break; // 同 1
        }
        return false;
    }
}

四、最大化最小值(最小值 最大)

★1552. 两球之间的磁力

class Solution {
    public int maxDistance(int[] position, int m) {
        Arrays.sort(position);
        int n = position.length;
        int left = 0, right = (position[n-1] - position[0]) / (m - 1) + 1;
        while (left < right) {
            int mid = left + right >> 1;
            int cnt = 1, pre = position[0];
            for (int x : position) {
                if (x >= pre + mid) {
                    cnt++;
                    pre = x;
                }            
            }
            if (cnt >= m) left = mid + 1;
            else right = mid;
        }
        return right - 1;
    }
}

★2517. 礼盒的最大甜蜜度

「任意两种糖果价格绝对差的最小值」等价于「排序后,任意两种相邻糖果价格绝对差的最小值」。
可以二分答案:甜蜜度越大,可选择的糖果数量越小,有单调性。

f(d) 表示甜蜜度至少为 d 时,至多能选多少类糖果。
二分上界可以取 ⌊ m a x ( p r i c e ) − m i n ( p r i c e ) k − 1 ⌋ ⌊\frac{max(price)-min(price)}{k-1}⌋ k1max(price)min(price)。因为最小值不会超过平均值。(平均值指选了 price 最小最大以及中间的一些糖果,相邻糖果差值的平均值。)

二分答案 d:

  • 如果 f(d) ≥ k,说明答案至少为 d。
  • 如果 f(d) < k,说明答案至多为 d−1。

二分结束后,设答案为 x,那么 f(x) ≥ k 且 f(x+1) < k。

对 price 从小到大排序,二分答案 d。从 x 开始,下一个可以选的数是第一个 ≥ x + d 的数。间隔选择数,最小间隔 ≥ d。如果可以选的数 < k,说明 d 取大了,否则说明 d 取小了。

请注意,二分的区间的定义是:尚未确定 f(d) 与 k 的大小关系的 d 的值组成的集合(范围)。在区间左侧外面的 d 都是 f(d) ≥ k 的,在区间右侧外面的 d 都是 f(d) < k 的。

class Solution {
    public int maximumTastiness(int[] price, int k) {
        Arrays.sort(price);
        int n = price.length;
        // k - 1 段,差尽量均匀
        // 右查找
        // 最小值不会超过平均值。(平均值指选了 price 最小最大以及中间的一些糖果,相邻糖果差值的平均值。)
        int left = 0, right = (price[n-1] - price[0]) / (k - 1) + 1;
        // while (left < right) { 
        //     int mid = left + right >> 1;
        //     if (f(price, mid) > k) left = mid + 1; 
        //     else right = mid;
        // }
        // return left - 1; // left 右邻居

        // while (left < right) { 
        //     int mid = left + right + 1 >> 1;
        //     if (f(price, mid) >= k) left = mid; 
        //     else right = mid - 1;
        // }
        // return left;

        while (left + 1 < right) { 
            int mid = left + right >> 1;
            if (f(price, mid) >= k) left = mid; 
            else right = mid;
        }
        return left;
    }

    private int f(int[] price, int d) {
        int cnt = 1, pre = price[0];
        for (int p : price) {
            if (p >= pre + d) {
                cnt++;
                pre = p;
            }
        }
        return cnt;
    }
}

3281. 范围内整数的最大得分

class Solution {
    public int maxPossibleScore(int[] start, int d) {
        Arrays.sort(start);
        int n = start.length;
        int left = 0;
        int right = (start[n - 1] + d - start[0]) / (n - 1) + 1;
        while (left < right) {
            int mid = left + right >>> 1;
            if (check(start, d, mid)) left = mid + 1;
            else right = mid;
        }
        return left - 1;
    }

    private boolean check(int[] start, int d, int score) {
        long x = start[0]; // 贪心
        for (int i = 1; i < start.length; i++) {
            x += score;
            if (x > start[i] + d) return false;
            if (x < start[i]) x = start[i];
        }
        return true;
    }
}

2528. 最大化城市的最小供电站数目

二分答案 + 前缀和 + 差分数组 + 贪心

1、二分答案 minPower,从左到右遍历 stations,如果 stations[i] 电量不足 minPower,那么需要建供电站来补足。
2、由于 i 左侧的不需要补足,所以 贪心 地在 min(i+r, n−1) 处建是最合适的,恰好让 i 在覆盖范围的边界上。
3、设需要建 m 个供电站,那么需要把 [i, min(i+2r, n−1)] 范围内的电量都增加 m,用 差分数组 来更新是最简单的。
4、最后判断修建的供电站是否超过 k,如果超过说明 minPower 偏大,否则说明偏小。

注:其实前缀和也不需要,可以改为长为 2r + 1 的滑动窗口,但这样写有点麻烦。

class Solution {
    int r, k, n;
    long[] power;
    public long maxPower(int[] stations, int r, int k) {
        n = stations.length;
        this.r = r; this.k = k;
        long[] acc = new long[n + 1]; // 前缀和
        for (int i = 0; i < n; ++i)
            acc[i + 1] = acc[i] + stations[i];
        long min = Long.MAX_VALUE;
        power = new long[n]; // 电量
        for (int i = 0; i < n; ++i) {
            power[i] = acc[Math.min(i + r + 1, n)] - acc[Math.max(i - r, 0)];
            min = Math.min(min, power[i]);
        }

        long left = min, right = min + k + 1; 
        while (left < right) {
            long mid = left + (right - left) / 2;
            if (check(mid) <= k) left = mid + 1;
            else right = mid;
        }
        return left - 1;
        //return check(left) ? left : right;
    }

    private long check(long minPower) {
        long[] dif = new long[n + 1]; // 差分数组
        long acc = 0, need = 0;
        for (int i = 0; i < n; ++i) {
            acc += dif[i]; // 累加差分值,左侧已经补充的电站数。
            long m = minPower - power[i] - acc;
            // 如果 i 不够贪心地建在 i + r 处,左侧会覆盖到 i,即 i 正好为左边界。
            if (m > 0) { // 需要 m 个供电站
                need += m;
               // if (need > k) return false; // 提前退出这样快一些
                acc += m; // 差分更新,相当于差分数组当前加 m
                if (i + r * 2 + 1 < n) dif[i + r * 2 + 1] -= m; // 差分更新
            }
        }
        return need;
    }
}

五、第 k 小/大(部分题目还可以用堆解决)

668.乘法表中第k小的数

在 [1, m * n] 范围内,二分查找过程中取得的值不一定在乘法表中。

比如 3 * 3 乘法表:1 2 3 2 4 6 3 6 9 但数字 5、7、8 都不在乘法表中,假设查找 k = 8 的值,你会发现 x = 6, 7, 8 时,count = k = 8 都成立,所以我们不能在 count = k 时直接返回结果,而应该继续进行二分查找,最终返回能满足 count = k 的最小 x 值,比如对于 k = 8,应该返回 6。

第 i 行中大于 x 的个数 min(x / i, n),其中(i 是行号,n 是矩阵的列数)x / i 代表的是如果 x 也在第 i 行,
它存在的列数,所以只要取最小值就是第 i 行不大于 x 的个数。

class Solution {
    public int findKthNumber(int m, int n, int k) {
        int l = 1, r = m * n;
        while (l < r) {
            int mid = l + r >> 1;
            if (check(m, n, mid) < k) l = mid + 1;
            else r = mid;
        } 
        return r;
    }
    int check(int m, int n, int x) {
        // int row = x / n; 
        // int cnt = row * n; // 先取整行的
        // for (int i = row + 1; i <= m; i++)
        //     cnt += x / i; // 不是整行的再按列数取
        
        int cnt = 0;
        for (int i = 1; i <= m; i++)
            cnt += Math.min(n, x / i);
        return cnt;
    }
}

719.找出第K小的数对距离

排序,最小距离为 0,最大距离为 nums[n - 1] - nums[0]。
距离 dis 越大,满足条件的数对就越多, 二分查找,找出合适的距离 dis。

class Solution {
    public int smallestDistancePair(int[] nums, int k) {
        Arrays.sort(nums);
        int left = 0, right = nums[nums.length-1] - nums[0];
        while (left < right) {
            int mid = left + right >> 1;
            if (check(nums, mid) < k) left = mid + 1;
            else right = mid;            
        }
        return left;
    }

    int check(int[] nums, int dis) {
        int cnt = 0, left = 0;
        for (int i = 0; i < nums.length; i++) {
        	// !!!
            while (nums[i] - nums[left] > dis) left++;
            cnt += i - left;
        }
        return cnt;
    }  
}

3134. 找出唯一性数组的中位数

class Solution {
    int[] nums;
    long k;
    public int medianOfUniquenessArray(int[] nums) {
        this.nums = nums;
        int n = nums.length;
        k = (n * (n + 1L) >> 1) + 1 >> 1;
        int left = 1, right = n;
        while (left < right) {
            int mid = left + right >> 1;
            if (!check(mid)) left = mid + 1;
            else right = mid;
        }
        return left;
    }
    
    boolean check(int upper) {
        int n = nums.length, left = 0;
        long count = 0;
        Map<Integer, Integer> cnt = new HashMap();
        // 统计 不同元素个数 <= upper 的子数组个数
        for (int i = 0; i < n; i++) {
            cnt.merge(nums[i], 1, Integer::sum);
            // 滑动窗口
            while (cnt.size() > upper) {
                int y = nums[left++];
                if (cnt.merge(y, -1, Integer::sum) == 0) cnt.remove(y);
            }
            count += i - left + 1; // 子数组个数,每个子数组贡献一个数。
            if (count >= k) return true;
        }
        return false;
    }
}

786. 第 K 个最小的素数分数

1439. 有序矩阵中的第 k 个最小数组和

2040. 两个有序数组的第 K 小乘积

2386. 找出数组的第 K 大和

373. 查找和最小的 K 对数字

378. 有序矩阵中第 K 小的元素

其它

754.到达终点数字

k 为移动次数,总步数 p = k * (k + 1) // 2; 找到满足 target <= p 的第一个 k,如果 x = p - target 是偶数,把第 x // 2 次移动方向取反,总步数会减 x;x 是奇数,需要再移动一次或二次凑成偶数,即次数为 k + 1 + k%2。

class Solution:
    def reachNumber(self, target: int) -> int:
        t = abs(target)
        # 方法一:二分答案
        i, j = 1, t - 1
        while i < j:
            mid = i + j >> 1           
            x = mid * (mid+1) // 2
            if x < t: i = mid + 1
            else: j = mid

        # i = bisect_left(range(t), t, key = lambda x: x * (x + 1) // 2)
        return i + i % 2 + 1 if (i * (i + 1) // 2 - t) % 2 else i

        # 方法二:直接计算
        i = p = 0
        while p < t or (p - t) % 2:
            i += 1
            p = p + i
        return i

878. 第 N 个神奇数字

class Solution:
    def nthMagicalNumber(self, n: int, a: int, b: int) -> int:
        # 容斥原理 + 二分(左查找)
        lcm = math.lcm(a, b)
        i, j = 0, min(a, b) * n  
        while i < j:  
            mid = i + j >> 1
            if mid // a + mid // b - mid // lcm < n: i = mid + 1
            else: j = mid 
        return i % (10 ** 9 + 7)

        lcm = math.lcm(a, b)
        return bisect_left(range(min(a, b) * n), n, key=lambda x: x // a + x // b - x // lcm) % (10 ** 9 + 7)

1201. 丑数 III

在这里插入图片描述
三量容斥

class Solution:
    def nthUglyNumber(self, n: int, a: int, b: int, c: int) -> int:
 
        # 最小公倍数
        # def lcm(a, b):
        #     return a * b / gcd(a, b)
        # def gcd(a, b):
        #     return a if b == 0 else gcd(b, a % b)

        x, y, z, p = lcm(a, b),lcm(a, c), lcm(b, c), lcm(a, b, c)
        def cnt(m):
            return m // a + m // b + m // c - m // x - m // y - m // z + m // p
        i, j = 1, min(a, b, c) * n 
        while i < j:
            mid = i + j >> 1
            if cnt(mid) < n: i = mid + 1
            else: j = mid
        return i

1237.找出给定方程的正整数解

class Solution:
    def findSolution(self, customfunction: 'CustomFunction', z: int) -> List[List[int]]:
        ans = []
        # for i in range(1, 1001):            
            # l, r = 1, 1000
            # while l < r:
            #     mid = l + r >> 1
            #     x = customfunction.f(i, mid)
            #     if x < z: l = mid + 1
            #     else: r = mid
            
            # r = bisect_left(range(1000), z, key=lambda x:customfunction.f(i, x))
            # if r > 0 and customfunction.f(i, r) == z: ans.append([i, r])

        i, j = 1, 1000
        while i < 1001 and j > 0:
            x = customfunction.f(i, j) 
            if x > z: j -= 1
            elif x < z: i += 1
            else: 
                ans.append([i, j])    
                i += 1
                j -= 1
        return ans

718.最长重复子数组

class Solution:
    def findLength(self, nums1: List[int], nums2: List[int]) -> int:
        n, m = len(nums2), len(nums1)
        #dp = [[0] * (n + 1) for _ in range(m + 1)]
        dp = [0] * (n + 1)
        ans = 0
        for i in range(m):
            # # 逆序遍历可降维,正序需要滚动数组
            # for j in range(n-1,-1,-1):
            #     if nums1[i] == nums2[j]:
            #         dp[j+1] = dp[j] + 1
            #         ans = max(ans, dp[j+1])
            #     else:dp[j+1] = 0
            tmp = [0] * (n + 1) # 滚动数组
            for j in range(n):
                if nums1[i] == nums2[j]:
                    #dp[i+1][j+1] = dp[i][j] + 1
                    #ans = max(ans, dp[i+1][j+1])
                    tmp[j+1] = dp[j] + 1
                    ans = max(ans, tmp[j+1])
            dp = tmp

        return ans

        # 方法:二分查找 + 哈希
        base, mod = 113, 10**9 + 9

        def check(length: int) -> bool:
            hashA = 0
            for i in range(length):
                hashA = (hashA * base + nums1[i]) % mod
            bucketA = {hashA}
            mult = pow(base, length - 1, mod)
            for i in range(length, len(nums1)):
                hashA = ((hashA - nums1[i - length] * mult) * base + nums1[i]) % mod
                bucketA.add(hashA)
            
            hashB = 0
            for i in range(length):
                hashB = (hashB * base + nums2[i]) % mod
            if hashB in bucketA:
                return True
            for i in range(length, len(nums2)):
                hashB = ((hashB - nums2[i - length] * mult) * base + nums2[i]) % mod
                if hashB in bucketA:
                    return True

            return False

        i, j, ans = 0, min(len(nums1), len(nums2)), 0
        while i <= j:
            mid = (i + j) // 2
            if check(mid):
                # ans = mid
                i = mid + 1
            else:
                j = mid - 1
        return j

1044.最长重复子串

1838.最高频元素的频数

class Solution:
    def maxFrequency(self, nums: List[int], k: int) -> int:
        # 方法一:前缀和 + 二分 纯为了二分  
        def check(w): # 滑动窗口
            for i in range(n - w + 1):
                j = i + w - 1
                tmp = nums[j] * w
                presum = acc[j+1] - acc[i] + k           
                if tmp <= presum: return True        
            return False
        
        nums.sort()     
        acc = list(accumulate(nums, initial=0))
        n = len(nums)
        i, j = 1, n # 最大可能频数范围
        while i < j:
            mid = i + j + 1 >> 1
            if check(mid): i = mid
            else: j = mid - 1

        # while i <= j:
        #     mid = i + j >> 1
        #     if check(mid): i = mid + 1
        #     else: j = mid - 1        
        return j 
   
        # 方法二:前缀和+滑动窗口 单边扩张
        nums.sort()
        acc, left = k, 0 # 执行最多 k 次操作
        for i, x in enumerate(nums):
            acc += x
            # 不够补齐,收缩左边,窗口平移,否则向右扩展;大于时可以减少 k 的次数,保持相等。
            if acc < (i - left + 1) * x:
                acc -= nums[left]
                left += 1
        return len(nums) - left

793. 阶乘函数后 K 个零

计算出 x! 的末尾 0 的个数,0 的个数由阶乘中可分解出多少个 10 决定,10 = 5 * 2,因此问题转化为 x! 的阶乘中有多少个因子 5 和多少个因子 2,并取他们的最小值,即有多少对儿5 * 2,又因为 5 的个数肯定小于等于 2 的个数,所以我们只需要计算出可以分解多少个 5 即可。

每个 k 对应 5 个连续整数,x∈[5,9]⇔k=1,k 与区间上界最多是 9 倍关系,最少在 1 倍以上,二分查找区间 [k,9k],注意,有些 k 对应的结果不存在,比如 25 = 52 k = 5 被跳过,同理 125 = 53 一样使得 29 和 30 被跳过,所以如果结果不存在则返回 0 。

class Solution:
    def preimageSizeFZF(self, k: int) -> int:
        # 172
        def zeta(n: int) -> int:
            res = 0
            while n:
                n //= 5 
                res += n
            return res

        def nx(k: int) -> int:
            #return bisect_left(range(5 * k), k, key=zeta)
            l, r = 4 * k, 5 * k
            while l < r:
                mid = l + r >> 1
                if zeta(mid) < k: l = mid + 1
                else: r = mid
            return r

        return nx(k + 1) - nx(k)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值