二分算法

二分算法

1. 二分算法原理

原理

  • 二分的本质是具有两段性

整数二分

  • 整数二分需要考虑很多边界问题,为此,整理如下模板(参考网址
bool check(int x) {/* ... */} // 检查x是否满足某种性质

// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
int bsearch_1(int l, int r)
{
    while (l < r)
    {
        int mid = l + r >> 1;
        if (check(mid)) r = mid;    // check()判断mid是否满足性质
        else l = mid + 1;
    }
    return l;
}
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
int bsearch_2(int l, int r)
{
    while (l < r)
    {
        int mid = l + r + 1 >> 1;
        if (check(mid)) l = mid;
        else r = mid - 1;
    }
    return l;
}

浮点数二分

  • 浮点数二分算法就没有边界问题了,模板如下(参考网址):
bool check(double x) {/* ... */} // 检查x是否满足某种性质

double bsearch_3(double l, double r)
{
    const double eps = 1e-6;   // eps 表示精度,取决于题目对精度的要求
    while (r - l > eps)
    {
        double mid = (l + r) / 2;
        if (check(mid)) r = mid;
        else l = mid;
    }
    return l;
}

2. AcWing上的二分题目

AcWing 789. 数的范围

问题描述

分析

  • 二次二分即可。

  • 第一次二分出大于等于目标值最左边的一个数据的位置,然后判断该位置的值是否等于目标值,如果不等于,输出-1 -1即可,如果等于,进行第二次二分。

  • 第二次二分出小于等于目标值最右边的一个数据的位置。

代码

  • C++
#include <cstdio>

const int N = 100010;

int n, m;
int a[N];

int main() {
    
    scanf("%d%d", &n, &m);
    for (int i = 0; i < n; i++) scanf("%d", &a[i]);
    
    while (m--) {
        int k;
        scanf("%d", &k);
        
        int l = 0, r = n - 1;
        while (l < r) {
            int mid = l + r >> 1;
            if (a[mid] >= k) r = mid;
            else l = mid + 1;
        }
        
        if (a[r] != k) {
            printf("-1 -1\n");
            continue;
        }
        
        int L = l;
        l = 0, r = n - 1;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (a[mid] <= k) l = mid;
            else r = mid - 1;
        }
        printf("%d %d\n", L, r);
    }
    
    return 0;
}

AcWing 790. 数的三次方根

问题描述

分析

  • 本题是浮点数二分,因此不需要考虑边界问题。

  • 因为最终的结果需要保留6位小数,一般情况下二分的边界多2位小数即可。

代码

  • C++
#include <cstdio>

int main() {
    
    double x;
    scanf("%lf", &x);
    
    double l = -100, r = 100;
    while (r - l > 1e-8) {
        double mid = (l + r) / 2;
        if (mid * mid * mid >= x) r = mid;
        else l = mid;
    }
    
    printf("%.6lf", r);
    
    return 0;
}

AcWing 102. 最佳牛围栏

问题描述

分析

  • 本题是一个浮点数二分的问题,二分的区间为[l, r],其中l=0r为数组最大值。

  • 每次取mid = (l+r)/2,然后检验是否存在一个长度大于F的区间平均值大于mid

  • 为了判断是否存在满足条件的区间,可以将数组中的数全部减去mid,这样问题就变为了是否存在一个长度大于F的区间平均值大于0。这样可以使用前缀和的技巧。

  • 使用mins表示s[0~i-F]中的最小值,每次判断s[i]是否大于等于mins即可,如果大于等于的话,说明存在满足条件的区间,返回true即可。

  • 最后输出答案的话,需要输出r,虽然说理论上lr是相等的,但由于误差的存在,lr略小。

代码

  • C++
#include <iostream>

using namespace std;

const int N = 100010;

int n, F;
double a[N], s[N];

bool check(double avg) {
    for (int i = 1; i <= n; i++) s[i] = s[i - 1] + a[i] - avg;
    
    double mins = 0;
    for (int i = F; i <= n; i++) {
        mins = min(mins, s[i - F]);
        if (s[i] >= mins) return true;
    }
    return false;
}

int main() {
    
    scanf("%d%d", &n, &F);
    
    double l = 0, r = 0;
    for (int i = 1; i <= n; i++) {
        scanf("%lf", &a[i]);
        r = max(r, a[i]);
    }
    
    while (r - l >= 1e-5) {
        double mid = (l + r) / 2;
        if (check(mid)) l = mid;
        else r = mid;
    }
    
    printf("%d\n", (int)(r * 1000));
    
    return 0;
}

AcWing 113. 特殊排序

问题描述

分析

  • 本题的做法类似于插入排序。

  • 首先我们将1插入到答案中,然后依次遍历2~N,每次将一个数据插入到已经排好序的数组中。现在的问题是给我们一个已经排好序的数组,如何找到插入数据的位置。

  • 假设已经排好序的数组是res,取数组中点mid,如果res[mid]>i,则i一定可以插到左边,我们可以找到第一个小于i的数res[k],则i可以插到res[k]res[k+1]之间,否则如果左边所有数据都大于i,则i可以插到数组开头。

  • 同理,如果res[mid]<i,说明i一定可以插入到mid的右侧。

代码

  • C++
// Forward declaration of compare API.
// bool compare(int a, int b);
// return bool means whether a is less than b.

class Solution {
public:
    vector<int> specialSort(int N) {
        vector<int> res(1, 1);
        for (int i = 2; i <= N; i++) {
            int l = 0, r = res.size() - 1;
            while (l < r) {
                int mid = l + r + 1>> 1;
                // res[mid] < i: 找到小于最右侧i的位置
                if (compare(res[mid], i)) l = mid;
                else r = mid - 1;
            }
            // 此时i应该插到res[r]~res[r+1]之间
            res.push_back(i);
            for (int j = res.size() - 2; j > r; j--) swap(res[j], res[j + 1]);
            // 如果i小于res中所有的元素,则r=0,此时i在res[1]处,需要将i换到res[0]上
            if (compare(i, res[r])) swap(res[0], res[1]);
        }
        return res;
    }
};

3. 力扣上的二分题目

Leetcode 0004 寻找两个正序数组的中位数

题目描述:Leetcode 0004 寻找两个正序数组的中位数

在这里插入图片描述

分析

  • 本题的考点:二分

  • 这里将nums1、nums2分别记为A、B。对于给定的数组A、B,我们要找到这两个数组中的中位数,我们考虑一个更加一般的问题,如何找到第k小的数据。

  • 假设我们需要找到A[i...]B[j...]这两个数组的第k小的数据,则我们可以比较 A [ i + ⌊ k 2 ⌋ − 1 ] A[i+\lfloor \frac{k}{2}\rfloor-1] A[i+2k1] B [ j + ⌊ k 2 ⌋ − 1 ] B[j+\lfloor \frac{k}{2}\rfloor-1] B[j+2k1] 的大小,则会存在三种情况:

    (1) A [ i + ⌊ k 2 ⌋ − 1 ] ≤ B [ j + ⌊ k 2 ⌋ − 1 ] A[i+\lfloor \frac{k}{2}\rfloor-1] \le B[j+\lfloor \frac{k}{2}\rfloor-1] A[i+2k1]B[j+2k1] :说明 A [ i . . . i + ⌊ k 2 ⌋ − 1 ] A[i...i+\lfloor \frac{k}{2}\rfloor-1] A[i...i+2k1] 都不可能是第k小的数,这是因为即使 B [ j . . . j + ⌊ k 2 ⌋ − 2 ] B[j...j+\lfloor \frac{k}{2}\rfloor-2] B[j...j+2k2]都小于 A [ i + ⌊ k 2 ⌋ − 1 ] A[i+\lfloor \frac{k}{2}\rfloor-1] A[i+2k1]的话, A [ i + ⌊ k 2 ⌋ − 1 ] A[i+\lfloor \frac{k}{2}\rfloor-1] A[i+2k1]也只是第k-1小的数,因此可以被删除;

    (2) A [ i + ⌊ k 2 ⌋ − 1 ] > B [ j + ⌊ k 2 ⌋ − 1 ] A[i+\lfloor \frac{k}{2}\rfloor-1] > B[j+\lfloor \frac{k}{2}\rfloor-1] A[i+2k1]>B[j+2k1] :说明 B [ j . . . j + ⌊ k 2 ⌋ − 1 ] B[j...j+\lfloor \frac{k}{2}\rfloor-1] B[j...j+2k1] 都不可能是第k小的数,同理也可以被删除;

在这里插入图片描述

  • 下面的代码中让: n e w I = i + ⌊ k 2 ⌋ − 1 newI = i+\lfloor \frac{k}{2}\rfloor-1 newI=i+2k1 n e w J = j + ⌊ k 2 ⌋ − 1 newJ = j+\lfloor \frac{k}{2}\rfloor-1 newJ=j+2k1

代码

  • C++
class Solution {
public:
    double findMedianSortedArrays(vector<int>& A, vector<int>& B) {

        int n = A.size() + B.size();
        if (n % 2) return get(A, B, n / 2 + 1);
        else {
            int l = get(A, B, n / 2);
            int r = get(A, B, n / 2 + 1);
            return (l + r) / 2.0;
        }
    }

    // 在两个有序数组 A 和 B 中获取第k小(从1开始)的数据
    int get(vector<int> A, vector<int> B, int k) {
        
        int i = 0, j = 0;  // 当前考察的区间 nums1[i...], nums2[j...]
        while (true) {
            // 考虑各种边界情况
            if (i == A.size()) return B[j + k - 1];
            if (j == B.size()) return A[i + k - 1];
            if (k == 1) return min(A[i], B[j]);

            int newI = min(i + k / 2, (int)A.size()) - 1;
            int newJ = min(j + k / 2, (int)B.size()) - 1;
            if (A[newI] <= B[newJ]) {
                k -= (newI - i + 1);  // 此时应该删除A[i...newI]
                i = newI + 1;
            } else {
                k -= (newJ - j + 1);  // 此时应该删除B[j...newJ]
                j = newJ + 1;
            }
        }
    }
};
  • Java
class Solution {
    public double findMedianSortedArrays(int[] A, int[] B) {

        int n = A.length + B.length;
        if (n % 2 != 0) {
            return get(A, B, n / 2 + 1);
        } else {
            int l = get(A, B, n / 2);
            int r = get(A, B, n / 2 + 1);
            return (l + r) / 2.0;
        }
    }

    // 在两个有序数组 A 和 B 中获取第k小(从1开始)的数据
    private int get(int[] A, int[] B, int k) {

        int i = 0, j = 0;  // 当前考察的区间 nums1[i...], nums2[j...]
        while (true) {
            // 考虑各种边界情况
            if (i == A.length) return B[j + k - 1];
            if (j == B.length) return A[i + k - 1];
            if (k == 1) return Math.min(A[i], B[j]);

            int newI = Math.min(i + k / 2, A.length) - 1;
            int newJ = Math.min(j + k / 2, B.length) - 1;
            if (A[newI] <= B[newJ]) {
                k -= (newI - i + 1);  // 此时应该删除A[i...newI]
                i = newI + 1;
            } else {
                k -= (newJ - j + 1);  // 此时应该删除B[j...newJ]
                j = newJ + 1;
            }
        }
    }
}
  • Python
class Solution:
    def findMedianSortedArrays(self, A: List[int], B: List[int]) -> float:
        n = len(A) + len(B)
        if n % 2 == 1:
            return self.get(A, B, n // 2 + 1)
        else:
            l = self.get(A, B, n // 2)
            r = self.get(A, B, n // 2 + 1)
            return (l + r) / 2

    def get(self, A, B, k):
        i = 0
        j = 0
        while True:
            if i == len(A):
                return B[j + k - 1]
            if j == len(B):
                return A[i + k - 1]
            if k == 1:
                return min(A[i], B[j])

            newI = min(i + k // 2, len(A)) - 1
            newJ = min(j + k // 2, len(B)) - 1
            if A[newI] <= B[newJ]:
                k -= (newI - i + 1)
                i = newI + 1
            else:
                k -= (newJ - j + 1)
                j = newJ + 1

时空复杂度分析

  • 时间复杂度: O ( l o g ( n + m ) ) O(log(n+m)) O(log(n+m))n、m为两个数组的长度。这是因为我们每次会让k减少一半。

  • 空间复杂度: O ( 1 ) O(1) O(1)

Leetcode 0033 搜索旋转排序数组

题目描述:Leetcode 0033 搜索旋转排序数组

在这里插入图片描述

分析

  • 本题的考点:二分

  • 二分的本质是二段性

  • 本题使用两次二分:(1)二分出旋转点的位置;(2)第二次二分出答案。关键是第一次二分,如下图:

在这里插入图片描述

  • 我们可以发现左右两部分具有两段性,左边元素全部 ≥ n u m s [ 0 ] \ge nums[0] nums[0],右边的元素 < n u m s [ 0 ] <nums[0] <nums[0]。据此可以二分出分界点。

  • 之后判断我们需要寻找的目标值targetnums[0]之间的关系,如果 t a r g e t ≥ n u m s [ 0 ] target \ge nums[0] targetnums[0],则在第一段中第二次二分,否则在另一半二分。

代码

  • C++
class Solution {
public:
    int search(vector<int>& nums, int target) {

        // 二分分界点
        int l = 0, r = nums.size() - 1;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (nums[mid] >= nums[0]) l = mid;
            else r = mid - 1;
        }
        // 判断target在哪一段中
        if (target >= nums[0]) l = 0;
        else l = r + 1, r = nums.size() - 1;
        // 二分出答案
        while (l < r) {
            int mid = l + r >> 1;
            if (nums[mid] >= target) r = mid;
            else l = mid + 1;
        }
        
        if (nums[r] == target) return r;
        return -1;
    }
};
  • Java
class Solution {
    public int search(int[] nums, int target) {

        // 二分分界点
        int l = 0, r = nums.length - 1;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (nums[mid] >= nums[0]) l = mid;
            else r = mid - 1;
        }
        // 判断target在哪一段中
        if (target >= nums[0]) l = 0;
        else {
            l = r + 1; r = nums.length - 1;
        }
        // 二分出答案
        while (l < r) {
            int mid = l + r >> 1;
            if (nums[mid] >= target) r = mid;
            else l = mid + 1;
        }
        
        if (nums[r] == target) return r;
        return -1;
    }
}
  • Python
class Solution:
    def search(self, nums: List[int], target: int) -> int:
        l = 0; r = len(nums) - 1
        while l < r:
            mid = l + r + 1 >> 1
            if nums[mid] >= nums[0]:
                l = mid
            else:
                r = mid - 1
        
        if target >= nums[0]:
            l = 0
        else:
            l = r + 1
            r = len(nums) - 1
        while l < r:
            mid = l + r >> 1
            if nums[mid] >= target:
                r = mid
            else:
                l = mid + 1
        
        if nums[r] == target:
            return r
        else:
            return -1

时空复杂度分析

  • 时间复杂度: O ( l o g ( n ) ) O(log(n)) O(log(n))n为数组长度。

  • 空间复杂度: O ( 1 ) O(1) O(1)

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

题目描述:Leetcode 0034 在排序数组中查找元素的第一个和最后一个位置

在这里插入图片描述

分析

  • 本题的考点:二分

  • 本题和AcWing 789. 数的范围一样。

  • 二次二分即可,第一次二分出左端点,第二次二分出右端点。

代码

  • C++
class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {

        if (nums.empty()) return {-1, -1};

        int l = 0, r = nums.size() - 1;
        while (l < r) {
            int mid = l + r >> 1;
            if (nums[mid] >= target) r = mid;
            else l = mid + 1;
        }

        if (nums[r] != target) return {-1, -1};

        int L = l;
        l = 0, r = nums.size() - 1;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (nums[mid] <= target) l = mid;
            else r = mid - 1;
        }
        
        return {L, r};
    }
};
  • Java
class Solution {
    public int[] searchRange(int[] nums, int target) {

        if (nums.length == 0) return new int[]{-1, -1};

        int l = 0, r = nums.length - 1;
        while (l < r) {
            int mid = l + r >> 1;
            if (nums[mid] >= target) r = mid;
            else l = mid + 1;
        }

        if (nums[r] != target) return new int[]{-1, -1};

        int L = l;
        l = 0; r = nums.length - 1;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (nums[mid] <= target) l = mid;
            else r = mid - 1;
        }
        
        return new int[]{L, r};
    }
}
  • Python
class Solution:
    def searchRange(self, nums: List[int], target: int) -> List[int]:
        if len(nums) == 0:
            return [-1, -1]
        l = 0; r = len(nums) - 1
        while l < r:
            mid = l + r >> 1
            if nums[mid] >= target:
                r = mid
            else:
                l = mid + 1
        if nums[r] != target:
            return [-1, -1]

        L = l
        l = 0; r = len(nums) - 1
        while l < r:
            mid = l + r + 1 >> 1
            if nums[mid] <= target:
                l = mid
            else:
                r = mid - 1
        return [L, r]

时空复杂度分析

  • 时间复杂度: O ( l o g ( n ) ) O(log(n)) O(log(n))n为数组长度。

  • 空间复杂度: O ( 1 ) O(1) O(1)

Leetcode 0035 搜索插入位置

题目描述:Leetcode 0035 搜索插入位置

在这里插入图片描述

分析

  • 本题的考点:二分

  • 二分出大于等于target的最小的数所在的位置即可。注意这里可能所有的数都小于target,因此右端点初始为nums.size()

代码

  • C++
class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {

        int l = 0, r = nums.size();
        while (l < r) {
            int mid = l + r >> 1;
            if (nums[mid] >= target) r = mid;
            else l = mid + 1;
        }
        return r;
    }
};
  • Java
class Solution {
    public int searchInsert(int[] nums, int target) {

        int l = 0, r = nums.length;
        while (l < r) {
            int mid = l + r >> 1;
            if (nums[mid] >= target) r = mid;
            else l = mid + 1;
        }
        return r;
    }
}
  • Python
class Solution:
    def searchInsert(self, nums: List[int], target: int) -> int:
        l = 0; r = len(nums)
        while l < r:
            mid = l + r >> 1
            if nums[mid] >= target:
                r = mid
            else:
                l = mid + 1
        return r

时空复杂度分析

  • 时间复杂度: O ( l o g ( n ) ) O(log(n)) O(log(n))n为数组长度。

  • 空间复杂度: O ( 1 ) O(1) O(1)

Leetcode 0069 x的平方根

题目描述:Leetcode 0069 x的平方根

在这里插入图片描述

分析

  • 本题的考点:二分

  • 答案区间一定在[0, x]之间,每次取中间数mid,如果 m i d 2 ≤ x mid ^ 2 \le x mid2x,说明答案一定在[mid, x]之间,否则答案在[0, mid-1]之间。

代码

  • C++
class Solution {
public:
    int mySqrt(int x) {

        int l = 0, r = x;
        while (l < r) {
            int mid = l + 1ll + r >> 1;  // 防止越界
            if (mid <= x / mid) l = mid;  // 防止越界
            else r = mid - 1;
        }
        return r;
    }
};
  • Java
class Solution {
    public int mySqrt(int x) {

        int l = 0, r = x;
        while (l < r) {
            int mid = (int)((long)l + r + 1 >> 1);  // 防止越界
            if (mid <= x / mid) l = mid;  // 防止越界
            else r = mid - 1;
        }
        return r;
    }
}
  • Python
class Solution:
    def mySqrt(self, x: int) -> int:
        l = 0; r = x
        while l < r:
            mid = (r - l + 1) // 2 + l
            if mid <= x // mid:
                l = mid
            else:
                r = mid - 1
        return r

时空复杂度分析

  • 时间复杂度: O ( l o g ( x ) ) O(log(x)) O(log(x))

  • 空间复杂度: O ( 1 ) O(1) O(1)

Leetcode 0074 搜索二维矩阵

题目描述:Leetcode 0074 搜索二维矩阵

在这里插入图片描述

分析

  • 本题的考点:二分

  • 二维数组按照从上向下依次一行一行展开后是递增的,因此可以使用二分。

  • 我们二分的区间为 [ 0 , n × m − 1 ] [0, n \times m - 1] [0,n×m1]n、m为行数、列数。关键问题对于区间中的mid,如何转为二维坐标?答案是:(mid / m, mid % m)

代码

  • C++
class Solution {
public:
    bool searchMatrix(vector<vector<int>> &matrix, int target) {

        int n = matrix.size(), m = matrix[0].size();
        if (matrix[0][0] > target || matrix[n - 1][m - 1] < target) return false;

        int l = 0, r = n * m - 1;
        while (l < r) {
            int mid = l + r >> 1;
            if (matrix[mid / m][mid % m] >= target) r = mid;
            else l = mid + 1;
        }
        return matrix[l / m][l % m] == target;
    }
};
  • Java
public class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {

        int n = matrix.length, m = matrix[0].length;

        int l = 0, r = n * m - 1;
        while (l < r) {
            int mid = (l + r) / 2;
            if (matrix[mid / m][mid % m] >= target) r = mid;
            else l = mid + 1;
        }
        return matrix[r / m][r % m] == target;
    }
}
  • Python
class Solution:
    def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
        n = len(matrix); m = len(matrix[0])
        l = 0; r = n * m - 1
        while l < r:
            mid = (l + r) // 2
            if matrix[mid // m][mid % m] >= target:
                r = mid
            else:
                l = mid + 1
        return matrix[r // m][r % m] == target

时空复杂度分析

  • 时间复杂度: O ( l o g ( n × m ) ) O(log(n \times m)) O(log(n×m))n、m为行数、列数。

  • 空间复杂度: O ( 1 ) O(1) O(1)

Leetcode 0081 搜索旋转排序数组II

题目描述:Leetcode 0081 搜索旋转排序数组II

在这里插入图片描述

分析

  • 本题的考点:二分

  • 本题是Leetcode 0033 搜索旋转排序数组的扩展题目,区别只有一点:本题中可以存在相同元素。

  • 为了能二分出分界点,我们需要删除最后和nums[0]相等的数据,这样就转换成了Leetcode 0033 搜索旋转排序数组,按照LC33的做法做一遍即可,分析如下:

  • 使用两次二分:(1)二分出旋转点的位置;(2)第二次二分出答案。关键是第一次二分,如下图:

在这里插入图片描述

  • 我们可以发现左右两部分具有两段性,左边元素全部 ≥ n u m s [ 0 ] \ge nums[0] nums[0],右边的元素 < n u m s [ 0 ] <nums[0] <nums[0]。据此可以二分出分界点。

  • 之后判断我们需要寻找的目标值targetnums[0]之间的关系,如果 t a r g e t ≥ n u m s [ 0 ] target \ge nums[0] targetnums[0],则在第一段中第二次二分,否则在另一半二分。

代码

  • C++
class Solution {
public:
    bool search(vector<int> &nums, int target) {

        int R = nums.size() - 1;
        while (R >= 0 && nums[R] == nums[0]) R--;
        if (R < 0) return nums[0] == target;  // 说明所有的数都一样

        int l = 0, r = R;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (nums[mid] >= nums[0]) l = mid;
            else r = mid - 1;
        }

        // 寻找target在哪一有序区间中
        if (target >= nums[0]) l = 0;
        else l++, r = R;

        while (l < r) {
            int mid = l + r >> 1;
            if (nums[mid] >= target) r = mid;
            else l = mid + 1;
        }

        return nums[r] == target;
    }
};
  • Java
class Solution {
    public boolean search(int[] nums, int target) {

        int R = nums.length - 1;
        while (R >= 0 && nums[R] == nums[0]) R--;
        if (R < 0) return nums[0] == target;  // 说明所有的数都一样

        int l = 0, r = R;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (nums[mid] >= nums[0]) l = mid;
            else r = mid - 1;
        }

        // 寻找target在哪一有序区间中
        if (target >= nums[0]) l = 0;
        else {
            l++; r = R;
        }

        while (l < r) {
            int mid = l + r >> 1;
            if (nums[mid] >= target) r = mid;
            else l = mid + 1;
        }

        return nums[r] == target;
    }
}

时空复杂度分析

  • 时间复杂度: O ( n ) O(n) O(n)n为数组长度。因为当数组中所有元素都相同时,需要遍历一遍数数组。

  • 空间复杂度: O ( 1 ) O(1) O(1)

Leetcode 0153 寻找旋转排序数组中的最小值

题目描述:Leetcode 0153 寻找旋转排序数组中的最小值

在这里插入图片描述

分析

在这里插入图片描述

  • 我们可以发现左右两部分具有两段性,左边元素全部 ≥ n u m s [ 0 ] \ge nums[0] nums[0],右边的元素 < n u m s [ 0 ] <nums[0] <nums[0]。据此可以二分出分界点。

代码

  • C++
class Solution {
public:
    int findMin(vector<int> &nums) {
        int l = 0, r = nums.size() - 1;
        if (nums[r] >= nums[l]) return nums[0];

        while (l < r) {
            int mid = l + r >> 1;
            if (nums[mid] < nums[0]) r = mid;
            else l = mid + 1;
        }
        return nums[r];
    }
};
  • Java
class Solution {
    public int findMin(int[] nums) {
        int l = 0, r = nums.length - 1;
        if (nums[r] >= nums[l]) return nums[0];  // 说明未旋转

        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (nums[mid] >= nums[0]) l = mid;
            else r = mid - 1;
        }
        return nums[r + 1];
    }
}

时空复杂度分析

  • 时间复杂度: O ( l o g ( n ) ) O(log(n)) O(log(n))n为数组长度。

  • 空间复杂度: O ( 1 ) O(1) O(1)

Leetcode 0154 寻找旋转排序数组中的最小值 II

题目描述:Leetcode 0154 寻找旋转排序数组中的最小值 II

在这里插入图片描述

分析

代码

  • C++
class Solution {
public:
    int findMin(vector<int> &nums) {

        int l = 0, r = nums.size() - 1;
        while (l < r && nums[l] == nums[r]) r--;
        if (nums[r] > nums[l]) return nums[l];  // 说明原来就是递增数组
        while (l < r) {
            int mid = l + r >> 1;
            if (nums[mid] < nums[0]) r = mid;
            else l = mid + 1;
        }
        return nums[r];
    }
};
  • Java
class Solution {
    public int findMin(int[] nums) {

        int l = 0, r = nums.length - 1;
        while (l < r && nums[l] == nums[r]) r--;
        if (nums[r] > nums[l]) return nums[l];
        while (l < r) {
            int mid = l + r >> 1;
            if (nums[mid] < nums[0]) r = mid;
            else l = mid + 1;
        }
        return nums[r];
    }
}

时空复杂度分析

  • 时间复杂度: O ( n ) O(n) O(n)n为数组长度。

  • 空间复杂度: O ( 1 ) O(1) O(1)

Leetcode 0162 寻找峰值

题目描述:Leetcode 0162 寻找峰值

在这里插入图片描述

分析

  • 本题的考点:二分

  • 首先考虑这个问题是否一定存在答案?结论是一定存在答案的,从前向后看每个数据,nums[0]大于nums[-1],如果nums[1]小于nums[0],则nums[0]是答案,否则整个序列一定是严格单调递增的,则最后一个数据是答案。因此答案区间是[0, nums.size()-1]

  • 每次取中点nums[mid],如果nums[mid]>nums[mid+1],则说明存在一个答案在[0, mid]中,否则一定有nums[mid]<nums[mid+1],则说明存在一个答案在[mid+1, nums.size()-1]中。

代码

  • C++
class Solution {
public:
    int findPeakElement(vector<int> &nums) {

        int l = 0, r = nums.size() - 1;
        while (l < r) {
            int mid = l + r >> 1;
            // 不存在越界情况,如果越界,意味着mid = r,则l也必须为r,不能进入循环
            if (nums[mid] > nums[mid + 1]) r = mid;
            else l = mid + 1;
        }
        return r;
    }
};
  • Java
class Solution {
    public int findPeakElement(int[] nums) {

        int l = 0, r = nums.length - 1;
        while (l < r) {
            int mid = l + r >> 1;
            // 不存在越界情况,如果越界,意味着mid = r,则l也必须为r,不能进入循环
            if (nums[mid] > nums[mid + 1]) r = mid;
            else l = mid + 1;
        }
        return r;
    }
}

时空复杂度分析

  • 时间复杂度: O ( l o g ( n ) ) O(log(n)) O(log(n))n为数组长度。

  • 空间复杂度: O ( 1 ) O(1) O(1)

Leetcode 0275 H指数 II

题目描述:Leetcode 0275 H指数 II

在这里插入图片描述

分析

  • 本题的考点:二分

  • 因为数组是从小到大排好序的,我们每次可以在区间[0, n]中搜索答案,每次取中点mid,如果有citations[n-mid]>=mid,说明n-mid ~ n-1mid篇论文的引用次数大于mid次,是一个符合要求的答案,可以更新左端点,否则更新右端点。

代码

  • C++
class Solution {
public:
    int hIndex(vector<int> &c) {

        int n = c.size();
        int l = 0, r = n;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (c[n - mid] >= mid) l = mid;
            else r = mid - 1;
        }
        return r;
    }
};
  • Java
class Solution {
    public int hIndex(int[] c) {
        int n = c.length;
        int l = 0, r = n;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (c[n - mid] >= mid) l = mid;
            else r = mid - 1;
        }
        return r;
    }
}
  • Python
class Solution:
    def hIndex(self, cs: List[int]) -> int:
        n = len(cs)
        l, r = 0, n
        while l < r:
            mid = l + r + 1 >> 1
            if cs[n - mid] >= mid:
                l = mid
            else:
                r = mid - 1
        return r

时空复杂度分析

  • 时间复杂度: O ( l o g ( n ) ) O(log(n)) O(log(n))n为数组长度。

  • 空间复杂度: O ( 1 ) O(1) O(1)

Leetcode 0278 第一个错误的版本

题目描述:Leetcode 0278 第一个错误的版本

在这里插入图片描述

分析

  • 本题的考点:二分

  • 直接使用二分即可。

代码

  • C++
class Solution {
public:
    int firstBadVersion(int n) {

        int l = 1, r = n;
        while (l < r) {
            int mid = (long long)l + r >> 1;
            if (isBadVersion(mid))
                r = mid;
            else l = mid + 1;
        }
        return l;
    }
};
  • Java
class Solution {
    public int firstBadVersion(int n) {
        int l = 1, r = n;
        while (l < r) {
            int mid = (r - l) / 2 + l;
            if (isBadVersion(mid)) r = mid;
            else l = mid + 1;
        }
        return l;
    }
}

时空复杂度分析

  • 时间复杂度: O ( n ) O(n) O(n)n为数组长度。

  • 空间复杂度: O ( n ) O(n) O(n)

Leetcode 0367 有效的完全平方数

题目描述:Leetcode 0367 有效的完全平方数

在这里插入图片描述

分析

  • 本题的考点:二分

  • 二分出小于等于num的最大平方数l*l,最后判断l*l==num是否成立即可

代码

  • C++
class Solution {
public:
    bool isPerfectSquare(int num) {
        int l = 1, r = num;
        while (l < r) {
            int mid = (r - l + 1) / 2 + l;
            if ((long) mid * mid <= num) l = mid;
            else r = mid - 1;
        }
        return l * l == num;
    }
};
  • Java
class Solution {
    public boolean isPerfectSquare(int num) {

        int l = 1, r = num;  // 在[l...r]中寻找 num 的平方根
        while (l < r) {
            int mid = (r - l + 1) / 2 + l;
            if ((long) mid * mid <= num) l = mid;
            else r = mid - 1;
        }
        return l * l == num;
    }
}

时空复杂度分析

  • 时间复杂度: O ( l o g ( n u m ) ) O(log(num)) O(log(num))

  • 空间复杂度: O ( 1 ) O(1) O(1)

Leetcode 0374 猜数字大小

题目描述:Leetcode 0374 猜数字大小

在这里插入图片描述

分析

  • 本题的考点:二分

  • 直接使用二分即可。

代码

  • C++
class Solution {
public:
    int guessNumber(int n) {
        int l = 1, r = n;
        while (l < r) {
            int mid = (long) r + l + 1 >> 1;
            if (guess(mid) >= 0) l = mid;  // 说明猜小了
            else r = mid - 1;
        }
        return l;
    }
};
  • Java
public class Solution extends GuessGame {
    public int guessNumber(int n) {
        int l = 1, r = n;
        while (l < r) {
            int mid = (r - l + 1) / 2 + l;
            if (guess(mid) >= 0) l = mid;  // 说明猜小了
            else r = mid - 1;
        }
        return l;
    }
}

时空复杂度分析

  • 时间复杂度: O ( l o g ( n ) ) O(log(n)) O(log(n))

  • 空间复杂度: O ( 1 ) O(1) O(1)

Leetcode 0378 有序矩阵中第 K 小的元素

题目描述:Leetcode 0378 有序矩阵中第 K 小的元素

在这里插入图片描述

分析

  • 本题的考点:二分

  • 注意本题和Leetcode 0240 搜索二维矩阵 II的区别,LC240给的二维数组和本题具有同样的特点,但是LC240是找某个元素在数组中是否存在。

  • 我们要充分每行每列都是递增的特点,采用二分解决该问题,初始二分区间[l, r][INT_MIN, INT_MAX],每次二分中点mid,找出数组中小于等于mid的数据的个数cnt,如果cnt>=k,说明答案在[l, mid]之间,将r更新为mid,否则将l更新为mid+1

  • 如何快速找出二维数组中小于等于mid的数的数量呢?假设第一行matrix[0][i]是最大的小于等于mid的数,则第二行最大的小于等于mid的数所在的列一定不会大于i,否则就不满足每列都是递增的了。因此,我们使用j遍历每一行,初始让i=matrix.size() - 1,随着j的增加,i一定是递减到的,因此找出小于等于mid的数的数量时间复杂度是O(n)的。

代码

  • C++
class Solution {
public:
    int kthSmallest(vector<vector<int>>& matrix, int k) {
        int l = INT_MIN, r = INT_MAX;
        while (l < r) {
            int mid = (long long)l + r >> 1;
            int i = matrix[0].size() - 1, cnt = 0;  // 统计小于mid的数的数量
            for (int j = 0; j < matrix.size(); j++) {  // 第j行有多少元素小于mid
                while (i >= 0 && matrix[j][i] > mid) i--;
                cnt += i + 1;
            }
            if (cnt >= k) r = mid;
            else l = mid + 1;
        }
        return r;
    }
};
  • Java
class Solution {
    public int kthSmallest(int[][] matrix, int k) {
        int l = Integer.MIN_VALUE, r = Integer.MAX_VALUE;
        while (l < r) {
            int mid = (int)((long)l + r >> 1);
            int i = matrix[0].length - 1, cnt = 0;
            for (int j = 0; j < matrix.length; j++) {
                while (i >= 0 && matrix[j][i] > mid) i--;
                cnt += i + 1;
            }
            if (cnt >= k) r = mid;
            else l = mid + 1;
        }
        return l;
    }
}

时空复杂度分析

  • 时间复杂度: O ( n × l o g ( V ) ) O(n \times log(V)) O(n×log(V))n为数组行数或者列数,V = INT_MAX - INT_MIN

  • 空间复杂度: O ( 1 ) O(1) O(1)

Leetcode 0410 分割数组的最大值

题目描述:Leetcode 0410 分割数组的最大值

在这里插入图片描述

分析

  • 本题的考点:二分、贪心

  • 我们在int范围内二分答案,二分区间[l, r]初始化为[0, INT_MAX],每次取区间中点mid,判断每个子数组中的数据之和都小于mid所需要的最少区间数cnt是否小于等于m,如果成立的话,说明答案在[l, mid]之间,否则不成立的话答案在[mid+1, r]中。

  • 如果判断区间和小于等于mid所需要的区间个数呢?这可以使用贪心,从前向后遍历整个数组,假设遍历到元素x,如果当前子数组中数据之和加上x后仍小于mid,则将其加入该子数组,否则的话开一个新子数组。下面证明这种贪心方法是正确的。

  • 假设区间和小于mid所需要的区间个数最优解对应为ans,贪心解为cnt,则一定有cnt>=ans,下面证明ans>=cnt:最优解可以转化为贪心解,因为数组中的数据都是非负的,找到最优解和贪心解不同的第一段区间,最优解对应的这段区间元素个数一定小于贪心解对应的该段区间元素个数(不可能多于,否则区间和一定会大于mid),则最优解的这段区间可以转化为贪心解的区间;对于后面不同的区间都可以这样转化。

代码

  • C++
class Solution {
public:
    bool check(vector<int> &nums, int mid, int m) {
        int s = 0, cnt = 0;
        for (auto x : nums) {
            if (x > mid) return false;  // 这一个数字都无法放进去
            if (s + x > mid) {
                s = x;
                cnt++;
            } else s += x;
        }
        if (s) cnt++;
        return cnt <= m;
    }

    int splitArray(vector<int>& nums, int m) {
        int l = 0, r = INT_MAX;
        while (l < r) {
            int mid = (long long)l + r >> 1;
            if (check(nums, mid, m)) r = mid;
            else l = mid + 1;
        }
        return r;
    }
};
  • Java
class Solution {
    public int splitArray(int[] nums, int m) {
        int l = 0, r = Integer.MAX_VALUE;
        while (l < r) {
            int mid = (r - l) / 2 + l;
            if (check(nums, mid, m)) r = mid;
            else l = mid + 1;
        }
        return l;
    }

    private boolean check(int[] nums, int mid, int m) {
        int s = 0, cnt = 0;
        for (int x : nums) {
            if (x > mid) return false;  // 这一个数字都无法放进去
            if (s + x > mid) {
                s = x;
                cnt++;
            } else s += x;
        }
        if (s != 0) cnt++;
        return cnt <= m;
    }
}

时空复杂度分析

  • 时间复杂度: O ( n × l o g ( S ) ) O(n \times log(S)) O(n×log(S))n为数组长度,S=INT_MAX

  • 空间复杂度: O ( 1 ) O(1) O(1)

Leetcode 0436 寻找右区间

题目描述:Leetcode 0436 寻找右区间

在这里插入图片描述

分析

  • 本题的考点:二分

  • 首先按照左端点对区间进行升序排序,因为最后返回的数组需要有每个区间在原始数组中的位置信息,所以排序前需要将每个区间的位置信息存储下来。

  • 然后从前向后依次遍历每个区间,对于每个区间,二分答案区间的位置。

代码

  • C++
class Solution {
public:
    vector<int> findRightInterval(vector<vector<int>>& q) {
        int n = q.size();
        for (int i = 0; i < n; i++) q[i].push_back(i);
        sort(q.begin(), q.end());
        vector<int> res(n, -1);
        for (auto &x : q) {
            int l = 0, r = n - 1;
            while (l < r) {
                int mid = l + r >> 1;
                if (q[mid][0] >= x[1]) r = mid;
                else l = mid + 1;
            }
            if (q[r][0] >= x[1]) res[x[2]] = q[r][2];
        }
        return res;
    }
};
  • Java
class Solution {
    public int[] findRightInterval(int[][] interval) {
        int n = interval.length;
        List<int[]> q = new ArrayList<>();
        for (int i = 0; i < n; i++) q.add(new int[]{interval[i][0], interval[i][1], i});
        q.sort((a, b) -> a[0] - b[0]);  // Collections.sort(q, (a, b) -> a[0] - b[0]);
        int[] res = new int[n];
        Arrays.fill(res, -1);
        for (int[] x : q) {
            int l = 0, r = n - 1;
            while (l < r) {
                int mid = l + r >> 1;
                if (q.get(mid)[0] >= x[1]) r = mid;
                else l = mid + 1;
            }
            if (q.get(r)[0] >= x[1]) res[x[2]] = q.get(r)[2];
        }
        return res;
    }
}

时空复杂度分析

  • 时间复杂度: O ( n ) O(n) O(n)n为数组interval的长度。

  • 空间复杂度:考虑结果的存储 O ( n ) O(n) O(n)

Leetcode 0852 山脉数组的峰顶索引

题目描述:Leetcode 0852 山脉数组的峰顶索引

在这里插入图片描述

分析

  • 本题的考点:二分

  • 根据相邻两个数的大小二分即可。

代码

  • C++
class Solution {
public:
    int peakIndexInMountainArray(vector<int>& arr) {
        int n = arr.size();
        int l = 1, r = n - 2;
        while (l < r) {
            int mid = l + r >> 1;
            if (arr[mid] > arr[mid + 1]) r = mid;
            else l = mid + 1;
        }
        return l;
    }
};
  • Java
class Solution {
    public int peakIndexInMountainArray(int[] arr) {
        int n = arr.length;
        int l = 1, r = n - 2;
        while (l < r) {
            int mid = l + r >> 1;
            if (arr[mid] > arr[mid + 1]) r = mid;
            else l = mid + 1;
        }
        return r;
    }
}
  • Python
class Solution:
    def peakIndexInMountainArray(self, arr: List[int]) -> int:
        n = len(arr)
        l = 0; r = n - 2
        while l < r:
            mid = l + r >> 1
            if arr[mid] > arr[mid + 1]:
                r = mid
            else:
                l = mid + 1
        return l

时空复杂度分析

  • 时间复杂度: O ( l o g ( n ) ) O(log(n)) O(log(n))n为数组长度。

  • 空间复杂度: O ( 1 ) O(1) O(1)

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

题目描述:Leetcode 1011 在 D 天内送达包裹的能力

在这里插入图片描述

分析

  • 本题的考点:二分

  • 因为D最小是1,因此本题一定存在答案,因为我们可以让传送带一次把所有物品运完。

  • 本题采用二分,二分的左端点l应该取数组元素的最大值,否则的话这个物品无法运输过去,右端点r取数组元素之和。

  • 对于每次考察的运输量mid,设days(mid)返回在运输量为mid的情况下需要的运输天数,如果 d a y s ( m i d ) ≤ D days(mid) \le D days(mid)D,则说明mid是一个合法解,右端点更新为mid,否则左端点更新为mid+1

代码

  • C++
class Solution {
public:
    int shipWithinDays(vector<int>& weights, int D) {

        int l = 0, r = 0;
        for (auto w : weights) l = max(l, w), r += w;

        while (l < r) {
            int mid = l + r >> 1;
            if (days(weights, mid) <= D) r = mid;
            else l = mid + 1;
        }
        return r;
    }
	
    // k -> days(k) : 是一个减函数。如果船舶最低载重为k, 需要运输的天数
    int days(vector<int> &w, int k) {

        int res = 0;
        int i = 0, t = k;
        while (i < w.size()) {
            while (i < w.size() && t >= w[i]) t -= w[i++];
            res++;
            t = k;
        }
        return res;
    }
};
  • Java
class Solution {
    public int shipWithinDays(int[] weights, int D) {

        int l = 0, r = 0;
        for (int w : weights) {
            l = Math.max(l, w);
            r += w;
        }
        while (l < r) {
            int mid = l + r >> 1;
            if (days(weights, mid) <= D) r = mid;
            else l = mid + 1;
        }
        return r;
    }
    
    // k -> days(k) : 是一个减函数。如果船舶最低载重为k, 需要运输的天数
    private int days(int[] w, int k) {

        int res = 0;
        int i = 0, t = k;
        while (i < w.length) {
            while (i < w.length && t >= w[i]) t -= w[i++];
            res++;
            t = k;
        }
        return res;
    }
}

时空复杂度分析

  • 时间复杂度: O ( n × l o g ( m ) ) O(n \times log(m)) O(n×log(m))n为物品个数,m为物品重量之和。

  • 空间复杂度: O ( 1 ) O(1) O(1)

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

题目描述:Leetcode 1482 制作 m 束花所需的最少天数

在这里插入图片描述

分析

  • 本题的考点:二分

  • 首先判断无解的情况,即如果所有的花都开了但是还是不够制作花束的话无解,即 m × k > n m \times k > n m×k>n

  • 存在解的话我们可以在区间[l, r]二分出答案,其中l=1r等于数组最大值,每次取中间点mid,判断mid天后是否能够制作m束花,如果可以的话,让r=mid,否则让l=mid+1

  • 判断mid天后是否能够制作m束花:判断数组中大于mid且连续长度等于k的连续子序列个数是否达到m即可。

代码

  • C++
class Solution {
public:
    int minDays(vector<int>& w, int m, int k) {
        
        if (m * k > w.size()) return -1;
        int l = 1, r = 0;  // 二分的区间[l, r]
        for (int x : w) r = max(r, x);
        while (l < r) {
            int mid = l + r >> 1;
            if (check(w, mid, m, k)) r = mid;
            else l = mid +1;
        }
        return r;
    }

    // 经过days天后是否能制作成m束花(每朵连续k个)
    bool check(vector<int> &w, int days, int m, int k) {
        int res = 0, cnt = 0;
        for (int x : w) {
            cnt = x <= days ? cnt + 1 : 0;
            if (cnt == k) res++, cnt = 0;
        }
        return res >= m;
    }
};
  • Java
class Solution {
    public int minDays(int[] w, int m, int k) {

        if (m * k > w.length) return -1;
        int l = 1, r = Arrays.stream(w).max().getAsInt();
        while (l < r) {
            int mid = l + r >> 1;
            if (check(w, mid, m, k)) r = mid;
            else l = mid + 1;
        }
        return l;
    }

    private boolean check(int[] w, int days, int m, int k) {
        int res = 0, cnt = 0;
        for (int x : w) {
            cnt = x <= days ? cnt + 1 : 0;
            if (cnt == k) { res++; cnt = 0; }
        }
        return res >= m;
    }
}

时空复杂度分析

  • 时间复杂度: O ( n × l o g ( n ) ) O(n \times log(n)) O(n×log(n))n为数组长度。

  • 空间复杂度: O ( 1 ) O(1) O(1)

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

题目描述:Leetcode 1723 完成所有工作的最短时间

在这里插入图片描述

分析

  • 本题的考点:二分

  • 本题采用二分的方式求解答案,答案区间在[l, r]之间,其中l表示数组中的最大值,r表示数组总和。每次二分区间中点mid,看mid能否作为答案。

  • 如果mid可以作为答案的,我们可以通过递归回溯将答案找出来,用数组w表示每个工人当前分配的工作量,初始全为0。依次考虑将每个工作分配给每个人,这样就可以将所有可能结果遍历出来,当我们遍历到一个可行解时就可以返回true。考虑剪枝:

    (1)优化搜索顺序:我们应该按照工作时间从大到小排序遍历,这样分支比较少。

    (2)排除等效冗余:无

    (3)可行性剪枝:如果某项工作分配给某个人会超过mid,则应该直接回溯。如果将某项工作分配给某人的话,无法成功,存在两种情况可以直接返回:当此人被分配的工作量为0时可以直接返回(当前一个工人没被分配工作时,下一个工人也不会被分配工作),因为分配给当前工人无法成功,则分配给下一个人同样也无法成功;当此人分配该工作后工作量达到了mid,可以直接返回,因为我们是从大到小枚举每个工作的,假设此工作分配给后面的某个人可以成功,则可以将该工作交换给此人,仍然符合要求,矛盾。

    (4)最优性剪枝:无。

代码

  • C++
class Solution {
public:

    vector<int> jobs;
    int k;

    int minimumTimeRequired(vector<int>& _jobs, int _k) {

        jobs = _jobs;
        k = _k;

        sort(jobs.begin(), jobs.end(), greater<int>());  // 优化搜索顺序
        int l = jobs[0], r = accumulate(jobs.begin(), jobs.end(), 0);
        while (l < r) {
            int mid = l + r >> 1;
            if (check(mid)) r = mid;
            else l = mid + 1;
        }
        return r;
    }

    bool check(int mid) {
        vector<int> w(k, 0);
        return dfs(w, 0, mid);
    }

    // 当前遍历到jobs[u], 工作量限制不超过mid
    bool dfs(vector<int> &w, int u, int mid) {
        if (u == jobs.size()) return true;

        int t = jobs[u];  // 待分配的工作
        for (auto &a : w) {  // 当前考察的某人的工作量为a
            if (a + t <= mid) {
                a += t;
                if (dfs(w, u + 1, mid)) return true;
                a -= t;
            }
            // 此时说明当前考察的人分配jobs[u]不能得到正确答案,则:
            if (a == 0 || a + t == mid) break;  // 两个剪枝
        }
        return false;
    }
};
  • Java
class Solution {

    int[] jobs;
    int k;

    public int minimumTimeRequired(int[] _jobs, int _k) {
        
        jobs = _jobs;
        k = _k;

        Arrays.sort(jobs);
        int l = jobs[jobs.length - 1], r = 0;
        for (int x : jobs) r += x;
        while (l < r) {
            int mid = l + r >> 1;
            if (check(mid)) r = mid;
            else l = mid + 1;
        }
        return l;
    }

    private boolean check(int mid) {
        int[] w = new int[k];
        Arrays.fill(w, 0);
        return dfs(w, jobs.length - 1, mid);
    }

    private boolean dfs(int[] w, int u, int mid) {
        if (u == -1) return true;

        int t = jobs[u];
        for (int i = 0; i < w.length; i++) {
            if (w[i] + t <= mid) {
                w[i] += t;
                if (dfs(w, u - 1, mid)) return true;
                w[i] -= t;
            }
            if (w[i] == 0 || w[i] + t == mid) break;
        }
        return false;
    }
}

时空复杂度分析

  • 时间复杂度: O ( k n ) O(k^n) O(kn)n为数组jobs的长度。

  • 空间复杂度: O ( n ) O(n) O(n)

Leetcode 1818 绝对差值和

题目描述:Leetcode 1818 绝对差值和

在这里插入图片描述

分析

  • 本题的考点:排序、二分

  • 对于考察的每对t = abs(nums1[i]-nums2[i]),我们需要找到一个index,使得abs(nums1[index] - nums2[i])最小,我们记录这个减小的差值,即d = t - abs(nums1[index] - nums2[i]),最终的结果减去这个d即可。

  • 找到和某个数最接近的数可以首先对数组排序然后二分的方式解决。或者将nums1放到有序集合中,调用其中函数即可。

代码

  • C++
class Solution {
public:
    const int mod = 1e9 + 7;

    int minAbsoluteSumDiff(vector<int>& nums1, vector<int>& nums2) {
        int n = nums1.size();
        vector<int> a(nums1);  // a是nums1排好序的结果
        sort(a.begin(), a.end());
        int sum = 0, d = 0;
        for (int i = 0 ; i < n; i++) {
            int t = abs(nums1[i] - nums2[i]);
            sum = (sum + t) % mod;
            // 返回a中大于等于nums2[i]的最小的数
            int index = lower_bound(a.begin(), a.end(), nums2[i]) - a.begin();
            if (index < n) d = max(d, t - abs(a[index] - nums2[i]));
            if (index > 0) d = max(d, t - abs(a[index - 1] - nums2[i]));
        }
        return (sum - d + mod) % mod;
    }
};
  • Java
class Solution {

    static final int mod = (int) (1e9) + 7;

    public int minAbsoluteSumDiff(int[] nums1, int[] nums2) {
        int n = nums1.length;
        TreeSet<Integer> a = new TreeSet<>();
        for (int x : nums1) a.add(x);
        int sum = 0, d = 0;
        for (int i = 0; i < n; i++) {
            int t = Math.abs(nums1[i] - nums2[i]);
            sum = (sum + t) % mod;
            if (a.ceiling(nums2[i]) != null) 
                d = Math.max(d, t - Math.abs(a.ceiling(nums2[i]) - nums2[i]));
            if (a.floor(nums2[i]) != null) 
                d = Math.max(d, t - Math.abs(a.floor(nums2[i]) - nums2[i]));
        }
        return (sum - d + mod) % mod;
    }
}
  • Python
class Solution:
    def minAbsoluteSumDiff(self, nums1: List[int], nums2: List[int]) -> int:
        mod = int(1e9) + 7
        n = len(nums1)
        a = nums1.copy()
        a.sort()
        sum, d = 0, 0
        for i in range(n):
            t = abs(nums1[i] - nums2[i])
            sum = (sum + t) % mod
            l, r = 0, n
            while l < r:
                mid = l + r >> 1
                if a[mid] >= nums2[i]:
                    r = mid
                else:
                    l = mid + 1
            if r < n:
                d = max(d, t - abs(a[r] - nums2[i]))
            if r > 0:
                d = max(d, t - abs(a[r - 1] - nums2[i]))
        return (sum - d + mod) % mod

时空复杂度分析

  • 时间复杂度: O ( n × l o g ( n ) ) O(n \times log(n)) O(n×log(n))n为数组长度。

  • 空间复杂度: O ( n ) O(n) O(n)

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值