正文索引 [隐藏]

传送门

思路

题目是很标准的“找到最大或最小”,也就是说假设正解为ans,(-∞,ans)范围内都无法满足题设条件,而[ans,+∞)都能满足要求,且ans是满足要求的值中最小的那个,这样ans就是一个“合法与非法”的分割点。对于形如这样的题设,就可以对答案序列进行二分来在一个较优的复杂度内枚举到答案。

就本体内容具体地看,天数显然就是我们二分的目标。枚举的左边界是0,右边界是bloomDay中最大值,这是显然地。

每次获取一个mid,然后去检查这个天数mid能否满足要求。如果可以的话,说明mid落在了[ans,+∞)范围内,令r=mid,继续;

如果不可以满足要求,那么说明mid落在了(-∞,ans) 范围内,令l=mid+1,缩小范围继续查找。

值得一提的是,与传统的二分不同,我们在缩小右边界时并非是r=mid-1,这是因为答案区间[ans,+∞)是左闭右开的,是可能存在r==ans的,如果这时还去-1,就会导致答案错误。

代码

class Solution {
public:
    int minDays(vector<int>& bloomDay, int m, int k) {
        const int n = bloomDay.size();
        if (m * k > n) return -1;

        vector<bool> flag(n);
        int l = 0, r = *max_element(bloomDay.begin(), bloomDay.end());
        while(l < r) {
            int mid = (l + r) / 2;
            if (check(bloomDay, mid, flag, m, k)) {
                r = mid;
            } else {
                l = mid + 1;
            }
        }

        // 如果二分终点值r都不合法,就找不到合法答案
        return check(bloomDay, r, flag, m, k) ? r : -1;
    }

    bool check(vector<int>& bld, int mid, vector<bool>& f, int m, int k) {
        int n = bld.size();

        // 初始化flag数组
        // 标记已经盛开的花朵
        for (int i = 0; i < n; i++) {
            f[i] = bld[i] <= mid;
        }

        // O(n)枚举
        int cnt = 0;
        for (int i = 0; i < n && cnt < m; ) {
            // 如果花朵i已经盛开
            if (f[i]) {
                int cur = 1, j = i;
                // 去枚举相邻的k朵花是不是也盛开了
                while (cur < k && j + 1 < n && f[j + 1]) {
                    j++;
                    cur++;
                }
                // 如果都盛开了,计数++
                if (cur == k) cnt++;
                // 从这相邻的一组花再后一个位置继续枚举
                i = j + 1;
            } else {
                // 如果没开就直接跳
                i++;
            }
        }

        return cnt >= m;
    }
};