二分查找算法练习和二分答案法算法联系

想系统学习算法的可以三连关注,后续更新大厂所有类型的算法

10.二分查找

二分查找 总结,

153. 寻找旋转排序数组中的最小值

已解答

中等

相关标签

相关企业

提示

已知一个长度为 n 的数组,预先按照升序排列,经由 1n旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:

  • 若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]

  • 若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]

注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]]

给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素

你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。

示例 1:

输入:nums = [3,4,5,1,2]
输出:1
解释:原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。
public static int findMin(int[] nums) {
    int l  = 0;
    int n = nums.length;
    int r = n-1;
    int min = Integer.MAX_VALUE;
    while(l<=r){
        int m = l+((r-l)>>>1);
        if(nums[m]>=nums[r]){
            l = m+1;
        }else{
            r = m-1;
        }
    }
    return nums[r];
}

34. 在排序数组中查找元素的第一个和最后一个位置

已解答

中等

相关标签

相关企业

给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]

你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。

示例 1:

输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]

public static int[] searchRange(int[] nums, int target) {
    int l = 0;
    int r = nums.length-1;
    int left  =  -1;
    int right = -1;
    while(l<=r){
        int mid = (l+r)>>>1;
        if(nums[mid]<target){
            l=mid+1;
        }else {
            left = mid;
            r = mid-1;
        }
    }
    l = 0;
    r = nums.length-1;
    while(l<=r){
        int mid = (l+r)>>>1;
        if(nums[mid]<=target){
            right = mid;
            l=mid+1;
        }else {
            r = mid-1;
        }
    };
    if (left>right||left ==-1||nums[left]!=target){
        return new int[]{-1,-1};
    }
    return new int[]{left,right};
}

11.二分答案法

前置知识:讲解005-对数器、讲解006-基本二分搜索、讲解042-进一步了解对数器

二分答案法

1)估计 最终答案可能的范围 是什么

2)分析 问题的答案 和 给定条件 之间的 单调性,大部分时候只需要用到 自然智慧

3)建立一个f函数,当答案固定的情况下,判断 给定的条件是否达标

4)在 最终答案可能的范围上不断二分搜索,每次用f函数判断,直到二分结束,找到最合适的答案

核心点:分析单调性、建立f函数

注意: 这个技巧常用且重要,一定要引起重视,非常的美、精妙! 后续还会使用到

875. 爱吃香蕉的珂珂

中等

珂珂喜欢吃香蕉。这里有 n 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,将在 h 小时后回来。

珂珂可以决定她吃香蕉的速度 k (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 k 根。如果这堆香蕉少于 k 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉。

珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。

返回她可以在 h 小时内吃掉所有香蕉的最小速度 kk 为整数)。

示例 1:

输入:piles = [3,6,7,11], h = 8
输出:4

public static int minEatingSpeed(int[] piles, int h) {
    int r = 0;
    int l = 1;
    for(int i:piles){
        r = Math.max(r,i);
    }
    int ans = r;
    while(l<=r){
        int m = l+((r-l)>>1);
        if(process(piles,m,h)){
            ans = m;
            r = m-1;
        }else{
            l = m+1;
        }
    }
    return ans;
}
​
public static boolean process(int []piles,int speed,int h){
    int ans = 0;
    for(int i = 0;i<piles.length;i++){
        ans += (piles[i]+speed-1)/speed;
        if (ans>h){
            return false;
        }
    }
    if(ans<=h){
        return true;
    }else{
        return false;
    }
}

机器人跳跃问题牛客题霸牛客网 (nowcoder.com)

描述

机器人正在玩一个古老的基于DOS的游戏。游戏中有N+1座建筑——从0到N编号,从左到右排列。编号为0的建筑高度为0个单位,编号为i的建筑的高度为H(i)个单位。

起初, 机器人在编号为0的建筑处。每一步,它跳到下一个(右边)建筑。假设机器人在第k个建筑,且它现在的能量值是E, 下一步它将跳到第个k+1建筑。它将会得到或者失去正比于与H(k+1)与E之差的能量。如果 H(k+1) > E 那么机器人就失去 H(k+1) - E 的能量值,否则它将得到 E - H(k+1) 的能量值。

游戏目标是到达第个N建筑,在这个过程中,能量值不能为负数个单位。现在的问题是机器人以多少能量值开始游戏,才可以保证成功完成游戏?

输入描述:

第一行输入,表示一共有 N 组数据.

第二个是 N 个空格分隔的整数,H1, H2, H3, ..., Hn 代表建筑物的高度

输出描述:

输出一个单独的数表示完成游戏所需的最少单位的初始能量

示例1

输入:

5
3 4 3 2 4

输出:

4

import java.util.Scanner;
import java.io.*;
// 注意类名必须为 Main, 不要有任何 package xxx 信息
public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader bf = new BufferedReader((new InputStreamReader(System.in)));
        // 注意 hasNext 和 hasNextLine 的区别
        String [] s = bf.readLine().split(" ");
        int n = Integer.parseInt(s[0]);
        int [] arr = new int[n];
        s = bf.readLine().split(" ");
        int min = Integer.MAX_VALUE;
        int max = Integer.MIN_VALUE;
        for (int i = 0; i < n; i++) {
            int num = Integer.parseInt(s[i]);
            arr[i] = num;
            min = Math.min(num, min);
            max = Math.max(max, num);
        }
        int ans = -1;
        int r = max;
        int l = min;
        while (l <= r) {
            int mid = l + ((r - l) >> 1);
            if (process(arr, mid, max)) {
                ans = mid;
                r = r - 1;
            } else {
                l = l + 1;
            }
        }
        PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
        out.print(ans);
        out.close();
        bf.close();
    }
​
​
​
    public static boolean process(int [] arr, int pouwer, int max) {
        for (int i = 0; i < arr.length; i++) {
            if (arr[i] > pouwer) {
                pouwer -= arr[i] - pouwer;
            } else {
                pouwer += pouwer - arr[i];
            }
            if (pouwer > max) {
                return true;
            }
            if (pouwer < 0) {
                return false;
            }
        }
        return true;
    }
}

分割数组的最大值(画匠问题) 给定一个非负整数数组 nums 和一个整数 m 你需要将这个数组分成 m 个非空的连续子数组。 设计一个算法使得这 m 个子数组各自和的最大值最小。

410. 分割数组的最大值

已解答

困难

给定一个非负整数数组 nums 和一个整数 k ,你需要将这个数组分成 k 个非空的连续子数组。

设计一个算法使得这 k 个子数组各自和的最大值最小。

这题也是分析很容易找到答案的给定条件k的单调性,k越大,答案就越小,那我们找到可能出现的最大答案max,以及最小的min,使用二分查找再合适不过了,关键就是找到答案和给定条件的单调性。

示例 1:

输入:nums = [7,2,5,10,8], k = 2
输出:18
解释:
一共有四种方法将 nums 分割为 2 个子数组。 
其中最好的方式是将其分为 [7,2,5] 和 [10,8] 。
因为此时这两个子数组各自的和的最大值为18,在所有情况中最小。
public static int splitArray(int[] nums, int k) {
        int max = 0;
        int min = 0;
        for(int i = 0;i<nums.length;i++){
            max+=nums[i];
            min = Math.max(nums[i],min);
        }
        int l = min;//取min是为了缩小范围,其次防止一个数都装不下
        int r = max;
        int ans = max;
        while(l<=r){
            int mid = l + ((r-l)>>>1);
            if(f(mid,nums)<=k){
                ans = mid;
                r = mid - 1;
            }else{
                l = mid + 1;
            }
        }
        return ans;
    }
​
    public static int f(int mid,int [] nums){
        int ans = 0;
        for(int l = 0,r = 0;r<nums.length;){
            int sum = 0;
            sum = nums[r++];
            while (r<nums.length&&(sum+nums[r])<=mid){
                sum +=nums[r++];
            }
            l = r;
            ans++;
        }
        return ans;
    }

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

已解答

困难

提示

数对 (a,b) 由整数 ab 组成,其数对距离定义为 ab 的绝对差值。

给你一个整数数组 nums 和一个整数 k ,数对由 nums[i]nums[j] 组成且满足 0 <= i < j < nums.length 。返回 所有数对距离中k 小的数对距离。

分析题目很轻易可以得到答案的范围介于0 和 nums[n-1]-nums[0];之间,

题目又要求第k小的距离,发现 答案范围越大,对数越多,那我们就找到答案的对数之间的单调性了,就很合适使用二分查找发,关键是找到单调性

示例 1:

输入:nums = [1,3,1], k = 1
输出:0
解释:数对和对应的距离如下:
(1,3) -> 2
(1,1) -> 0
(3,1) -> 2
距离第 1 小的数对是 (1,1) ,距离为 0 。
public static int smallestDistancePair(int[] nums, int k) {
    int n = nums.length;
    Arrays.sort(nums);
    int max = nums[n-1]-nums[0];
    int l = 0;
    int r = max;
    int ans = 0;
    while (l<=r){
        int mid = l + ((r-l)>>1);
        int result = f(mid,nums);
        if (result>=k){
            ans = mid;
            r = mid-1;
        }else {
            l = mid+1;
        }
    }
    return ans;
}
​
​
//使用滑动窗口计算满足条件的数对个数更快
private static int f(int mid, int[] nums) {
    int k = 0;
    for (int l = 0 ,r = 0;l<nums.length;l++){
        while (r+1<nums.length&&nums[r+1]-nums[l]<=mid) {
            r++;
        }
        k+=r-l;
    }
    return k;
}

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

已解答

困难

相关标签

相关企业

提示

你有 n 台电脑。给你整数 n 和一个下标从 0 开始的整数数组 batteries ,其中第 i 个电池可以让一台电脑 运行 batteries[i] 分钟。你想使用这些电池让 全部 n 台电脑 同时 运行。

一开始,你可以给每台电脑连接 至多一个电池 。然后在任意整数时刻,你都可以将一台电脑与它的电池断开连接,并连接另一个电池,你可以进行这个操作 任意次 。新连接的电池可以是一个全新的电池,也可以是别的电脑用过的电池。断开连接和连接新的电池不会花费任何时间。

注意,你不能给电池充电。

请你返回你可以让 n 台电脑同时运行的 最长 分钟数。

示例 1:

img

输入:n = 2, batteries = [3,3,3]
输出:4
解释:
一开始,将第一台电脑与电池 0 连接,第二台电脑与电池 1 连接。
2 分钟后,将第二台电脑与电池 1 断开连接,并连接电池 2 。注意,电池 0 还可以供电 1 分钟。
在第 3 分钟结尾,你需要将第一台电脑与电池 0 断开连接,然后连接电池 1 。
在第 4 分钟结尾,电池 1 也被耗尽,第一台电脑无法继续运行。
我们最多能同时让两台电脑同时运行 4 分钟,所以我们返回 4 。

public static void main(String[] args) {
    System.out.println(maxRunTime(3, new int[]{10, 10, 3, 5}));
}
public  static long maxRunTime(int n, int[] batteries) {
    long sum = 0;
    long min = Integer.MAX_VALUE;
    for(int i = 0 ;i<batteries.length;i++){
        sum +=batteries[i];
        min = Math.min(min,batteries[i]);
    }
    long l  = min;
    long r = sum;
    long ans = min;
    while(l<=r){
        long mid = l+((r-l)>>1);
        if(f(n,mid,batteries,sum)){
            ans = mid;
            l = mid+1;
        }else{
            r  = mid-1;
        }
    }
    return ans;
}
​
public static boolean f(int n,long mid ,int [] batteries,long sum){
    int length = batteries.length;
    for (int i:batteries){
        if (i>=mid){
            n--;
            sum-=i;
            length--;
        }
    }
    if(sum>=mid*n&&length>n){
        return true;
    }else{
        return false;
    }
}

谷歌真题
// 计算等位时间
// 给定一个数组arr长度为n,表示n个服务员,每服务一个人的时间
// 给定一个正数m,表示有m个人等位,如果你是刚来的人,请问你需要等多久?
// 假设m远远大于n,比如n <= 10^3, m <= 10^9,该怎么做是最优解?
// 谷歌的面试,这个题连考了2个月
// 找不到测试链接,所以用对数器验证
​
public static int waitingTime2(int[] arr, int m) {
    int min = Integer.MAX_VALUE;
    for (int i = 0;i<arr.length;i++){
        min = Math.min(min,arr[i]);
    }
    int r = min*m;
    int l = 0;
    int ans = 0;
    while (l<=r){
        int mid = l+((r-l)>>1);
        if (f(mid,arr,m)){
            ans = mid;
            r = mid-1;
        }else {
            l = mid+1;
        }
    }
    return ans;
}
​
public static boolean f(int mid,int [] arr,int m){
    int ans = 0;
    for (int num : arr) {
        ans += (mid / num) + 1;
    }
    return ans>m;
}
​
​
// 堆模拟
// 验证方法,不是重点
// 如果m很大,该方法会超时
// 时间复杂度O(m * log(n)),额外空间复杂度O(n)
public static int waitingTime1(int[] arr, int m) {
    // 一个一个对象int[]
    // [醒来时间,服务一个客人要多久]
    PriorityQueue<int[]> heap = new PriorityQueue<>((a, b) -> (a[0] - b[0]));
    int n = arr.length;
    for (int i = 0; i < n; i++) {
        heap.add(new int[] { 0, arr[i] });
    }
    for (int i = 0; i < m; i++) {
        int[] cur = heap.poll();
        cur[0] += cur[1];
        heap.add(cur);
    }
    return heap.peek()[0];
}
​
​
// 对数器测试
public static void main(String[] args) {
    System.out.println("测试开始");
    int N = 50;
    int V = 30;
    int M = 3000;
    int testTime = 20000;
    for (int i = 0; i < testTime; i++) {
        int n = (int) (Math.random() * N) + 1;
        int[] arr = randomArray(n, V);
        int m = (int) (Math.random() * M);
        int ans1 = waitingTime1(arr, m);
        int ans2 = waitingTime2(arr, m);
        if (ans1 != ans2) {
            System.out.println("出错了!");
        }
    }
    System.out.println("测试结束");
}
​
// 对数器测试
public static int[] randomArray(int n, int v) {
    int[] arr = new int[n];
    for (int i = 0; i < n; i++) {
        arr[i] = (int) (Math.random() * v) + 1;
    }
    return arr;
}

大厂真题
// 刀砍毒杀怪兽问题
// 怪兽的初始血量是一个整数hp,给出每一回合刀砍和毒杀的数值cuts和poisons
// 第i回合如果用刀砍,怪兽在这回合会直接损失cuts[i]的血,不再有后续效果
// 第i回合如果用毒杀,怪兽在这回合不会损失血量,但是之后每回合都损失poisons[i]的血量
// 并且你选择的所有毒杀效果,在之后的回合都会叠加
// 两个数组cuts、poisons,长度都是n,代表你一共可以进行n回合
// 每一回合你只能选择刀砍或者毒杀中的一个动作
// 如果你在n个回合内没有直接杀死怪兽,意味着你已经无法有新的行动了
// 但是怪兽如果有中毒效果的话,那么怪兽依然会在血量耗尽的那回合死掉
// 返回至少多少回合,怪兽会死掉
// 数据范围 :
// 1 <= n <= 10^5
// 1 <= hp <= 10^9
// 1 <= cuts[i]、poisons[i] <= 10^9
// 本题来自真实大厂笔试,找不到测试链接,所以用对数器验证
​
// 动态规划方法(只是为了验证)
// 目前没有讲动态规划,所以不需要理解这个函数
// 这个函数只是为了验证二分答案的方法是否正确的
// 纯粹为了写对数器验证才设计的方法,血量比较大的时候会超时
// 这个方法不做要求,此时并不需要理解,可以在学习完动态规划章节之后来看看这个函数
public static int fast1(int[] cuts, int[] poisons, int hp) {
    int sum = 0;
    for (int num : poisons) {
        sum += num;
    }
    int[][][] dp = new int[cuts.length][hp + 1][sum + 1];
    return f1(cuts, poisons, 0, hp, 0, dp);
}
​
// 不做要求
public static int f1(int[] cuts, int[] poisons, int i, int r, int p, int[][][] dp) {
    r -= p;
    if (r <= 0) {
        return i + 1;
    }
    if (i == cuts.length) {
        if (p == 0) {
            return Integer.MAX_VALUE;
        } else {
            return cuts.length + 1 + (r + p - 1) / p;
        }
    }
    if (dp[i][r][p] != 0) {
        return dp[i][r][p];
    }
    int p1 = r <= cuts[i] ? (i + 1) : f1(cuts, poisons, i + 1, r - cuts[i], p, dp);
    int p2 = f1(cuts, poisons, i + 1, r, p + poisons[i], dp);
    int ans = Math.min(p1, p2);
    dp[i][r][p] = ans;
    return ans;
}
​
// 二分答案法
// 最优解
// 时间复杂度O(n * log(hp)),额外空间复杂度O(1)
public static int fast2(int[] cuts, int[] poisons, int hp) {
    int ans = Integer.MAX_VALUE;
   int r = hp+1;
   int l  = 0;
   while (l<=r){
       int mid = l+((r-l)>>1);
       if(f(cuts,poisons,hp,mid)){
           ans = mid;
           r = mid-1;
       }else {
           l = mid+1;
       }
   }
   return ans;
}
​
// cuts、posions,每一回合刀砍、毒杀的效果
// hp:怪兽血量
// limit:回合的限制
public static boolean f(int []cuts,int []poisons,int hp,int limit) {
    int n = Math.min(cuts.length, limit);
    for (int i = 0, j = 1; i < n; i++, j++) {
        int poison =  (limit - i-1) *  poisons[i];
        if (cuts[i]>poison){
            hp-=cuts[i];
        }else{
            hp -=poison;
        }
        if (hp <= 0) {
            return true;
        }
    }
    return false;
}
​
// 对数器测试
public static void main(String[] args) {
    // 随机测试的数据量不大
    // 因为数据量大了,fast1方法会超时
    // 所以在数据量不大的情况下,验证fast2方法功能正确即可
    // fast2方法在大数据量的情况下一定也能通过
    // 因为时间复杂度就是最优的
    System.out.println("测试开始");
    int N = 30;
    int V = 20;
    int H = 300;
    int testTimes = 10000;
    for (int i = 0; i < testTimes; i++) {
        int n = (int) (Math.random() * N) + 1;
        int[] cuts = randomArray(n, V);
        int[] posions = randomArray(n, V);
        int hp = (int) (Math.random() * H) + 1;
        int ans1 = fast1(cuts, posions, hp);
        int ans2 = fast2(cuts, posions, hp);
        if (ans1 != ans2) {
            System.out.println("出错了!");
        }
    }
    System.out.println("测试结束");
}
​
// 对数器测试
public static int[] randomArray(int n, int v) {
    int[] ans = new int[n];
    for (int i = 0; i < n; i++) {
        ans[i] = (int) (Math.random() * v) + 1;
    }
    return ans;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值