二分法的妙用

基础

首先了解C++中存在于头文件<algorithm>两个库函数:lower_bound和upper_bound
lower_bound(start, end, val)返回在[start, end)中找到第一个大于等于val的位置
upper_bound(start, end, val)返回在[start, end)中找到第一个大于val的位置

lower_bound及upper_bound的简单实现

只有一个等号的区别,以lower_bound为例,当v[mid]小于x时,left肯定要取mid+1,否则,right=mid,right一定符合, 让left不断逼近right

#include <iostream>
#include <vector>
using namespace std;
int lower_bound(vector<int>& v, int left, int right, int x){
    while(left < right){
        int mid = (left + right) / 2;
        if(v[mid] < x) left = mid + 1;
        else right = mid;
    }
    return left;
}

int upper_bound(vector<int>& v, int left, int right, int x){
    while(left < right){
        int mid = (left + right) / 2;
        if(v[mid] <= x) left = mid + 1;
        else right = mid;
    }
    return left;
}
int main() {
    vector<int> v{1,2,3,4,5,6,6,7,8,9};
    cout << lower_bound(v, 0, v.size(), 6);
    cout << upper_bound(v, 0, v.size(), 6);
    return 0;
}

题目

Split Array Largest Sum

题目链接:410. Split Array Largest Sum
题目解法:Solution with Greedy Algorithm and Binary Search
给定一个非负整数数组和一个整数 m,你需要将这个数组分成 m 个非空的连续子数组。设计一个算法使得这 m 个子数组各自和的最大值最小
由题意,我们可以知道m必定在[max(max(nums), sum(nums)/m), sum(nums)]之间,我们可以先猜一个中间值max,并构造子数组使得每个子数组之和都不超过max,如果我们从小到大取max,得到一个结果组成数组B,则B中一定是[F,F,F,T,T]这种的,越大更容易为True,这种情况我们可以使用二分法找到第一个T,这个T对应的max就是最小的最大值

class Solution {
public:
    bool doable(vector<int>& nums, long long max, int cut){
        long acc = 0;
        for (int &num: nums) {
            if(num > max) return false;
            else if(acc + num <= max) acc += num;
            else {
                cut--;
                acc = num;
                if(cut < 0) return false;
            }
        }
        return true;
    }
    int splitArray(vector<int>& nums, int m) {
        long long left = 0, right = 0;
        for (int &num : nums) {
            left = max(left, (long long)num);
            right += num;
        }
        
        while (left < right) {
            long long mid = left + (right - left) / 2;
            if (doable(nums, mid, m - 1)) right = mid;
            else left = mid + 1;
        }
        return left;
    }
};

DP解法:dp方程很简单dp[i][j]表示将索引到i的(包括i)数组分解为j个组的最小最大和递推方程为dp[i][j] = min(max(dp[k][j-1], sum[k+1,i]) ),很慢


class Solution {
public:
    int splitArray(vector<int>& nums, int m) {
        int n = nums.size();
        vector<long long> p(n+1);
        p[0] = 0;
        
        for(long i = 1; i <= n; i++) p[i] = p[i-1] + nums[i-1];
        // sum[i, j] = p[j+1] - p[i];
        
        
        vector<vector<long long>> dp(n + 1, vector<long long>(n+1, INT_MAX));
        int MAX = 0;
        for(int i = 0; i < n; i++) {
            MAX = max(nums[i], MAX);
            dp[i][1] = p[i+1];
            dp[i][i+1] = MAX;
        }
        for(int i = 0; i < n; i++){
            for(int j = 2; j <= m; j++){
                for(int k = j-2; k < i; k++){
                    dp[i][j] = min(max(dp[k][j-1], p[i+1]- p[k+1]), dp[i][j]);
                }
            }
        }
        
        return dp[n-1][m];
    }
};

二维变一维


class Solution {
public:
    int splitArray(vector<int>& nums, int m) {
        int n = nums.size();
        vector<long long> p(n+1);
        p[0] = 0;
        
        for(long i = 1; i <= n; i++) p[i] = p[i-1] + nums[i-1];
        // sum[i, j] = p[j+1] - p[i];
        
        
        vector<long long> dp(n + 1, INT_MAX);
        int MAX = 0;
        for(int i = 0; i < n; i++) {
            
           
            dp[i] = p[i+1];
        }
        
        for(int j = 2; j <= m; j++){
            for(int i = n - 1; i >= 0; i--){
                dp[i] = INT_MAX;
                for(int k = j-2; k < i; k++){
                    dp[i] = min(max(dp[k], p[i+1]- p[k+1]), dp[i]);
                    
                }  
               
            }
        }
        
        
        
        return dp[n-1];
    }
};

Maximum Length of Repeated Subarray

题目链接:718. Maximum Length of Repeated Subarray
题目解法:Github Issue 324. Maximum Length of Repeated Subarray
这题本身很简单,DP的话和LCS差不多,也可以用偏移做,时间复杂度是\(O(n^2)\)
但是值得一提的是二分搜索的解法Binary Search + Rabin Karp + Hash Table, O(N log N), Beats 100%
大致思路是计算每个[0,i]串的hash值,然后计算长度为m的串hash值,判断两个数组中的串的hash是否有重合,如果有重合,增大l=m+1, 如果没有r=m-1,这种做法容易溢出,但思路很好


class Solution {
public:
    int findLength(vector<int>& A, vector<int>& B) {
        const int P = 19941229; //Rabin-Karp
        
        vector<int> hash1(A.size() + 1, 0), hash2(B.size() + 1, 0), p(max(A.size(), B.size()) + 1);
        int len = max(A.size(), B.size());
        p[0] = 1;
        for (int i = 1; i <= len; i++)
            p[i] = p[i - 1] * P;
        for (int i = 1; i <= A.size(); i++)
            hash1[i] = hash1[i - 1] + A[i - 1] * p[i];
        for (int i = 1; i <= B.size(); i++)
            hash2[i] = hash2[i - 1] + B[i - 1] * p[i];
        int l = 1, r = min(A.size(), B.size());
        while (l <= r) { //Binary search the answer
            int m = (l + r) / 2;
            unordered_set<int> hash;
            bool ok = false;
            for (int i = 1; i + m - 1 <= A.size(); i++) //Calculate Rabin-Karp hash for all substring s for the first string of length m, insert into hash table
                hash.insert((hash1[i + m - 1] - hash1[i - 1]) * p[len - (i + m - 1)]);
            for (int i = 1; i + m - 1 <= B.size(); i++){ //Calculate Rabin-Karp hash for the second string, query the hash table
                if (hash.count((hash2[i + m - 1] - hash2[i - 1]) * p[len - (i + m - 1)])) {
                    ok = true;
                    break;
                }
            }
            if (ok)
                l = m + 1;
            else
                r = m - 1;
        }
        return r;
    }
};

Dungeon Game

题目链接:174. Dungeon Game
题目解答:A Simple C++ Solution using Binary Search
当然这道题也是一道经典的DP题目采用逆向思维从后往前进行DP,二分法也是不错的方法

class Solution {
public:
    int calculateMinimumHP(vector<vector<int>>& dungeon) {
        int left = 1;
        int right = 1;
        int M = dungeon.size();
        int N = dungeon[0].size();
        for (int i = 0; i < M; i++) {
            int val = dungeon[i][0];
            if (val < 0) right += (-val);
        }
        for (int i = 0; i < N; i++) {
            int val = dungeon[M-1][i];
            if (val < 0) right += (-val);
        }

        int dead = INT_MIN / 3;
        while(left < right){
            int mid = (right - left) / 2 + left;
            vector<vector<int>> h(M + 1, vector<int>(N+1, dead));
            h[0][1] = h[1][0] = mid;
            for(int i = 1; i <= M; i++){
                for(int j = 1; j <= N; j++){
                   
                    h[i][j] = max(h[i-1][j], h[i][j-1]) + dungeon[i-1][j-1];
                   
                    if(h[i][j] < 1){
                        h[i][j] = dead;
                    }
                }
            }
            
            if(h[M][N] > 0){
                right = mid;
            } else {
                left = mid + 1;
            }
        }
        
        return left;
    }
};

参考

posted @ 2019-07-23 17:08  qbits  阅读(375)  评论(0编辑  收藏  举报