• 【CPL 001】二分


    手写二分在涉及负数的时候,处理得不好容易导致死循环,比如下面这个例子:

    对于任意的 \(x_1<x_2\) ,若 \(check(x_1)\) 为真,则 \(check(x_2)\) 也为真(真区间在右边)时:

    ll findFirst (ll L, ll R) {
        while (L < R) {
            ll M = (L + R) / 2;
            if (check (M)) {
                R = M;
            } else {
                L = M + 1;
            }
        }
        return L;
    }
    

    \(L==-1, R==0\) 时, \(M==0\) ,若 \(check(M)\) 为真,则下一个迭代时 \(L==-1, R==0\) ,造成死循环。

    此类问题的根源来自于保留 \(M\) 的值的那个分支,在上面的例子里,保留 \(M\) 的分支为 \(R==M\) 。只要让 \(M\) 永远不等于 \(R\) ,那么每次迭代都能使得答案区间的长度减少至少 \(1\)

    下面这样修复可以吗:

    ll findFirst (ll L, ll R) {
        while (L < R) {
            ll M = (L + R + 1) / 2;
            if (check (M)) {
                R = M;
            } else {
                L = M + 1;
            }
        }
        return L;
    }
    

    其实也是不可以的,在 \(L==0, R==1\) 时, \(M==1\) ,若 \(check(M)\) 为真,则下一个迭代时 \(L==0, R==1\) ,造成死循环。

    不想分析那么清楚怎么办?可以把退出的条件放宽一点,在 \(L == R - 1\) 的情形下进行额外检测。

    不想写那么多额外的检测怎么办?这里需要使用 C++ 的右移运算符,对于有符号数的负值右移运算符的结果是一个 ub ,但是在 x86 和 x64 的体系下,有符号数的右移运算实现为“算术右移”。

    使用这一点可以保证 \(-3>>1==-2, -1>>1==-1, 1>>1==0, 3>>1==1\) ,这就是 \(\lfloor\frac{x}{2}\rfloor\) ,除以2的向下取整。

    使用向下取整的方法就可以保证永远取不到上边界啦!

    然而这种方法是使用 ub 的,会导致漏洞和CPU的架构相关,bug会非常难以理解。建议还是写特殊处理。

    ll findFirst (ll L, ll R) {
        while (L < R + 1) {
            ll M = (L + R) / 2;
            if (check (M)) {
                R = M;
            } else {
                L = M + 1;
            }
        }
        if (check (L)) {
            return L;
        }
        return R;
    }
    

    当真区间在左边时:

    ll findLast (ll L, ll R) {
        while (L < R) {
            ll M = (L + R) / 2;
            if (check (M)) {
                L = M;
            } else {
                R = M - 1;
            }
        }
        return L;
    }
    

    这样写直接就死循环了,正数都会死循环,分析跟上面一样,我们关心那个保留 \(M\) 的分支,发现是赋值给 \(L\) ,而 \(L==M\) 在正数除以2的时候就会轻松出现。

    这里的正确写法是

    ll findFirst (ll L, ll R) {
        while (L < R) {
            ll M = (L + R) >> 1;
            if (check (M)) {
                L = M;
            } else {
                R = M - 1;
            }
        }
        return L;
    }
    
    ll findLast (ll L, ll R) {
        while (L < R) {
            ll M = (L + R + 1) >> 1;
            if (check (M)) {
                L = M;
            } else {
                R = M - 1;
            }
        }
        return L;
    }
    

    分析问题,确定问题的真区间是在左半段还是右半段是有必要的,然后针对两种情况选择不同的写法。但是要注意的是这种使用 ub 的行为是不可移植的。

    或者用一个不带有 ub 的写法。

    ll findFirst (ll L, ll R) {
        while (L < R + 1) {
            ll M = (L + R) / 2;
            if (check (M)) {
                L = M;
            } else {
                R = M - 1;
            }
        }
        if (check (L)) {
            return L;
        }
        return R;
    }
    
    ll findLast (ll L, ll R) {
        while (L < R + 1) {
            ll M = (L + R) / 2;
            if (check (M)) {
                L = M;
            } else {
                R = M - 1;
            }
        }
        if (check (R)) {
            return R;
        }
        return L;
    }
    

    这里有时候有人担心溢出的风险,其实我觉得是没有必要的,如果加法都要溢出了那么大概是不适合用这个长度的整数了。而且只能说明出题人真恶心。

    参考资料

    1. 左移和右移运算符 ( " <<" 和 ">>" ) | Microsoft Docs
    2. 《算法竞赛进阶指南》P25 0x04 二分
  • 相关阅读:
    C++网易云课堂开发工程师-操作符重载
    C++网易云课堂开发工程师-参数传递与返回值
    C++网易云课堂开发工程师-class的声明
    C++网易云课堂开发工程师-头文件与类声明
    线性代数的本质-08第二部分-以线性代数的眼光看叉积
    线性代数本质-08第一部分-叉积的标准介绍
    线性代数的本质-07-点积与对偶性
    线性代数的本质-06补充说明-非方阵
    线性代数的本质-06-逆矩阵、列空间与零空间
    cocos2d-x
  • 原文地址:https://www.cnblogs.com/purinliang/p/16019998.html
Copyright © 2020-2023  润新知