【二分练习集】(二分查找、二分答案)(5549字)

目录

板块一:二分查找 

第一题:cf706B

查找第一个合法的答案:

查找最后一个合法的答案:

板块二:二分答案: 

第一题:洛谷P2440

第二题:洛谷P1182

第三题:洛谷P1873

第四题:洛谷P2678

第五题:洛谷P3853


板块一:二分查找 

第一题:cf706B

代码:

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int a[N], n;
int bs(int x) {
	int l = 0, r = n, mid;
	while (l < r) {
		mid = l + r >> 1;
		if (a[mid] > x) {
			r = mid;
		} else {
			l = mid + 1;
		}
	}
	return l;
}
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	cin >> n;
	for (int i = 0; i < n; i++) {
		cin >> a[i];
	}
	sort(a, a + n);
	int m;
	cin >> m;
	while (m--) {
		int x;
		cin >> x;
		cout << bs(x) << '\n';
	}
	return 0;
}

复习二分查找模板:天下无二唯二模板

查找第一个合法的答案:

r一定是合法的,l是可能是合法的。l-1必然是非法的,而且不断中间偏左地取,因为除二会把奇数损失1,直到l是第一个合法的数,然后r逐渐逼近,直到相邻然后r取到l,出循环。

int bs(int x) {//binary search
	int l = 0, r = n, mid;
	while (l < r) {//小于
		mid = l + r >> 1;
		if (a[mid] > x) {//这里的if语句写的是正确的条件
			r = mid;//可能直接踩到答案
		} else {
			l = mid + 1;//mid处肯定不是答案
		}
	}//最终一定可以相等的!!!
	return l;
}

查找最后一个合法的答案:

这里是取中间偏右,这和最后的状态(相邻)有关。l一定是合法的,r可能是合法的,r+1一定是非法的,直到相邻,l是合法的,r是最后一个合法的,然后mid一下出循环,r = l;

	while (l < r)
    {
        int mid = l + r + 1 >> 1;	//(l+r+1)/2
        if (check(mid))  l = mid;
        else r = mid - 1;
    }

 上面那个模板用的比较多。两个都很重要。

两者的区别在于符合条件的数据再左还是在右,例如,如果说我们要找的是第一个大于xxx的数,而且是一个递增区间,那么答案区间在右侧,右侧才有大于xxx的数,check的时候r = mid,直到最后一个符合,一个不符合,例如l = 4, r = 5此时,r一定是符合的,mid = 4不符然后右移,出循环。

下面那个答案区间在左,移动l,直到l = 4, r = 5, mid = 5不符左移。这个求的是最后一个符合条件的值。

不过上面的好。

杂项知识点:getline函数就算是空行也是可以读取的,可以使用str == ""判断来跳过空行。

板块二:二分答案: 

废板子:(不必要, 建议跳过)

while (l + 1 < r) {
		mid = l + r >> 1;
		if (check(mid)) {
			l = mid;
		} else {
			r = mid;
		}
	}

两个微妙的地方:一个是是l要来加一,另一个是mid不能加一。

两者都是为了避免超时而设计。

1234567891011121314151617181920

开始是足够大的r和足够小的l,然后开始猜答案,答案有一个区间,存在且是唯一的(有某种单调性)。首先,l和r不是答案。第一次操作,区间折半,一直操作的过程中,r一定不是答案,而l是可能的答案。l从零开始,l = 合法,r = 非法, 如果 l + 1 = r 的话,说明l是最大的合法答案, 如果l + 1 < r  我们可以知道 l 合法 l + 1 ? …… r - 1 ? r 非法,中间未可知。mid肯定在l和r之间,判断合法和非法,区间减小,每次取中间偏左一些,直到长度为三的区间只需判断,中间一个是否合法就行了。建议对着上面的表格想象一下。

为什么l要加1呢?因为如果l不加一的话,区间缩小到相邻的时候,左边合法右边非法,会导致死循环。

其实也是因为mid没有加一的缘故,因为我们直到这个点是非法的话,下一个点就可能是非法的也可能是合法的,和上面的分析不同。

第一题:洛谷P2440

一样的题目:洛谷P1969

说了那么多其实并没有用。

突然明白其实二分模板只有两个,一个寻找第一个和寻找第最后一个合法的答案,其实用板子二就行了:

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 1e6 + 5;
ll n, k, a[N];
bool check(ll x) {
	ll ans = 0;
	for (int i = 1; i <= n; i++) {
		ans += a[i] / x;
	}
	return ans >= k;
}
int main() {
	cin >> n >> k;
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
	}
	ll l = 0, r = 1e8 + 1;
	ll mid;
while (l < r) {
		mid = l + r + 1 >> 1;
		if (check(mid)) {
			l = mid;
		} else {
			r = mid - 1;
		}
	}
	cout << l << '\n';
	return 0;
}

第二题:洛谷P1182

批注:模板确实只有两个,但是,但是,但是,注意你的check条件,上下两行的位置可能不同,不能盲目的套模板。

分析一下:check函数,这里的check函数是来求出给定的长度的最小值,当区间和不超过x的时候至少要几段。简单的看一下,第一段毫无争议,能放就放是当前的最优解,满出来之后我们不得不放到第二段,注意,对于当前的最小放法不是唯一的,我们可以将区间1的最后一个放到区间2的第一个,但是,这并没有改变区间当前的区间数目,所以根据贪心的想法,能放就放,不能放就新开区间可以求出每处的最小区间数。

二分答案就是去猜答案,答案区间找对,一半一半猜。题目给出最小区间数,要求将区间分割的最大值的最小值。首先,如果这个值很小,则区间数很多,不对,如果这个值很大,区间数为1,也不对,照道理两边找均可。不过可能要改模板,为了不改模板,我们把数很小区间数大于所给的区间数的当成非法的,数较大的区间数小于等于所给区间的看成是合法的。这样我们二分要求的是第一个合法的值(必然存在,必然相等,如果严格小于只需分割一些区间即可),用模板一。

下面是代码:

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int a[N], n, m;
int check(int x) {
	int sum = 0, num = 1;
	for (int i = 1; i <= n; i++) {
		sum += a[i];
		if (sum > x) {
			sum = a[i];
			num++;
		}
	}
	return num <= m;
}
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	cin >> n >> m;
	int l = 0;
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
		l = max(l, a[i]);
	}
	int r = 1e9 + 1, mid;
	while (l < r) {
		mid = l + r >> 1;
		if (check(mid)) {
			r = mid;
		} else {
			l = mid + 1;
		}
	}
	cout << l << '\n';
	return 0;
}

如果,把大于等于当成是合法的话,就麻烦了,必须改模板,这样边界条件又会让人头大。虽然左边都是合法的,但是你又不能求最后一个合法的,这样求出来的,不一定是最小的。只能说是能分出这个区间数里面最大的。

合法合法合法且区间数相符(答案)合法且区间数相符合法且区间数相符(你的答案)非法非法非法非法非法

这是单增的。

求出来的是黑色加粗的,但是答案是红色的。

上面那个就不一样。

非法非法非法非法非法合法且区间数相符(答案)合法且区间数相符合法且区间数相符合法合法

这就刚刚好。

求最小的用模板一,设计check使得合法在右侧。

第三题:洛谷P1873

同样是构造二分答案,这里是取求最高的高度,选择模板二,求最后一个合法的的值,相应地我们要构造出,左边合法的样子,如上面的第一个表格。有时候两个都可以,合法的值只能有一个,但是很多情况是不行的,这里的话好像是可以的,因为锯子升高一定减少,这是严格单调的。实际上也是不行的,因为无法保证小于等于可以取到等号。

归纳:一般来说是单调增的答案区间,如果要求最最小值,也就是第一个合法的答案,用板子1,如果要求最大值,也就是最后一个合法的答案,用板子2。为此要相应地构造check条件,使得板子1从右开始合法,板子2从左开始合法。关于边界条件的记忆,在搜索的过程中,一端开始合法,最后也是合法,过程中一直合法,而一端开始不是合法的,那么就要加减一变得可能合法,而且,这一端停止的位置就是答案,mid求法的设置是偏向这个方位的,从而使得另一端可以与这端重合。

代码:

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
int a[N], n, m;
bool check(int x) {
	long long sum = 0;
	for (int i = 1; i <= n; i++) {
		if (a[i] > x) {
			sum += a[i] - x;
		}
	}
	return sum >= m;
}
int main() {
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
	}
	int l = 0, r = 2e9, mid;
	while (l < r) {
		mid = l + r + 1 >> 1;
		if (check(mid)) {
			l = mid;
		} else {
			r = mid - 1;
		}
	}
	cout << l << '\n';
	return 0;
} 

第四题:洛谷P2678

跳石头这道题首先,我们需要求的是最大值,所以选择模板2,为此我们需要构造check函数,使得从左开始合法,找到最后一个合法的。首先,这里最短跳跃距离必然存在,而且是整数。单调性,当最短跳跃距离很小的时候,一个石头都不用搬,num = 0,照道理不应该取到这么小,因为这个根本不存在,但是不影响答案,如果有必要的话可以取两块石头间的最小距离。最大距离是起点到终点,取更大也不影响答案,在存在的情况下,最短距离越长要般的石头越多,所以num<=m,是合法的。如何判断num就是如果距离比最短的要小就说明一定要搬,否则就不用搬。有一个看起来不那么重要的问题,就是所有可能出现的距离的集合并不是连续的,一堆散点,如何能够保证最后取来的答案是存在的,或者说刚好有一个位置可以取到这个值。因为这是最后一个合法的值,它的下一个值必然是非法的,也就是无论这么搬都不可能实现,说明这个值刚刚好是搬动m个石头的极限值,通过搬走石头得到的最大的最小距离。

大功告成。

代码如下:

#include <bits/stdc++.h>
using namespace std;
const int N = 50005;
int a[N], n, m;
bool check(int x) {
	int num = 0, now = 0;
	for (int i = 1; i <= n + 1; i++) {
		if (a[i] - a[now] < x) {
			num++;
		} else {
			now = i;
		}
	}
	return num <= m;
}
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	int L;
	cin >> L >> n >> m;
	a[0] = 0;
	a[n + 1] = L; 
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
	}
	int l = 0, r = L + 1, mid;
	while (l < r) {
		mid = l + r + 1 >> 1;
		if (check(mid)) {
			l = mid;
		} else {
			r = mid - 1;
		}
	}
	cout << l << '\n';
	return 0;
} 

第五题:洛谷P3853

    题目分析:题目要求最小值用模板1,求第一个合法的答案,为此我们需要构造相应的check函数,使得在答案区间内部左侧为非法的,右侧为为合法的,右侧的数较小,空旷程度大,所需的补充的路标数目少,所以出口条件设置为num <= k。
    核心代码:如何求出最小的路灯放置数,这里绝对不可能用n方的方法,既然已经用了二分想必这里肯定只需遍历一边,不然这个数量级可就没办法了。计算两者之间的距离,然后整除空旷指数,如果能够完全整除减一,如果不能完全整除就不减一。而且注意,无论是否放置都要改变i,这和跳石头有些不一样。
    关于含义:这里求出的num是不得不放置的数目,如果说这小于等于k的话我们定义为合法,为了配合二分模板,下面是表格。
    |非法| 非法 | 合法m也符合(答案) |合法,m也符合  |合法,m不符  |  合法,m不符|合法,m不符  |合法,m不符  | 
    我们要找的是第一个合法的即可。因为这刚好是这个摆放数的空旷指数的极限,但凡小一点就不够用。
    等号是一定取到的,有兴趣可以看看。
    坑点:l不能取0会下面的测试点会RE,除零错误(模零错误)可还行。

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int a[N], n, k;
bool check(int x) {
    int now = 1, num = 0;
    for (int i = 2; i <= n; i++) {
        int d = a[i] - a[now];
        if (d > x) {
            num += d / x - 1 + (d % x != 0);
        }
        now = i;
    }
    return num <= k; 
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    int L;
    cin >> L >> n >> k;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    int l = 1, r = 1e7 + 5, mid;
    while (l < r) {
        mid = l + r >> 1;
        if (check(mid)) {
            r = mid;
        } else {
            l = mid + 1;
        }
    }
    cout << l << '\n'; 
    return 0;
}


 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值