二分

二分查询与应用的二分验证答案。

封左,即在一堆符合条件的数里面缩小右边界,最后选一个最靠左的。封右同理。

即封左减右和封右减左。

注意,如果在序列中找不到这个数,那么封左会找到大于它的靠左的数(最后是 \(l + 1\)),同理封右可以找到小于它的最大数。你可以找数据试一试。规避情况即可。

封左

int find(int x)
{
	int l = 1, r = n;
	while (l < r)
	{
		int mid = l + r >> 1;
		if (check(mid)) r = mid;
		else l = mid + 1;
	}
	return l;
}

封右

int find(int x)
{
	int l = 1, r = n;
	while (l < r)
	{
		int mid = l + r + 1 >> 1;
		if (check(mid)) l = mid;
		else r = mid - 1;
	}
	return l;
}

本质

对于浮点数二分,直接 \(mid = (l + r) / 2\) 即可,因为浮点数可以找到绝对中值,而整型不行,因为对于和为奇数时,无法得到一个绝对中值,只能向下取整,而这就会导致一些问题。

如在 \(1,2\) 中二分查找 \(2\),使用上面的封右模版,如果 \(mid\) 还是等于 \((l + r) / 2\),那么 \(mid\) 等于 \(1\) 此时判断符合条件,于是更改左边界,完了 1 还是左边界,使得无限循环。

为什么?因为以上代码是简化了中值特判,最本质的写法应为:找中值判断是否为满足条件的值,如果需要直接返回此下标,如果是不满足条件的,观察左右哪边符合条件,如果是左边那么 \(r = mid - 1\),如果是右边,那么 \(l = mid + 1\),即跳过了 \(mid\),防止了无限循环。

而此写法简化了中值特判,因为整型的向下取整,再加上缩小合法范围时包含 \(mid\),如果此时边界为更新左边界为 \(mid\),那么下一次的 mid 就会有向上一次 \(mid\) 靠近的趋向(向下取整中值偏左),一旦两个 \(mid\) 相同,就会无限循环。

为了防止 \(mid\) 无限成为边界,应当调整取整方向,使其“背离” \(mid\)(即如果包含的 \(mid\) 的边界是左边界,那么取整尽量使其上,即中值偏右),只要打消了偏向,就可以防止 \(mid\) 成为恒定中值。

因此在使用封左时,mid 要下取整,背离更新的右边界,同理在封右时,要上取整,背离更新的左边界。简单说,封哪边,向哪边走。

posted @ 2025-10-28 16:06  blind5883  阅读(4)  评论(0)    收藏  举报