跳转至

二分

应用:当问题的答案具有单调性时,可以通过二分把求解转化判定

复杂度理论:判定难度小于求解

效率提升本质:利用单调性,通过一次比较即可批量判断解是否符合条件

整数集合上的二分

规定:

1.答案位于\([l,r]\)

注:\([l,r]\)为当前能确认含有答案的准确区间,即\(l,r\)符合条件,而\(l-1,r+1\)不符合条件

2.每次循环后区间缩小

3.条件为\(mid\)符合条件

分类:

a.若\(mid\)符合条件,则区间向左缩小,如图:

符合向左缩小

由1知,当\(mid\)符合条件时,\([l,r]\rightarrow[l,mid]\);当\(mid\)不符合条件时,\([l,r]\rightarrow[mid+1,r]\)

由2知,\(\left\{\begin{array}{ll}mid<r,\\mid+1>l\end{array}\right.\Leftrightarrow l-1<mid<r\)

由于我们要进行二分操作,\(mid\)应尽量与\(\dfrac{l+r}{2}\)靠近,又由于\(mid\in \mathbb{Z}\),故问题转化为选择\(\lfloor\dfrac{l+r}{2}\rfloor\)还是\(\lceil\dfrac{l+r}{2}\rceil\)作为\(mid\)的值。

易知\(x-1<\lfloor x\rfloor\leqslant x\),故选择\(mid=\lfloor\dfrac{l+r}{2}\rfloor\)时有\(\dfrac{l+r}{2}-1<mid\leqslant\dfrac{l+r}{2}\),

由1知,\(l<r\),故\(mid>\dfrac{l+r}{2}-1>\dfrac{l+l}{2}-1=l-1,mid\leqslant\dfrac{l+r}{2}<\dfrac{r+r}{2}=r\),

\(l-1<mid<r\),符合条件。

实现:

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

细节:

1.终止时\(l=r\),则\(l\leqslant ans\leqslant r=l\Leftrightarrow ans=l=r\)

2.\(\lfloor\dfrac{l+r}{2}\rfloor\)应写作(l+r)>>1而不是(l+r)/2.

略证:考虑奇数\(S(S>0)\),要证\((-S)>>1=\lfloor\dfrac{-S}{2}\rfloor\),只要证\(\overline{-S}-1=-S-1\),

\(0-(\overline{-S}-1)=[2^n-(\overline{-S})]+1=S+1\),即证。

c++整数除法向0取整。

b.若\(mid\)符合条件,则区间向右缩小,如图:

符合向右缩小

由1知,当\(mid\)符合条件时,\([l,r]\rightarrow[mid,r]\);当\(mid\)不符合条件时,\([l,r]\rightarrow[l,mid-1]\)

由2知,\(\left\{\begin{array}{ll}mid>l,\\mid-1<r\end{array}\right.\Leftrightarrow l<mid<r+1\)

易知\(x\leqslant\lceil x\rceil< x+1\)​,故选择\(mid=\lceil\dfrac{l+r}{2}\rceil\)时有\(\dfrac{l+r}{2}\leqslant mid<\dfrac{l+r}{2}+1\),

由1知,\(l<r\),故\(mid\geqslant\dfrac{l+r}{2}>\dfrac{l+l}{2}=l,mid<\dfrac{l+r}{2}+1<\dfrac{r+r}{2}=r+1\),

\(l<mid<r+1\),符合条件。

实现:

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

细节:

1.由于上取整不易直接使用下取整来表示,我们先对可能的结果进行讨论:若\(\dfrac{x}{2}\in \mathbb{Z}\),则\(\lceil\dfrac{x}{2}\rceil=\dfrac{x}{2}\);若\(\dfrac{x}{2}\in \mathbb{Q}\),则\(\dfrac{x}{2}\)可写作\(\overline{n.5}\)的形式,\(\lceil\dfrac{x}{2}\rceil=\lfloor\dfrac{x}{2}\rfloor+1\).

不难发现,此时\(\lceil\dfrac{x}{2}\rceil=[\dfrac{x}{2}]\),对\(\dfrac{x}{2}\)进行四舍五入即可求得\(\lceil\dfrac{x}{2}\rceil\)的值。

\([x]=\lfloor x+0.5\rfloor\)(\(\{x\}\geqslant0.5\)将进位),故\(\lceil\dfrac{l+r}{2}\rceil=[\dfrac{l+r}{2}]=\lfloor\dfrac{l+r}{2}+0.5\rfloor=\lfloor\dfrac{l+r+1}{2}\rfloor\),写作(l+r+1)>>1.

值得注意的是,我们认为答案位于\([l,r]\)中,事实上已经假设有解;若\([1,n]\)中无解,以符合便向左缩小情况为例,每一个\(mid\)都不符合条件,答案区间最终会向右缩小至n,而我们并没有判断n是否符合条件。一方面,我们可以特判n,另一方面,我们可以将求解范围扩展至\([1,n+1]\),这样,当无解时范围将会被缩小至n+1,显然超出求解范围,便能快速判断是否无解。

符合便向右缩小情况类似,扩展至\([0,n]\)即可。

总结:

mid符合 r = mid l = mid
mid不合 l = mid+1 r = mid-1
mid取值 \(\lfloor\dfrac{l+r}{2}\rfloor\) \(\lceil\dfrac{l+r}{2}\rceil=[\dfrac{l+r}{2}]\)
代码实现 (l+r)>>1 (l+r+1)>>1
无解判定 \([1,n+1]\) \([0,n]\)

简记:左下右上

c++ STL中lower_bound与upper_bound函数实现了在一个序列中二分查找某个整数x的后继。

问题写法:

1.\(l=mid+1,r=mid-1\):符合便向左缩小时\(r\)缩过头导致\(mid\)丢失;符合便向右缩小时\(l\)缩过头导致\(mid\)丢失

2.\(l=mid,r=mid\):符合便向左缩小时\(l\)未缩到位导致\(mid+1\)混入;符合便向右缩小时\(r\)未缩到位导致\(mid-1\)混入

实数域上的二分

实现1:精度控制

while (l + eps < r) { // 1.
  double mid = (l + r) / 2; // 2.
  if (check(mid)) r = mid;
  else l = mid;
}

细节:

1.eps为精度,即\(r-l<eps\),若保留k位小数,一般取\(eps=10^{-(k+2)}\),此时出现\(l+eps\)发生进位的概率极小,几乎能求出正确结果。

2.此处\(l,r\)为浮点数,做实数运算。

实现2:固定次数

const int K = 100;

for (int i = 0; i < K; i++) {
  double mid = (l + r) / 2;
  if (check(mid)) r = mid;
  else l = mid;
}

三分法求单峰函数极值

定义:拥有唯一极值点,且极值点两侧严格单调

思路:分类讨论

以单峰函数为例,在定义域\([l,r]\)任取两点\(lmid,rmid(lmid<rmid)\),则

\(f(lmid)<f(rmid)\)时,如图:

l<r

由于\(rmid\)位置不确定,而\(lmid\)一定在\(x_0\)左侧(由\(lmid<rmid\)限制),故利用\(lmid<x_0\)确定极值点比\(lmid\)大,将\(l\)缩小至\(lmid\)

\(f(lmid)>f(rmid)\)时,如图:

l>r

同理,利用\(rmid>x_0\)\(r\)缩小至\(rmid\)

\(f(lmid)=f(rmid)\)时,如图:

l=r

由于该函数在极值点两侧具有严格单调性,故\(lmid,rmid\)一定位于\(x_0\)两侧,可将\(l\)缩小至\(lmid\),\(r\)缩小至\(rmid\).

在此基础上,取\(lmid,rmid\)\([l,r]\)三等分点,每次将范围缩小\(\dfrac{1}{3}\),或取\(lmid,rmid\)靠近中点,可将范围近似缩小\(\dfrac{1}{2}\),在\(O(logn)\)的时间中求得最值。

二分答案转化为判定

二分的本质

二分的本质是寻找被二分量对应的合法与不合法的边界处的合法值。

为了证明这一性质,我们先对上述表述进行简化:

1.将\([l,r]\)中每一个数以\(check(i)=0/1\)代替(未进行判定的用?代替),表示取该值时是否合法;

2.将符合便向左缩小情况记作\(+1\),将符合便向右缩小情况记作\(1+\).

对于\(+1\)型,我们先考虑一般情况,即判定过程中即有符合情况,又有不符合情况。在最后一次区间缩小前,应为\(0[1,1]\).接着计算出\(check(mid)=1\)(下取整,左侧1),缩小后得到\(0[1]1\),即求得\(0,1\)边界处对应为1的值。

再考虑特殊情况:

当判定过程仅有符合情况时,最后一次缩小前为\([?,1]\).接着将会计算并确定?的值,如果\(?=0\),缩小后得到\(0[1]\),符合;如果\(?=1\),缩小后得到\([1]1\),为最左边的符合条件的值。

当判定过程仅有不符情况时,最后一次缩小前为\(0[?,?]\).接着将会计算并确定左侧?的值,如果\(?=1\),缩小后得到\(0[1]?\),符合;如果\(?=0\),缩小后得到\(0[?]\),需进行特判,与上述无解处理一致。

对于\(1+\)型,同理,一般情况时最后一次缩小前为\([1,1]0\),缩小为\(1[1]0\),符合;

仅有符合情况时,最后一次缩小前为\([1,?]\),接着确定?的值,\(?=0\)缩小为\([1]0\),符合;\(?=1\)缩小为\(1[1]\),为最右边的符合条件的值;

仅有不符情况时,最后一次缩小前为\([?,?]0\).接着确定右侧?的值,\(?=1\)缩小为\(?[1]0\),符合;\(?=0\)缩小为\([?]0\),需进行特判,与上述无解处理一致。

这样我们就证明了二分的本质。

我们可以定义广义的二分单调性,即通过对某一点情况的考察,能判断解位于该点左侧或右侧。

在此基础上,对于一个不具有一般意义下的单调性问题,我们将1用\(↗\)代替,0用\(↘\)代替,可将其图示为(以\(+1\)型为例):

vlphlR.png

不难发现,二分法事实上求得的是极值点,且\(+1\)型求得的是极小值点,\(1+\)型求得的是极大值点。

进而推广到\(+1\)型求得的是一个形如\(0[1]\)的分界,而\(1+\)型求得的是一个形如\([1]0\)的分界;更一般的,1,0不一定为合法状态和非法状态,可以为任意的两个状态。

最优化问题

最优化问题中最终价值具有单调性(较小合法、过大非法或较大合法、过小非法),所以我们对最终价值进行二分,进而求得最优化的价值。

在这一过程中,我们将整个问题转化为了判定最终价值是否合法,即二分答案转化为判定。


例1 Best Cow Fences

思路:二分答案+前缀和

当最终价值(平均数)过小或过大时都非法,形成\(↗↘\)形式的单调性,故可用二分法(1+型)求最值。

判断二分形式:1.表达单调性 2.将极值点记作+,将\(↗\)记作1

对最终价值进行二分,问题转化为判定是否存在一个长度不小于F的连续子段,使得其平均数大于等于给定的二分值,即\(\dfrac{1}{n}\sum a_i\geqslant avg\)

为了便于计算,我们将等式右边化为0,即

\(\sum a_i\geqslant n*avg\Leftrightarrow\sum(a_i-avg)\geqslant0\)

故把数列中每一个数减去二分值,并判断是否存在一个长度不小于F的连续子段,子段和非负。

部分和使用前缀和简化计算,取\(a_{j+1..i}(i\geqslant j+F)\),则

\(\max\sum\limits_{k=j+1}^i a_k=\max\limits_{F\leqslant i\leqslant n}\{S_i-\min\limits_{0\leqslant j\leqslant i-F}S_j\}\)

并且\(\min\limits_{0\leqslant j\leqslant i-F}S_j\)可随i变化动态更新。

实现:

bool check(double avg) {
  double ret = -1, tmp = INT_MAX;
  for (int i = 1; i <= n; i++)
    cur = a[i] - avg, s[i] = cur + s[i - 1];
  for (int i = f; i <= n; i++)
    tmp = min(tmp, s[i - f]), ret = max(ret, s[i] - tmp);
  return ret >= 0;
}
int main() {
  double l = 1, r = INF, eps = 1e-5; // 保留三位小数
  cin >> n >> f;
  for (int i = 1; i <= n; i++)
    cin >> a[i];
  while (l + eps < r) {
    double mid = (l + r) / 2;
    if (check(mid))
      l = mid;
    else
      r = mid;
  }
  cout << (int)(r * 1000) << endl;
}

例2 Innovative Business

思路:二分答案

本题即要求有向完全图(竞赛图)的Hamilton路径(不重不漏经过所有点的路径)。

先证本题有解,即任意竞赛图都存在Hamilton路径。

下对图中节点数\(n\)用数学归纳法:

\(n=1,2\)时,显然;

\(n\geqslant3\)时,假设\(n=k\)时结论成立,即\(k\)点的竞赛图存在Hamilton路径;

\(n=k+1\)时,有以下三种情况:

1.第k+1点有一条指向原Hamilton路径起点的出边,如图:

vlKFl6.png

那么我们直接将它作为新Hamilton路径的起点;

2.第k+1点有一条被原Hamilton路径终点指向的入边,如图:

vlKAOO.png

那么我们直接将它作为新Hamilton路径的终点;

3.若以上两种情况都不符合,即第k+1点被原Hamilton路径起点指向的入边,有指向原Hamilton路径终点的出边,如图:

vlKk6K.png

方便起见,我们将指向第k+1点的入边记作0,由第k+1点发出的出边记作1,问题转化为证明0-1串\(0…1\)中存在相邻两数为\(01\).

采用反证法,若该0-1串中任意相邻两数都相等,易知此时该串末尾的数为0,矛盾!

故存在相邻两数不等,且从左往右第一个出现的组合一定为\(01\)(两数之前都是0),即证。

那么我们只要将第k+1点连入涉及到的两点之间即可。

由归纳原理知,结论成立。

回到原题,仿照上述数归思路,我们可以逐个确定元素的位置,将第k+1个元素放入已排好序的前k个元素中去。由于放置点为出边状态和入边状态的边界,具有\(↘↗\)的局部单调性,故可用二分法(+1型)求解。

实现:

vector<int> specialSort(int N) {
  vector<int> ans;
  ans.push_back(1);
  if (N == 1)
    return ans;
  if (compare(1, 2))
    ans.push_back(2);
  else
    ans.insert(ans.begin(), 2);
  if (N == 2)
    return ans;  // 特判n = 1, 2
  for (int i = 3; i <= N; i++) {
    int l = 1, r = i; // 1.
    while (l < r) {
      int mid = (l + r) >> 1;
      if (compare(i, ans[mid - 1])) // 下标从0开始
        r = mid;
      else
        l = mid + 1;
    }
    ans.insert(ans.begin() + l - 1, i); // 2.
  }
  return ans;
}

细节:

1.此处需对“无解”情况进行处理:本题实际上将某数插入至\(0\downarrow1\)处,仅有符合情况时,会插入至\(\downarrow1\)处,符合条件;而仅有不符情况时,会插入至\(0\downarrow0\)处(未对末位0作判定)导致结果出错,故需向右拓宽区间,使得此情况能插入至\(0\downarrow\)处。

2.向vector中插入某元素:\(v.insert(v.begin() + pos, val);\)将val插入至第pos位(下标从0开始)前(使val放置在第pos位)。