【二分法】小朋友分巧克力问题

我觉得算法也就那样,也不见得有多难啊。

上一讲的尺取法是不是太简单了?让你产生「算法不过尔尔」的错觉。看来本讲要稍稍加码,讲一个复杂一点点的算法。

神秘人:那就让我上场吧。

哦吼,你又是 who?

它可是大名鼎鼎的「二分法」,一种思路简单、编码容易、应用面广、效率极高的基本算法。

嘻嘻,过奖了过奖了。

而且,比赛似乎很喜欢出二分法的题目呢。

二分?没听说过,不过我现在倒是快 饿疯了。尽快开始吧,我妈妈喊我回家吃饭了。。

二分法有「整数二分」和「实数二分」两种情况。实数二分的代码好写不易出错;整数二分的代码因为要考虑整除的问题,代码容易出错。

说说看吧,你的算法思想是啥?

二分法:我的算法思想?那可是太简单了,简单到小学一年级都懂。

真的假的?你也别卖关子了,直接说吧。

二分法:这样吧,我们来做个游戏:给你一个1~100之内的数字,你必须要在 7 次内猜出这个数字,怎么猜?

这还能怎么猜,看运气呗。

二分法:…。我给你演示一下我的方法: 例如这个数是54,我问你7次:

大于等于50吗?是。(1~100二分,中位数是50)
大于等于75吗?否。(50~100二分,中位数是75)
大于等于63吗?否。(50~75二分,…)
大于等于56吗?否。
大于等于53吗?是。
大于等于54吗?是。
等于55吗?否。
那么这个数等于54。懂个大概了吗?

nb啊,这原理也太简单易懂了,根本不需要额外解释!不过要如何编码呢?

二分法:这个逻辑不复杂,我相信你肯定能编码出来。

你试着练习练习吧,并且记录一下自己的编码时间。如果你从没有编过二分法的代码,就能在 2020​ 分钟内编码出来,恭喜你,你的编码水平已经很高了!不过我严重怀疑 95%​ 的萌新无法在 3030​​ 分钟内写出二分法的完全正确的代码。

呜呜呜,大佬求个代码。

好吧,下面我会给出二分法的模板代码 bin_search() 函数。bin_search() 操作 3​ 个变量:区间左端点 left、右端点 right、二分的中位数 mid。每次把区间缩小一半,把 left 或 right 移动到 mid;直到 left = right 为止,即找到了答案所处的位置。

模板代码
大佬:下面是我代码运行的结果。

51 76 64 58 55 53 54

哦,原来这就是二分法,这个算法真的好简单。

简单是简单,但是它的效率极高。比如在有序的n个数中找某个数,只需要二分 log2底n次,也就是说它的时间复杂度是 O(log_2底n)的。例如有 n = 10^7 个数,只需要 log_2底10^7 =24 次就能找到呢。

总结一下吧:二分法把一个长度为 nn 的有序序列上 O(n)的查找时间,优化到了O(log_2n)注意,这个序列的数字一定要是「有序」的,二分才有意义。在乱序的一堆数字上搞二分,没有任何用处。所以这个数字序列如果是乱序的,应该先排序再二分。

原来如此。诶?不对呀大佬!先排序的话,排序复杂度是 O(nlogn)​,再二分是 O(logn)。排序加二分,总复杂度是 O(nlogn)。但是如果暴力搜,直接在乱序的 n个数里面找,最多找 n 次,复杂度是O(n)​ 的,比你排序再二分快多了!

对对对,你说得对,你都对。

二分法:那如果强化一下上述查数字问题:「现在不是查一个数,而是查 m 个数。那么排序再二分就是 O(nlogn+mlogn),而你的暴力法是 O(mn) 的。现在二分是不是快多了?

orz。
如果前面你只是看了看代码,并没有自己动手写代码,我想你会觉得:「二分法不过尔尔,我已经掌握了」,对吧?

em,我并没这么…

不!如果你自己写了上述代码,就会体会到,二分易懂不易写,容易出错。

接下来我会给出几种常见的二分法代码,你可看仔细了。

好的!

单调递增序列中找 x 或者 x 的后继
大佬:「在单调递增序列中找 xx 或者 xx 的后继」,即在单调递增数列 a[]a[] 中查找某个数 xx,如果数列中没有 xx,则找比它大的下一个数。前面所给出的 bin_search() 函数就是「在单调递增序列中找 xx 或者 xx 的后继」的模板代码。二分法有很多编码方法,这里给出的区间是左闭右开的编码,即 [0, n)。也可以直接对 [0, n-1] 区间二分,代码略有不同。

模板代码
当 a[mid] >= x 时,说明 xx 在 midmid 的左边,新的搜索区间是左半部分,leftleft 不变,更新 right = mid。
当 a[mid] < x 时,说明 xx 在 midmid 的右边,新的搜索区间是右半部分,rightright 不变,更新 left = mid + 1。
代码执行完毕后,left = right,两者相等,即答案所处的位置。代码很高效,每次把搜索的范围缩小一半,总次数是 log(n)。
在单调递增序列中查找 x 或者 x 的前驱
大佬:接下来是「在单调递增序列中找 xx 或者 xx 的前驱」的模板代码。

模板代码
当 a[mid] <= x 时,说明 xx 在 mid 的右边,新的搜索区间是右半部分,所以 rightright 不变,更新 left = mid;
当 a[mid] > x 时,说明 xx 在 mid 的左边,新的搜索区间是左半部分,所以 leftleft 不变,更新 right = mid – 1。
同样可以分析出,当 a[mid] > x 时,不能写成 right = mid,会导致 while() 的死循环。

你咋不说话了。

大佬 nb。

题目描述
儿童节那天有 K 位小朋友到小明家做客。小明拿出了珍藏的巧克力招待小朋友们。
小明一共有 N块巧克力,其中第 i 块是 Hi * Wi的方格组成的长方形。为了公平起见,小明需要从这 N 块巧克力中切出 K 块巧克力分给小朋友们。切出的巧克力需要满足:
形状是正方形,边长是整数;大小相同;例如一块 6×5​ 巧克力可以切出 62×2​ 的巧克力或者 23×3 的巧克力。

当然小朋友们都希望得到的巧克力尽可能大,你能帮小明计算出最大的边长是多少么?

输入描述
第一行包含两个整数 N,KN,K (1<=N,K≤10^5 )。
以下 N 行每行包含两个整数 Hi,Wi
(1<=Hi, W10^51≤Hi,Wi≤10^5)。

输入保证每位小朋友至少能获得一块  11×1 的巧克力。

输出描述
输出切出的正方形巧克力最大可能的边长。

样例输入
2 10
6 5
5 6
样例输出
2

让我先试试暴力法吧。

哦?你想怎么暴力?

暴力的思路很简单:把边长从 11​ 开始到最大边长 DD,每个值都试一遍,一直试到刚好够分的最大边长为止。
编码:边长初始值 d=1d=1​​,然后 d=2,3,4\cdotsd=2,3,4⋯​ 一个个试。
下面是暴力代码,大家有兴趣自己写一遍,当作一道模拟题来练习。

暴力做法

嗯想法不错,可惜 n个长方形,长方形的最大边长 D,复杂度是 O(nD),而 n和 D 的最大值是 10^6,暴力法会超时。
是唉,一个个试边长 dd 太慢了,要是能快速找到就好了。

嘿嘿,可以用前面刚学的二分呀,让我们祭出二分大法!有请二分大法。

二分法:hello 大家好我又来了。对于这道题的 d,我们可以按小学生的猜数方法,猜 d 的取值。即对边长 d 的取值范围二分,这样复杂度一下子从 O(D) 优化到了O(logD)。

噢噢,原来如此。

注意呀:整数的二分法的编码虽然简单,但是很容易出错。左右边界 LL​、R​ 和中间值 mid的迭代,由于整数的取整问题,极易出错。下面的整数二分代码,给出了两种写法,请仔细体会。

JAVA

import java.util.Scanner;
public class Main {
    private static int maxn = 100000;
    public static void main(String[] args) {
        int n,k;
        int[] h = new int[maxn];
        int[] w = new int[maxn];
        Scanner scanner = new Scanner(System.in);
        n = scanner.nextInt();
        k = scanner.nextInt();
        for (int i = 0; i < n; i++) {
            h[i] = scanner.nextInt();
            w[i] = scanner.nextInt();
        }
        int ans = 0;
        int right = maxn + 1;
        int left = 1;
        while (left<=right){
            int mid = (left + right) >> 1;
            int cnt = 0;
            for (int i = 0; i < n; i++) {
                cnt += (h[i] / mid) * (w[i] / mid);
            }
            if (cnt >= k){
                left = mid + 1;
                ans = mid; 
            }else
                right = mid - 1;
        }
        System.out.println(ans);
    }

PYTHON

def check(d):
    global w,h
    res = 0
    for i in range(len(w)):
        res += (w[i]//d) * (h[i]//d)
    if res >= k:  return True
    return False
n,k = map(int,input().split())
w = []
h = []
for i in range(n):
    a,b = map(int,input().split())
    w.append(a)
    h.append(b)
L ,R = 1, 100000
while L < R:
    mid = (L+R)//2
    if check(mid):  L = mid +1
    else :          R = mid
print(L-1)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

007的米奇妙妙屋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值