二分答案法

二分答案法

  • 估计最终答案的大概范围

  • 分析问题的答案和给定条件之间的单调性

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

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

875. 爱吃香蕉的珂珂

#include <vector>
#include <algorithm>

using namespace std;

class Solution {
public:
    // 返回要消耗的时间
    long timeConsuming(vector<int> &piles, int k) {
        long res = 0;
        for (const auto &item: piles)
            // item / k 向上取整,前提都是非负数
            res += (item + k - 1) / k;
        return res;
    }

    // 时间复杂度 O(n * log(max)),额外空间复杂度 O(1)
    int minEatingSpeed(vector<int> &piles, int h) {
        int left = 1;
        int right = 0;
        for (const auto &item: piles)
            right = max(right, item);
        int mid;

        while (left <= right) {
            mid = left + ((right - left) >> 1);
            if (timeConsuming(piles, mid) <= h) {
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }
        return left;
    }
};

410. 分割数组的最大值

画匠问题:

  • 一维数组表示每个位置的画完成需要的时间,k 表示画匠人数
  • 每个画匠可以画连续的几幅画,画匠可以并行工作,求最小耗时
  • 其实就是把数组分成连续的 k 个子数组,使得所有子数组中和最大的那个的和尽量小
#include <vector>
#include <algorithm>

using namespace std;

class Solution {
public:
    // 每个连续部分的和不超过 limit 的情况下,需要多少个画匠完成全部画作
    int painterNeeded(vector<int> &nums, int limit) {
        int count = 1;
        int sum = 0;
        // 时间复杂度 O(n)
        for (const auto &num: nums) {
            // 表示完成不了
            if (num > limit) return INT_MAX;
            if (sum + num > limit) {
                count++;
                sum = num;
            } else {
                sum += num;
            }
        }
        return count;
    }

    // 时间复杂度 O(n * log(sum)),额外空间复杂度 O(1)
    int splitArray(vector<int> &nums, int k) {
        long left = 0;
        long right = 0;
        for (const auto &item: nums)
            right += item;
        long mid;

        while (left <= right) {
            mid = left + ((right - left) >> 1);
            if (painterNeeded(nums, mid) <= k) {
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }
        return left;
    }
};

机器人跳跃问题

#include <vector>
#include <iostream>
#include <algorithm>

using namespace std;

// 以初始能量 energy 能否走完数组
bool finished(vector<int> &nums, int energy, int maxH) {
    for (const auto &item: nums) {
        energy += (energy - item);
        // 如果超过高度最大值,后面肯定通关了,可以提前返回
        if (energy >= maxH) return true;
        if (energy < 0) return false;
    }
    return true;
}

// 时间复杂度 O(n * log(maxH)),额外空间复杂度 O(1)
int main() {
    int n;
    cin >> n;
    vector<int> nums(n);
    int maxH = 0;
    for (int i = 0; i < n; ++i) {
        cin >> nums[i];
        maxH = max(maxH, nums[i]);
    }
    int left = 0;
    int right = maxH;
    int mid;

    while (left <= right) {
        mid = left + ((right - left) >> 1);
        if (finished(nums, mid, maxH)) {
            right = mid - 1;
        } else {
            left = mid + 1;
        }
    }
    cout << left;
}

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

#include <vector>
#include <algorithm>

using namespace std;

class Solution {
public:
    // 返回任意两数差值小于等于 limit 的数对个数
    int countLower(vector<int> &nums, int limit) {
        int count = 0;
        for (int l = 0, r = 0; l < nums.size(); ++l) {
            while (r + 1 < nums.size() && nums[r + 1] - nums[l] <= limit)
                r++;
            count += r - l;
        }
        return count;
    }

    // 时间复杂度 O(n * log(n) + n * log(max-min)),额外空间复杂度 O(1)
    int smallestDistancePair(vector<int> &nums, int k) {
        sort(nums.begin(), nums.end());
        int left = 0;
        int right = nums.back() - nums.front();
        int mid;

        while (left <= right) {
            mid = left + ((right - left) >> 1);
            if (countLower(nums, mid) >= k) {
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }
        return left;
    }
};

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

#include <vector>

using namespace std;

class Solution {
public:
    // 能否让 computers 台电脑共同运行 time 分钟
    bool finished(vector<int> &batteries, int computers, long time) {
        // 碎片电量总和
        long fragmentCharge = 0;
        for (const auto &charge: batteries) {
            if (charge > time) {
                // time 时间内全都给这台电脑供电,没有提供碎片电量
                computers--;
            } else {
                // 碎片电量
                fragmentCharge += charge;
            }
            // 碎片电量 >= 台数 * 要求
            if (fragmentCharge >= (long) computers * time) return true;
        }
        return false;
    }

    // 时间复杂度 O(n * log(sum)),额外空间复杂度 O(1)
    long long maxRunTime(int n, vector<int> &batteries) {
        long sum = 0;
        for (const auto &item: batteries)
            sum += item;
        long left = 0;
        long right = sum;
        long mid;

        while (left <= right) {
            mid = left + ((right - left) >> 1);
            if (finished(batteries, n, mid)) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return right;
    }
};
  • 贪心优化
#include <vector>

using namespace std;

class Solution {
public:
    // 能否让 computers 台电脑共同运行 time 分钟
    bool finished(vector<int> &batteries, int computers, long time) {
        // 碎片电量总和
        long fragmentCharge = 0;
        for (const auto &charge: batteries) {
            if (charge > time) {
                // time 时间内全都给这台电脑供电,没有提供碎片电量
                computers--;
            } else {
                // 碎片电量
                fragmentCharge += charge;
            }
            // 碎片电量 >= 台数 * 要求
            if (fragmentCharge >= (long) computers * time) return true;
        }
        return false;
    }

    // 时间复杂度 O(n * log(_max)),额外空间复杂度 O(1)
    long long maxRunTime(int n, vector<int> &batteries) {
        long sum = 0;
        int _max = 0;
        for (const auto &item: batteries) {
            sum += item;
            _max = max(_max, item);
        }

        // 优化
        if (sum > (long) _max * n) {
            // 所有电池的最大电量是 _max
            // 如果此时 sum > (long) _max * num,
            // 说明: 最终的供电时间一定在 >= max,而如果最终的供电时间 >= max
            // 说明: 对于最终的答案 X 来说,所有电池都是碎片电池
            // 那么寻找 ? * num <= sum 的情况中,尽量大的 ? 即可
            // 即 sum / num
            return sum / n;
        }
        // 最终的供电时间一定在 < _max 范围上

        long left = 0;
        long right = _max;
        long mid;

        while (left <= right) {
            mid = left + ((right - left) >> 1);
            if (finished(batteries, n, mid)) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return right;
    }
};

计算等位时间

  • 给定一个数组 arr 长度为 n,表示 n 个服务员,每服务一个人的时间
  • 给定一个正数 m,表示有 m 个人等位,如果你是刚来的人,每个客人都遵循有空位就上的原则,请问你需要等多久?
  • 假设 m 远远大于 n,比如 n <= 10^3, m <= 10^9,该怎么做是最优解?
package class051;

import java.util.PriorityQueue;

// 计算等位时间
// 给定一个数组arr长度为n,表示n个服务员,每服务一个人的时间
// 给定一个正数m,表示有m个人等位,如果你是刚来的人,请问你需要等多久?
// 假设m远远大于n,比如n <= 10^3, m <= 10^9,该怎么做是最优解?
// 谷歌的面试,这个题连考了2个月
// 找不到测试链接,所以用对数器验证
public class Code06_WaitingTime {

    // 堆模拟
    // 验证方法,不是重点
    // 如果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];
    }

    // 二分答案法
    // 最优解
    // 时间复杂度O(n * log(min * w)),额外空间复杂度O(1)
    public static int waitingTime2(int[] arr, int w) {
        int min = Integer.MAX_VALUE;
        for (int x : arr) {
            min = Math.min(min, x);
        }
        int ans = 0;
        for (int l = 0, r = min * w, m; l <= r; ) {
            // m中点,表示一定要让服务员工作的时间!
            m = l + ((r - l) >> 1);
            // 能够给几个客人提供服务
            if (f(arr, m) >= w + 1) {
                ans = m;
                r = m - 1;
            } else {
                l = m + 1;
            }
        }
        return ans;
    }

    // 如果每个服务员工作time,可以接待几位客人(结束的、开始的客人都算)
    public static int f(int[] arr, int time) {
        int ans = 0;
        for (int num : arr) {
            ans += (time / num) + 1;
        }
        return ans;
    }

    // 对数器测试
    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;
    }

}

刀砍毒杀怪兽问题

package class051;

// 刀砍毒杀怪兽问题
// 怪兽的初始血量是一个整数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 class Code07_CutOrPoison {

    // 动态规划方法(只是为了验证)
    // 目前没有讲动态规划,所以不需要理解这个函数
    // 这个函数只是为了验证二分答案的方法是否正确的
    // 纯粹为了写对数器验证才设计的方法,血量比较大的时候会超时
    // 这个方法不做要求,此时并不需要理解,可以在学习完动态规划章节之后来看看这个函数
    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;
       for (int l = 1, r = hp + 1, m; l <= r;) {
          // m中点,一定要让怪兽在m回合内死掉,更多回合无意义
          m = l + ((r - l) >> 1);
          if (f(cuts, poisons, hp, m)) {
             ans = m;
             r = m - 1;
          } else {
             l = m + 1;
          }
       }
       return ans;
    }

    // cuts、posions,每一回合刀砍、毒杀的效果
    // hp:怪兽血量
    // limit:回合的限制
    public static boolean f(int[] cuts, int[] posions, long hp, int limit) {
       int n = Math.min(cuts.length, limit);
       for (int i = 0, j = 1; i < n; i++, j++) {
          hp -= Math.max((long) cuts[i], (long) (limit - j) * (long) posions[i]);
          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;
    }

}
posted @ 2024-10-09 13:15  n1ce2cv  阅读(4)  评论(0编辑  收藏  举报