多种二分模板与写不对二分的可能问题

目录

  1. 三段式快排与归并排序的学习笔记
  2. 二分写法的思考与万用式二分

笔记

1. 三段式快排与归并排序的学习笔记

标准的快排或使用三段式快排都是不稳定的排序方法,在一些情况下很容易被卡成狗,例如 P1309 [NOIP2011 普及组] 瑞士轮 使用 STLstd::sort\(\texttt{TLE}\) 多个点。若使用 stable_sort 换用封装的归并排序便可以获得稳定的时间复杂度。

2. 二分写法的思考与万用式二分

目前我见到的二分主要有两套模板:

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

upd in 2026/1/7:还是第二套模板好用,感觉更不容易出问题。

对于第一套模板,在二分查找模板题 P2249 【深基13.例1】查找 中,我们是这样写的:

while(r>l){
	int mid = (l+r)/2;
	if(a[mid]>=t)r = mid;
	else l = mid+1;
}

而在二分题 P2678 [NOIP2015 提高组] 跳石头 中,我们是这样写的:

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

在前一题中,答案编号单调不减,而后一题中,所需要移除的石头数最短跳跃距离单调不减,但前一题我们需要找到尽可能小的编号,后一题我们应使跳跃距离的最小值最大化,这样就产生了分歧:在 mid 得到取等条件时,前一题我们要让右指针左移而偏左,后一题我们要让左指针右移而偏右;而为什么后一题要维护一个 ans 但前一题却不用呢?在前一题由于是偏左答案,最后 \(\texttt{l = r}\) 都为答案,后一题是偏右答案,显然我们知道右指针的 check(r) != m 在最后为了跳出循环,左指针会 +1 使得 \(\texttt{l = r}\) 都不满足 check(),而左指针在变化前才是我们需要的值,因为由题意可知是必然存在解的。
另外,为什么我们需要让 l = mid+1 呢,模拟一下便知道,由于 int 的舍去小数特性,即天然偏左,如果我们不让左指针 +1,在 check(mid)==m 时会陷入死循环,有朋友可能会问:这难道不会丢解吗?
一般地,我们认为 check(x) == m 时我们找到了一个解 \(\texttt{x}\),在前一题中取等后会有 r=x 操作,指针恰好站在解上没有丢解,在后一题中我们有 ans 维护当前(不一定是最优)解,这也是后一题多维护一个 ans 的原因。

当然,\(\texttt{rxz}\) 老师指出,学院派的二分写法会有极大可能写错,而使用万用的如下二分可以规避写错问题,既可以偏左也可以偏右还不用考虑舍去:

while(r - l > 5) {
        int mid = (l + r) / 2;

        if(check(mid))
            r = mid;
        else
            l = mid;
    }

    for(int x=l; x<=r; x++)
        if(check(x)) {
            cout << x << endl;
            return;
        }

但是万用式二分虽然万用,在遇到一些情况时反而会更复杂:比如 P4343 [SHOI2015] 自动刷题机 这类解的范围很广的情况。
当用传统写法,我们只需要画个图判断一下左右倾向即可。

void lf(){
    ll l = 1, r = 1e14;
    while(l<r){
        ll mid = (l+r)/2;
        if(check(mid) <= k) r = mid;
        else l = mid+1;
    }
    if(check(l) == k) cout<<l;
    else cout<<-1;
}
void rht(){
    ll l = 1, r = 1e14;
    ll ans = 0;
    while(l<r){
        ll mid = (l+r)/2;
        if(check(mid) < k) r = mid;
        else l = mid+1, ans = mid;
    }
    if(check(ans) == k) cout<<ans;
}

或者用另一种模板,在此题下更为清晰,或者说更有对称美。

while(l<=r){
	mid=(l+r)/2;
	if(check(mid)<=k){
		r=mid-1;
		if(check(mid)==k) ans1=mid;
	}
	else l=mid+1;
}
while(l<=r){
	mid=(l+r)/2;
	if(check(mid)>=k){
		l=mid+1;
		if(check(mid)==k) ans2=mid;
	}
	else r=mid-1;
}
posted @ 2024-08-02 09:43  [丘李]Chilllee  阅读(101)  评论(0)    收藏  举报