下面,我将从leetcode
的一种高频题型来讲述一下关于我对二分搜索的一种进阶用法。从刷题以来,这种题型我经常遇见,由于经常遇见,所以说看见这种题我基本上就是直接ac了,不用怀疑,面对这种题型,完全不用动脑子,一套框架,直接带走,当然前提是你得知道这道题用这种方法可解才行,那么如何知道?下面我会告诉你们。
题目描述
这是一道来自leetcode
的周赛第三题。
题目描述,可以自行点击链接查看,这里不做多的赘述。
如果是第一次做这种题型的人,很容易写出O(n*m)时间复杂度的代码,也就是暴力解法,我贴出代码大家体会一下。
class Solution {
public:
int minDays(vector<int>& bloomDay, int m, int k) {
if (m * k > bloomDay.size()) return -1;
int day = -1;
int cnt = 0;
while (cnt < m) {
int bloom = 0;
cnt = 0;
day++;
for (int i = 0; i < bloomDay.size(); ++i) {
if (bloomDay[i] <= day) {
bloom++;
if (bloom == k) {
cnt++;
bloom = 0;
}
} else {
bloom = 0;
}
}
cout << cnt << " " << day << endl;
}
return day;
}
};
所谓的暴力解法,也就是我们遍历出每一种可能,直到某一种可能满足条件,我们就返回这个可能。
在这里,我们依次将需要等待的天数从day
= 0开始递增,然后循环数组,看一下等待day
天后可以制作多少花,如果满足条件就可以退出循环返回答案了。
在这里 n 表示 bloomDay
的最大天数,m就表示bloomDay
的数组长度,所以时间复杂度不言而喻。
题目明确规范了数据范围:
bloomDay.length
== n- 1 <= n <= 10^5
- 1 <=
bloomDay[i]
<= 10^9 - 1 <= m <= 10^6
- 1 <= k <= n
难点就在于 n 可能会很大,会超时。
比如题中的case 4:
bloomDay = [1000000000,1000000000], m = 1, k = 1
在这里的话,我们的day会遍历到1000000000
,很明显,超时了,暴力法行不通。
接下来看二分搜索的进阶用法。
二分check法
为什么会叫二分check法?看完后,你就会明白。
对于这种题,其实官方很明显的提示我们该怎么做,接下来你就需要总结出经验,形成一种feel
,下次看见了就可以直接秒了。
一般而言,leetcode
的提示都会在数据范围里找得到。
我这里举几个case
:
- 数组里的值在 0 … n 的范围内,n表示数组长度
- 数组里的数据不会重复
- 数组长度 大于 0 小于 多少多少
第一个提示官方在告诉我们可以用 位运算
或者 哈希数组
来做
第二个提示代表我们不用担心算出来的答案会重复,比如说用回溯法 DFS
的时候会考虑结果集重复问题。
第三个提示就是这道题的提示了,很明显,看见这种提示我们可以考虑用暴力法了,那为什么不能用暴力法呢?
官方还提示了一点,也就是 1 <= bloomDay[i] <= 10^9。
其实我们应该可以想出来该怎么做了,优化 day
的遍历,其实答案不会超过bloomDay
数组的最大值,所以我们不用从 **0 -> max(bloomDay) **依次遍历,只需要二分来找就是了,perfect
。
思路:
- 二分搜索
0.... max(bloomDay)
,每次搜索时检查(check)数组 - 检查结果做对比,不断的缩小搜索范围
思路得出后,下面我总结出一套对于二分check法的框架,以后对于这种题直接秒就行了,当然你得知其所以然。
伪代码框架:
int binar_search_check(int[] arr) {
int right = max(arr); // 找到right值,也就是数组最大值
int left = 0;
// 下面就是二分搜索的框架了
while (left < right) {
int mid = left + (right - left) / 2;
int check = get_check(arr, mid);
if (条件) {
left = mid + 1;
} else if (条件) {
right = mid;
}
}
return left;
}
int get_check(int[] arr, int k) {
int ans = 0;
for (遍历数组) {
if (条件) {
ans++;
} else {
处理代码
}
}
return ans;
}
然后,我们处理一下框架,让这套框架成为题解。
添加一点点细节
class Solution {
public:
int minDays(vector<int>& bloomDay, int m, int k) {
if (m * k > bloomDay.size()) {
return -1;
}
int left = 0;
int right = 0;
for (int v : bloomDay) {
right = max(v, right);
}
// 二分搜索
while (left < right) {
int mid = left + (right - left) / 2;
int cnt = getCount(bloomDay, mid, k);
if (cnt < m) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
}
// check (其实就是遍历一边数组,保存满足条件的)
int getCount(vector<int>& bloomDay, int day, int k) {
int ans = 0;
int cnt = 0;
for (int i = 0; i < bloomDay.size(); ++i) {
if (bloomDay[i] <= day) {
cnt++;
} else {
cnt = 0;
}
if (cnt == k) {
cnt = 0;
ans++;
}
}
return ans;
}
};
为了方便大家理解,我找了一下这种题型的题,感兴趣的都可做一下,切实感受这种解法的奥妙。
其他类型题目:
1095我曾在git上提交过题解,有兴趣的可以看一下我的理解:
完结撒花
其实也没啥好说的,也就是继续捞一手我的微信公众号。
- 微信公众号:NonCover