二分以及编程过程中求中点各种写法思想解析以及完美写法

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/FlushHip/article/details/82319086

你真的理解二分的写法吗 - 二分写法详解这篇博客中,有几个朋友给二分求中点mid = (l + r) / 2提出了一些疑问和改进。自己最近也对这个问题有过一些思考,因此在这里系统详细地聊聊自己的看法,看看程序中求区间终点到底应该怎么写才是完美的。

先公布程序中求区间中点的完美写法:mid = l + (r - l) / 2

在数学上,mid = l + (r - l) / 2mid = (l + r) / 2是等价的,这个自己通分一下就行了。但是在程序中,往往需要求一个数组区间的中点,这也就意味着区间必须是一个整数,如果中点是x.5这种情况,你是向下取整还是向上取整。这在编程中是一个很大的坑。

mid = (l + r) / 2的劣势

首先看看mid = (l + r) / 2这种求中点有什么劣势。

  • 溢出
  • 求上下界会不统一

溢出

溢出比较好理解,l + r可能会溢出int的最大范围,而l + (r - l) / 2不会,这里用减法替代了加法,这种思想很多地方都有用到,比如求最小一个数,这个数的平方大于或等于给定的一个值n,直观代码的写法如下:

int x;
for (x = 0; x * x < n; ++x) {}

循环跳出,x就是答案,但是如果n = (1 << 31) - 1呢?这个循环永远不会结束,可以试试看,原因就是x * x会大过int的最大值,从而导致溢出出现负数。正确的写法如下:

int x;
for (x = 0; x < n * 1.0 / x; ++x) {}

这就是利用除法代替乘法,规避了溢出的分险,从数学的角度来看,这两者等价,但是在编程的领域中差别很大。下面分享一个题,你就更能体会这种思想了。

这里写图片描述

这是2018湘潭邀请赛的F题,题目很好理解,给三元组排序,排序规则就是题中的不等式。这题一看不就很好写嘛,sort一下,cmp函数自己写一下,但是先看看数据范围, 1 a i , b i , c i 10 9 。对于这种分式不等式,最好的做法就是把分式转化成乘法,也就是 2 A 3 B 2 A 3 B 6 A B 6 A B ,把数据范围代入进去,发现 2.4 × 10 19 ,而<stdint.h>unsigned long long的最大值:#define UINT64_MAX 0xffffffffffffffffULL /* 18446744073709551615ULL */,显然直接做数据范围承受不了。

这个题正确的做法应该是把 2 A 3 B 2 A 3 B 转化成 3 B 2 A 3 B 3 B 2 A 3 B ,这样子 ( 3 B 2 A ) 3 B 的最大值为 1.2 × 10 19 ,这是可以承受的。

通过代码如下:

#include <bits/stdc++.h>

using namespace std;

typedef unsigned long long uLL;

typedef tuple<uLL, uLL, uLL, int> Node;

int main()
{
    for (size_t n; EOF != scanf("%u", &n); ) {
        vector<Node> arr;
        uLL x, y, z;
        for (size_t i = 0; i < n; ++i)
            scanf("%lld%lld%lld", &x, &y, &z),
                  arr.emplace_back(x, y, z, i);
        sort(arr.begin(), arr.end(), [] (const Node &A, const Node &B)
        {
            uLL AA = get<2>(A) * (get<0>(B) + get<1>(B) + get<2>(B));
            uLL BB = get<2>(B) * (get<0>(A) + get<1>(A) + get<2>(A));
            return AA == BB ? get<3>(A) < get<3>(B) : AA > BB;
        });

        for (size_t i = 0; i < n; i++)
            printf("%u%c", get<3>(arr[i]) + 1, i == n - 1 ? '\n' : ' ');
    }
    return 0;
}

避免数据溢出的技巧还有很多,这里就先介绍到这里。

求上下界不统一

这个很有趣,一般不怎么会注意求上下界不统一这个坑,可以看下这篇博客右移一位和除二的区别。举个例子,区间[2, 5]的中点求下界是mid = (2 + 5) / 2 = 3,这没问题;区间[-5, 2]的中点求下界是mid = (-5 + 2) / 2 = -1,这里就出现问题了,[-5, 2]求下界是-2。而(l + r) / 2求出来是-1,也就是说(l + r) / 2想要正确求出区间中点的上下界就要针对(l + r)的正负做不同的处理。而用mid = l + (r - l) / 2就不会出现上述问题。

mid = l + (r - l) / 2的优势

mid = (l + r) / 2的劣势其实就是mid = l + (r - l) / 2的优势,这里总结一下:

  • 不会溢出
  • 上下界求法统一
    下界:mid = l + (r - l) / 2
    上界:mid = l + (r - l + 1) / 2

为什么很多人还是会写mid = (l + r) / 2

这是个问题,因为我也这样写,我总结了三点原因,如下。

  • 代码短,简单
  • 溢出这个问题一般不会发生,因为如果左右端点的范围到了 10 9 ,我都会选用long long,因此就不会发生溢出
  • 由于二分是求一个数组或向量的区间中点,那么l + r肯定不会是负数,那么也不会出现上下界不统一的问题

这也是为什么mid = (l + r) / 2能活这么久,且很多人没有发现mid = l + (r - l) / 2好处的原因。

总结

一个很小的细节里面的思想很精髓,我们应该多去思考“为什么是这样,为什么不是那样,那样和这样的区别是什么,优劣势是什么”。

C++标准库中STL的lower_boundupper_bound也是时候解开它们的神秘面纱了,近期会写,只能说标准库中到处都是精髓,且标准库中的上下界二分比你真的理解二分的写法吗 - 二分写法详解这篇博客中的二分更好理解。

猜你喜欢

转载自blog.csdn.net/FlushHip/article/details/82319086
今日推荐