寒假讲课Day 11:二分法、分治法

二分法、分治法

二分法

二分查找

问题引入:猜数游戏

已知\(l,r\in\mathbb{Z},\ l\leq r\),我指定一个整数\(x^*\in[l,r]\)让你猜。你每次可以猜一个数\(x\),然后我会告诉你\(x\)\(x^*\)的大小关系。最坏情况下,你最少要几次才能猜出\(x^*\)?换句话说,猜数的最优策略是怎样的?

显然最优策略是每次猜可能范围中央的那个值,逐渐缩小范围,最坏需要\(\lceil\log_2(r-l+1)\rceil\)次。这里最关键的就是可以知道估计值和准确值的“大小关系”。

让我们抽象一层:

给定单调递增函数\(f:\mathbb{Z}\cap[l,r]\to\mathbb{R}\),指定一个\(y^*\in\operatorname{ran}f\),求\(x^*\in\mathbb{Z}\cap[l,r]\)使\(f(x^*)=y^*\)

继续抽象一层:

给定有限全序\(\langle S,\leq\rangle\),我指定一个\(x^*\in S\)让你猜,你每次可以猜一个数\(x\),然后我会告诉你\(x\)\(x^*\)的大小关系。最坏情况下,你最少要几次才能猜出\(x^*\)

方法描述

二分法的关键在于有序性。特别地,有序通常表现为单调性

一般有三种查找方式:

  • 找到任何一个\(x^*\)使\(f(x^*)=y^*\)
  • 找到最小的\(x^*\)使\(f(x^*)\geq y^*\)
  • 找到最小的\(x^*\)使\(f(x^*)>y^*\)

例题

STL

注意到这个逻辑很容易抽象出来,所以标准库其实为我们提供了好用的库函数。

  • std::binary_search:二分查找返回是否存在目标值。废物。
  • std::lower_bound:返回最小的能插入目标元素而不破坏有序性的位置。实际上是最小的大于等于目标元素的位置。
  • std::upper_bound:返回最大的能插入目标元素而不破坏有序性的位置。实际上是最小的大于目标元素的位置。

Python也有类似的bisect库。神秘的是,Java似乎只提供了“找到任何一个”的函数。

二分答案

大部分时候,由于时空复杂度限制,我们没办法将整个函数保存为数组再进行二分查找。由于二分查找过程中,我们仅使用了极少的几个位置上的值(\(\lceil\log_2n\rceil\)个),所以可以临时计算这些位置的函数值。这是“二分答案”的实现基础。

对于一些题目,我们可以考虑枚举答案然后检验正确性。如果检验的函数是满足单调性的,就可以使用二分法。特别地,如果答案的一侧满足条件而另一侧不满足,也可以视作一个单调的bool向量。

二分法并不局限于整数集。对于实数上的二分,注意循环条件应用\(r-l\geq\varepsilon\)

典型例题

模板

/**
 * @brief behaves as if it generates func[beg, end) and
 * 	performs std::lower_bound on it
 */
template <typename T, typename Ret, typename Func,
		  typename Cmp = std::less<Ret>>
inline T lowerBound(T beg, T end, const Ret &val,
					Func func = Func(), Cmp cmp = Cmp()) {
	while (beg < end) {
		T mid = beg + (end - beg) / 2;
		if (cmp(func(mid), val)) {
			beg = mid + 1;
		} else {
			end = mid;
		}
	}
	return beg;
}

/**
 * @brief behaves as if it generates func[beg, end) and
 * 	performs std::upper_bound on it
 */
template <typename T, typename Ret, typename Func,
		  typename Cmp = std::less<Ret>>
inline T upperBound(T beg, T end, const Ret &val,
					Func func = Func(), Cmp cmp = Cmp()) {
	while (beg < end) {
		T mid = beg + (end - beg) / 2;
		if (cmp(val, func(mid))) {
			end = mid;
		} else {
			beg = mid + 1;
		}
	}
	return beg;
}

分治法

定义

分治(divide and conquer),字面上的解释是「分而治之」,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。

分治法是“子问题思想”下的另一种方法论。二分法可以视作是一种特殊的分治。

适用条件

  • 该问题的规模缩小到一定的程度就可以容易地解决。
  • 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质,利用该问题分解出的子问题的解可以合并为该问题的解。
  • 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题。

典型例题

归并排序

有时间就讲一下罢。

定义了全序关系的集合可以排序。“排序”这个操作完全符合分治的条件:

  • 排序一个或两个元素的序列是很容易的。
  • 将一个序列排序,可以将其分解为分别排序前后两半部分,然后利用双指针合并为一个有序序列。
template <typename OutputIter, typename AuxIter>
void mergeSort(OutputIter first, OutputIter last, AuxIter aux) {
	size_t n = std::distance(first, last);
	if (n <= 1) return;

	// divide and conquer
	auto mid = std::next(first, n >> 1);
	mergeSort(first, mid, aux);
	mergeSort(mid, last, aux);

	// merge
	auto i = first, j = mid, k = aux;
	while (i != mid && j != last) {
		if (*i > *j) {
			*(k++) = *(j++);
		} else {
			*(k++) = *(i++);
		}
	}
	while (i != mid) *(k++) = *(i++);
	while (j != last) *(k++) = *(j++);
	std::copy(aux, k, first);
}
posted @ 2026-02-14 14:36  我就是蓬蒿人  阅读(3)  评论(0)    收藏  举报