【基础】三分

想着一直都没有写过三分的模板,每次都是用for循环收尾,习惯确实不太好。

三分法求先增后减或者先减后增函数的最值。其实都没必要是上凸或者下凸的函数。

下面是每次缩减1/3长度的写法,比较容易理解,面对负数等情况也不容易出问题。

// x = 4, f(x) = 20
ll f (ll x) {
    return -x * (x - 9);
}

ll ternary_search () {
    ll L = -1e9, R = 1e9;
    while (R - L + 1 >= 3) {
        printf ("[%lld, %lld]\n", L, R);
        ll M1 = L + (R - L + 1) / 3;
        ll M2 = R - (R - L + 1) / 3;
        // 如果f是上凸/先增后减函数,寻找“最靠左的”最大值,则为<号
        // 如果f是下凸/先减后增函数,寻找“最靠左的”最小值,则为>号
        // 如果要改成“最靠右的”最值,添加=号
        if (f (M1) < f (M2)) {
            L = M1;
        } else {
            R = M2;
        }
    }
    // 如果f是上凸/先增后减函数,寻找“最靠左的”最大值,则为<号
    // 如果f是下凸/先减后增函数,寻找“最靠左的”最小值,则为>号
    // 如果要改成“最靠右的”最值,添加=号
    if (f (L) < f (R)) {
        L = R;
    }
    printf ("x = %lld, f(x) = %lld\n", L, f (L));
    return L;
}

但其实对于中间点的选取,可以取接近1/2的位置,因为本质上凸函数最多存在一段单调递增的和一段单调递减的(对于上凸/先增后减函数,如果有减区间,一定是先增后减的),通过判断相邻的两个点的大小即可知道目前在增区间还是减区间。然后利用移位合并各种边界情况,写的跟二分一模一样,因为本质上就是二分最后一个单调增的位置。

// x = 4, f(x) = 20
ll f (ll x) {
    return -x * (x - 9);
}

ll ternary_search () {
    ll L = -1e9, R = 1e9;
    while (L < R) {
        printf ("[%lld, %lld]\n", L, R);
        // 有负数,只能用移位不能用除法
        ll M = (L + R) >> 1;
        // 如果f是上凸/先增后减函数,寻找“最靠左的”最大值,则为<号
        // 如果f是下凸/先减后增函数,寻找“最靠左的”最小值,则为>号
        // 如果要改成“最靠右的”最值,添加=号
        if (f (M) < f (M + 1)) {
            L = M + 1;
        } else {
            R = M;
        }
    }
    printf ("[%lld, %lld]\n", L, R);
    printf ("x = %lld, f(x) = %lld\n", L, f (L));
    return L;
}

注意这里的M的写法需要是移位而非除法,C++的除法是向0取整,而C++的移位是向负无穷取整。简单理解就是-1 >> 1还是-1,但是-1 / 2为0。
这里并不需要像二分那样面对“最靠右的”值时修改M的算法。如果修改为M = (L + R + 1) >> 1的话会导致M + 1越界R。这里只需要在if中添加一个等于号即可变成最靠右的写法(因为两个选择都会让L和R至少步进1,这个和二分并不一样)。边界情况就是L == R - 1的时候,此时一定要让M == L,然后 M + 1 == R,所以为了考虑负数要使用移位的方法。

相对的,二分也可以用相似的方法合并,通过在check函数前增加!取反来统一第一个选择支移动L,第二个选择支移动R,但是取反之后为了保证步长至少为1,所以M的取值还是要考虑偏左还是偏右。但是没必要,已经写习惯了这么久了。

有一种新的二分的写法,说不定以后有用:

int L = 0, R = 1e9;
int ans = -1;
while (L <= R) {
    int M = (L + R) >> 1;
    if (check (M)) {
        ans = max (ans, M);
        L = M + 1;
    } else {
        R = M - 1;
    }
}

这种写法可以处理不存在答案的情况,而且不再需要区分M怎么取。最重要的是,LR是待搜索区间的长度,意味着区间长度衰减得比我习惯的写法快。对于某些特殊的题可能可以有常数级别的优化,毕竟check不见得是O(1)的操作,能省就省。

posted @ 2024-04-20 17:49  purinliang  阅读(4)  评论(0编辑  收藏  举报