学习笔记(一) 二分法

整数二分规范

介于二分取左右边界的不确定性和容易陷入死循环的问题,笔者这里自己总结一点学习小技巧

  1. 先写出未完善二分模板
int fun(int n){
	int l=0,r=n;
	while(l<r){
		int mid=(l+r)>>1;
		if(check(mid)) l=mid;//或者r=mid
		else r=mid-1;//或者l=mid+1
	}
	return l;
}

2.通过题干条件完善check以及后面的mid的转移

check内部根据题干完成,后边根据要求看l=mid还是r=mid,这些完成后else后面的语句自然就完成了。

3.看if或者else后边的语句,根据其压缩到哪个边界选择mid是左中位数还是右中位数

首先,这里讲解左右中位数,取小运算左中位数,反之取大运算是右中位数。对于 (l+r)/2 这个语句在 l+r 为正数的时候才是取左中位数,为负数时是右中位数,所以一般不用整除,采用 (l+r)>>1 作为左中位数,避免这种问题,而右中位数则是 (l+r+1)>>1 。

然后,看if后边的语句才是记录着答案(符合check的答案),意味着else后边的语句会在结束的时候向if后边的语句收敛(或者if后带着答案收敛于else)。因为else后语句不可避免要+1或者-1因为整数二分要避免死循环,但是就算如此中位数压缩到左右不对也会导致死循环。

最后,这里我们拿else后语句为 r=mid-1 举例,反之亦然。r会收敛于l,如果mid使用左中位数,答案会向左边界压缩,而语句中 l 记录着答案,l直接等于mid没有+1的改变,所以压缩到 l 时会导致死循环。所以这里我们使用右中位数,避免左中位数压缩左边导致的死循环。

总结,哪个会动采用哪边的中位数,哪个会动看else后语句,即else语句后是r则用右,是l则用左。

如此我们便完成了整数二分的规范,避免了死循环且答案压缩在if后面的语句(实际上最后l=r,哪个都可以),这里用单调递增数列二分检索x和x的前驱(即找到第一个小于等于x的数)举例

int fun(int a[],int n,int x){
	int l=0,r=n-1;//这里初值的选取可以用特殊值,看最后收敛至哪里需求改变(其实也可以设置特别宽)
	while(l<r){
		int mid=(l+r+1)>>1;//因为第六行,所以用右中位数
		if(a[mid]<=x) l=mid;//答案保存在l中
		else r=mid-1;//r会改变
	}
	return l;
}

总结:1.写出未完善代码 2.完善check及后面语句 3.根据else后语句选择中位数

可以参考如下习题,(一)中第一道压缩至右边界(选择左中位数),第二道压缩至左边界。

二分习题练习

(一)最大值最小化(最小值最大化)

题目建模:

一般这种题目采用二分思路

洛谷P1462

附链接https://www.luogu.com.cn/problem/P1462

题面

建模:

x为边权,x<=b

st. y=max{fi} fi所连成总边权x<=b

求 min{y}

思路:对点权二分,检查到当前点权fi时删除所有大于fi的点,对剩余点用dijkstra算法(优先队列优化后),检查是否满足x<=b,算法时间复杂度O(mlognlogn)

#include <bits/stdc++.h>
#define lowbit(x) ((x) & -(x))
#define rep(i,l,r) for(register int i=l;i<=r;i++)
using namespace std;

const int N = 1e4 + 5;
const int INF = 0x3fffffff;
const double eps = 1e-7;
typedef long long ll;
const int modp = 1e6 + 37;

typedef struct ff {
	ll pos;
	ll data;
	bool operator <(const ff &a) {
		return data < a.data;
	}
} F;

typedef struct mm {
	ll p;
	ll d;
	bool operator >(const mm &a)const {
		return d > a.d;
	}
} M;

F f[N];
ll n, m, b;
vector<ll> son[N], w[N];
bool vis[N];
ll dist[N];

ll dj() {
	if (!vis[1])
		return INF;
	priority_queue<M, vector<M>, greater<M> > pq;
	ll visnum = 0;
	rep(i, 1, n) dist[i] = INF;
	rep(i, 1, n) if (vis[i])
		visnum++;
	rep(i, 1, son[1][0]) if (vis[son[1][i]]) {
		M tmp;
		tmp.p = son[1][i];
		tmp.d = w[1][i];
		pq.push(tmp);
		dist[son[1][i]] = w[1][i];
	}
	rep(i, 1, visnum - 1) {
		if (pq.empty())
			break;
		ll pos = 0, dis = INF;
		while (!vis[pos]) {
			M tmp = pq.top();
			pq.pop();
			pos = tmp.p;
			dis = tmp.d;
			if (dist[pos] != dis)
				continue;
		}
		if (pos == 0)
			continue;
		rep(j, 1, son[pos][0]) {
			if (vis[son[pos][j]] && dist[son[pos][j]] > w[pos][j] + dis) {
				dist[son[pos][j]] = w[pos][j] + dis;
				M tmp;
				tmp.d = dist[son[pos][j]];
				tmp.p = son[pos][j];
				pq.push(tmp);
			}
		}
	}
	return dist[n];
}

bool check(int x) {
	vis[0] = false;
	rep(i, 1, n) if (i <= x)
		vis[f[i].pos] = true;
	else
		vis[f[i].pos] = false;
	ll dis = dj();
	if (dis <= b)
		return true;
	return false;
}

void solve() {
	sort(f + 1, f + n + 1);
	ll l = 1, r = n + 1;
	while (l < r) {
		ll mid = (l + r) >> 1;
		if (check(mid))
			r = mid;
		else
			l = mid + 1;
	}
	if (l == n + 1)
		cout << "AFK";
	else
		cout << f[l].data;
}

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin >> n >> m >> b;
	rep(i, 1, n) {
		ll x;
		cin >> x;
		f[i].pos = i;
		f[i].data = x;
	}
	rep(i, 1, n) son[i].push_back(0), w[i].push_back(0);
	rep(i, 1, m) {
		ll a, b, c;
		cin >> a >> b >> c;
		if (a == b)
			continue;
		son[a].push_back(b);
		son[a][0]++;
		son[b].push_back(a);
		son[b][0]++;
		w[a].push_back(c);
		w[b].push_back(c);
	}
	solve();
	return 0;
}

洛谷P1824

附链接https://www.luogu.com.cn/problem/P1824

题面

建模:

条件:c头牛在xn个坐标上

y=dist(xi,xj)

求 max{y}

思路:二分距离y,检查y是否满足c头牛都在坐标上。

#include <bits/stdc++.h>
#define lowbit(x) ((x) & -(x))
#define rep(i,l,r) for(register int i=l;i<=r;i++)
using namespace std;

const int N = 1e5 + 5;
const int INF = 0x3fffffff;
const double eps = 1e-7;
typedef long long ll;
const int modp = 1e6 + 37;

int n, c;
int a[N];

bool check(int x) {
	int i = 1, j = 1;
	int sum = 1;
	while (j <= n) {
		while (j <= n && a[j] - a[i] < x)
			j++;
		if (j > n)
			break;
		sum++;
		i = j;
	}
	if (sum >= c)
		return true;
	return false;
}

void solve() {
	int l = 0, r = a[n] - a[1];
	while (l < r) {
		int mid = (l + r + 1) >> 1;
		if (check(mid))
			l = mid;
		else
			r = mid - 1;
	}
	cout << l;
}

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin >> n >> c;
	rep(i, 1, n) cin >> a[i];
	sort(a + 1, a + n + 1);
	solve();
	return 0;
}


这里选择一道实数二分,相比整数二分考虑的就没有那么多了,注意精度问题,精度过大错误,精度过小超时

POJ 2018 最大平均字段和

附链接http://poj.org/problem?id=2018

题面

建模:

条件:j-i>=F

st. y=(a[j]-a[i])/(j-i)

求 max{y}

思路:这是一道最大平均字段和问题,二分平均值,检查y时找到一个平均字段和大于等于y,判断这个字段的长度是否小于F。这里对数组中每个元素a[i]都减去y即 a[i]=a[i]-y 然后求其前缀和,我们找所有长度大于等于F的前缀和,判断其是否大于等于0就好。当前前缀和-F个位置之前的最小前缀和就是当前位置长度大于等于F的最大区间和。

#include<iostream> //poj不能用万能头
#include<cstdio>
#define lowbit(x) ((x) & -(x))
#define rep(i,l,r) for(register int i=l;i<=r;i++)
using namespace std;

const int N = 1e5 + 5;
const int INF = 0x3fffffff;
const double eps = 1e-5;
typedef long long ll;
const int modp = 1e6 + 37;

int n, f;
double a[N];

bool check(double x) {
	double b[N];
	b[0] = 0;
	rep(i, 1, n) {
		b[i] = a[i] - x;
		b[i] += b[i - 1];
	}
	double minn = INF;
	double maxsum = -INF;
	rep(i, f, n) {
		minn = min(b[i - f], minn);
		maxsum = max(maxsum, b[i] - minn);
	}
	return maxsum >= 0;
}

void solve() {
	double l = 0, r = 2005;
	while ((r - l) > eps) {
		double mid = (l + r) / 2;
		if (check(mid))
			l = mid;
		else
			r = mid;
	}
	cout << (int)(r * 1000);//这里答案压缩在右边界
}

int main() {
	//ios::sync_with_stdio(0);
	//cin.tie(0);
	//cout.tie(0);
	while (scanf("%d%d", &n, &f) == 2) { //poj这里不这样处理会死循环而超时
		rep(i, 1, n)scanf("%lf", &a[i]);
		solve();
	}
	return 0;
}


poj有一些问题需要注意,double和负值情况也需要注意。

这里留下个疑问,答案压缩在左边界就wa了,实数二分理应左右边界均可以,观察很多实数二分代码均打印else后答案。

欢迎lao来解答。

总结

回归将最大值最小化问题建模后样子,解决此问题综合来讲有以下几步

  1. 对y二分
  2. 检查时利用 f(x) 和 I(x) 判断是否符合要求,大部分为利用 f(x) 检查 I(x) ,也有综合利用逆向考虑的
  3. 二分结果为最终答案

(二)to be continue...

全部评论

相关推荐

05-12 11:09
已编辑
门头沟学院 后端
已注销:没必要放这么多专业技能的描述。这些应该是默认已会的,写这么多行感觉在凑内容。项目这块感觉再包装包装吧,换个名字,虽然大家的项目基本都是网上套壳的,但是你这也太明显了。放一个业务项目,再放一个技术项目。技术项目,例如中间件的一些扩展和尝试。
简历中的项目经历要怎么写
点赞 评论 收藏
分享
一表renzha:手写数字识别就是一个作业而已
点赞 评论 收藏
分享
不愿透露姓名的神秘牛友
07-01 11:47
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务