先来一道二分的板子题:
P2249 【深基13.例1】查找 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
二分查找的代码如下:
#include<cstdio>
#include<algorithm>
#include<iostream>
using namespace std;
int a[1002000];
int findpos(int a[], int l, int r, int k)
{
if (l == r)
{
if (a[l] == k)
return l;
else
return -1;/*最后位置的数与待查询数不相等,说明数列里没有此数*/
}
int mid = l + (r-l) / 2;
if (k <= a[mid])
return findpos(a, l, mid, k);/*在左边区域找*/
else
return findpos(a, mid + 1, r, k);/*在右边区域找*/
}
int main()
{
int n, m, k;
cin >> n >> m;
for (int i = 1; i <= n; i++)
scanf("%d", &a[i]);
while (m--)
{
cin >> k;
cout << findpos(a, 1, n, k) << " ";
}
return 0;
}
如果不懂的话,可以先去学学。
下面都是一些练手的题目,都是黄题,题单链接放在下面:
【算法1-6】二分查找与二分答案 - 题单 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
先看看第一道题吧:A-B数对
P1102 A-B 数对 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
这道题主要的学习地方在于思路的转化,对数据的处理,以前写过笔记,直接贴上来:
思路分析
可以将问题转化为枚举A,那么问题就变成了统计数列中B+C出现了多少次。 可以将原来的输入数列进行排序,那么B+C也就会对应这段数列中的连续一段,我们只需要找到第一个B+C(不妨记录为n)和最后一个B+C(不妨记录为m)即可,统计出来的数目即是m-n+1
原本可以用二分查找的基础模板写,但是我们这里介绍一种新方法:
STL中的lower_bound()与upper_bound()
使用方法:
1)lower_bound(begin,end,val):
在值有序的数组连续地址【begin,end)中找到第一个的位置并返回其地址,使得val插入在这个位置的前面,整个数组仍然有序。 2)upper_bound(begin,end,val):
在值有序的数组连续地址【begin,end)中找到最后一个的位置并返回其地址,使得val插入在这个位置的前面,整个数组仍然有序。
简单总结一下:lower_bound()可以找到某个数第一次出现的位置 upper_bound()可以找到某个数最后一次出现的位置的后面
回过头来再看这道题,那么我们就可以使用upper_bound()-lower_bound()来求出B+C在数列中出现的数目。
贴上代码:
#include<iostream>
#include<algorithm>
#define MAXN 200010
using namespace std;
using ll = long long;
ll a[MAXN];
int n, c;
int main() {
cin >> n >> c;
for (int i = 0; i < n; i++) {
cin >> a[i];
}
sort(a, a + n);
ll cnt = 0;
for (int i = 0; i < n; i++) {
cnt += upper_bound(a, a + n, a[i] + c) - lower_bound(a, a + n, a[i] + c);
}
cout << cnt;
return 0;
}
这里还有补充的解法:
思路:
寻找lower_bound()与upper_bound()的位置,我们可以发现随着被查询的a【i】+c的增大,lower_bound与upper_bound的位置也在向后移动,可以把这两个位置维护出来,也就是随着a【i】+c的增大向后移动,因为这两个位置移动的数目是常数级别,所以这个算法的时间复杂度只有O(n),代码如下
#include<algorithm>
#define MAXN 200010
using namespace std;
using ll = long long;
ll a[MAXN];
int n, c;
int main() {
cin >> n >> c;
for (int i = 0; i < n; i++) {
cin >> a[i];
}
sort(a, a + n);
int l = 0, r = 0;
ll sum = 0;
for (int i = 0; i < n; i++) {
while (a[l] < a[i] - c && l < n) {
l++;//l相当于lower_bound(),可以找到第一个a[l]>=a[i]+c的元素
}
while (a[r] <= a[i] - c && r < n) {
r++;//r相当于upper_bound(),可以找到第一个a[r]>a[i]+c的元素
}
if (a[i] - a[l] == c) {
sum += r - l;
}
}
cout << sum << endl;
return 0;
}
接下来先放这两道题,因为比较相似:
P2678 [NOIP2015 提高组] 跳石头 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
P3853 [TJOI2007] 路标设置 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
讲讲跳石头吧:
看完题意,我们发现还是熟悉的最小值最大的问题,那我们就很容易想到二分。
那是二分什么呢:二分的是最小值,最大化的任务落在二分上,那么找合法解的任务就落在check函数上。
我们不妨这样想:既然我们是要让check(x)中的x为最小值,也就是x是相邻两块石头的最小距离,那么很显然,如果在扫描整个序列的过程中,发现不满足的情况,也就是有相邻两块石头的距离小于x,那么我们就得把它给搬走,反之,我们就继续扫描接下来相邻的两个。
那么我们的check函数要返回什么值呢?我们可以发现可以搬走的石头个数这个条件我们还没有用到,所以我们抓住这个条件,如果搬运的石头小于题目要求的,那么它是一个可行解,我们可以继续找最大的。相反,我们这个解的数值太大,我们只能找更小的。
代码如下:
#include<iostream>
using namespace std;
const int N = 5e4 + 10;
int len, n, m, d;
int a[N];
bool check(int);
int main() {
cin >> len >> n >> m;
a[0] = 0;
a[n + 1] = len;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
int l = 1, r = len, ans, mid;
while (l <= r) {
int mid = l + r >> 1;
if (check(mid)) {
ans = mid, l = mid + 1;
}
else {
r = mid - 1;
}
}
cout << ans << endl;
return 0;
}
bool check(int d) {
int k = 0, last = 0;
for (int i = 1; i <= n + 1; i++) {
if (a[i] - last < d) {
k++;
}
else {
last = a[i];
}
}
return k <= m;
}
另外一道路标设置的思路跟这道很像,可以自己尝试做做看,代码我也贴在下面:
#include<iostream>
#include<algorithm>
using namespace std;
using ll = long long;
ll l, n, k;
ll a[10000000];
bool check(int m) {
int y = k;
ll size = 0;
for (int i = 2; i <= n; i++) {
if (y < 0) {
break;
}
if (a[i] - size <= m) {
size = a[i];
}
else {
size += m;
i--;//特别注意这里要i--,因为还需要再判断这个新放置的和接下来的下一个是否<=m
y--;
}
}
if (y >= 0) {
return 1;
}
return false;
}
int main() {
cin >> l >> n >> k;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
int left = 0, right = l;
int ans = 0;
while (left <= right) {
int mid = left + (right - left) / 2;
if (check(mid)) {
ans = mid;
right = mid-1;
}
else {
left = mid+1;
}
}
cout << ans;
return 0;
}
P1873 [COCI 2011/2012 #5] EKO / 砍树 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
P2440 木材加工 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
再看看这两道题目: 重点还是先讲一下砍树,毕竟思路大同小异。
分析题目,我们可以发现:满足单调性,所以我们可以使用二分答案。
我们二分的对象是高度,注意题目的要求,获得的数目重量,所以我们可以先把它确定为check(x)函数的返回值。 看看砍树得到的木材是否>=题目要求的,如果是,那么我们的高度低了,可以再高一点,反之,高度得下降。
再继续分析:如何统计得到的木材,这就很简单了,一个循环就可实现,枚举每一棵树,他们的高度与x的差值的和就是我们能得到的树木总量。代码如下:
#include<iostream>
#define MAXN 1000010
using namespace std;
using ll = long long;
ll a[MAXN], n, m;
bool judge(int h) {
ll cnt = 0;
for (int i = 0; i < n; i++) {
if (a[i] > h) {
cnt += a[i] - h;
}
}
return cnt >= m;
}
int main() {
cin >> n >> m;
for (int i = 0; i < n; i++) {
cin >> a[i];
}
int l = 0, r = 1e9, ans, mid;
while (l <= r) {
mid = l + (r - l) / 2;
if (judge(mid)) {
ans = mid, l = mid + 1;
}
else
r = mid - 1;
}
cout << ans;
return 0;
}
接下来就可以拿木材加工练练手了,代码还是贴在下面:
#include<iostream>
#include<algorithm>
#include<math.h>
using namespace std;
using ll = long long;
ll n, k;
ll h[100000100];
bool check(ll d) {
ll ans = 0;
for (int i = 1; i <= n; i++) {
ans += h[i] / d;
}
return ans >= k;
}
int main() {
cin >> n >> k;
for (ll i = 1; i <= n; i++) {
cin >> h[i];
}
ll l = 0;
ll r = 1e8 + 1;
while (l+1 < r) {
int mid =l + (r - l) / 2;
if (check(mid)) {
l = mid;
}
else r = mid;
}
cout << l;
}
思路大同小异,没什么好说的。
就先这样吧。