2020-03-14Leetcode刷题

今天的专题是二分法——二分答案,英文叫Binary Search on Result

这种题的特点是往往没有给你一个数组让你二分同样是找到满足某个条件的最大或者最小值


先总结两道相似的题,两道题来自Lintcode,因为Leetcode上没有这两道题

183. 木材加工

完整的c++代码如下:

class Solution 
{
public:
    int woodCut(vector<int> &L, int k) 
    {
        // write your code here
        int l = 1;
        
        int r = 0;
        for (int item : L)
        {
            r = std::max(r, item);
        }
        
        while (l + 1 < r)
        {
            int mid = l + (r - l) / 2;
            if (count(L, mid) >= k)
            {
                l = mid;
            }
            else
            {
                r = mid;
            }
        }
        
        
        if (count(L, r) >= k)
        {
            return r;
        }
        
        if (count(L, l) >= k)
        {
            return l;
        }
        
        return 0;
    }
    
private:
    int count(vector<int> &L, int len)
    {
        int sum = 0;
        for (int item : L)
        {
            sum = sum + item / len;
        }
        
        return sum;
    }
};

当跳出while循环后,L = L, mid = L + 1, R = R + 2。
L表示Mid左边的最大值,R表示mid右边的最小值。
跳出while循环后,重新再验证一下就好。

算是一个模板题,再看看下一道。


437. 书籍复印

不过这个题比较复杂啦。它的难点在于check()函数的逻辑。(当然这个题我还没有完全把逻辑盘对!)
就是判断当前的左右边界L和R所确定的二分边界mid是否何理,不合理的话,则更新边界R或者L。

check中有一个num变量,表示复印完 n 本书需要的人数
核心是2点:

  • 将尽可能多的书分给同一个人
  • 判断复印完 n 本书需要的人数num是否不大于 k.

这个题刚开始比较懵,是被check()函数中的left变量迷惑了。一直把left当下界看,当成一个向量在看,从mid出发,指向左无穷大的向量。导致一直没理解,其实Left变量就是表示一个标量,想象成一条线段,表示还可以使用的时间额度。刚开始没有,left=0,说明没有可用的时间额度,那么来一个人,多一个人就增加了时间额度。
如果当前item在当前的时间额度中,就是item<=left,那么这个item复印完后,left清为0,或者left - item。很好理解。其他的就是模板。

因为书籍复印一开始没太懂,所以在VS中调试了,下面给出VS2019调试的代码——

#include "C.h"

vector<int> pages = { 3, 2, 4 };
int k = 3;

bool check(vector<int>& pages, int limit, int k)
{
    int num = 0;
    int left = 0;

    for (int item : pages)
    {
        // 如果当前书的页数比mid大,说明mid边界取错了
        // 我们让left=0表示假定ans的范围是在left~mid之间
        // 但如果我们复印第i本书花的时间比limit大,那说明当前确定的范围是错的
        if (item > limit)
        {
            return false;
        }

        // 当item比left大的时候,说明ans是在left~limit之间的
        // 那么说明第i本书可复印,num++
        // 当前第1次这个item是3,比left左边界为0要大
        // 但我们知道当前item=3是<=limit的
        if (item > left)
        {
            num++;

            left = limit;
        }

        left = left - item;
    }

    // num是什么,为什么要拿num和k比较呢?
    // num是复印完 n 本书需要的人数
    return num <= k;
}

// Binary Search
int copyBooks(vector<int>& pages, int k)
{
    // write your code here
    int l = 2;
    int r = 9;

    // template
    // while()函数的目的就是在不断地确定左右边界l和r
    while (l + 1 < r)
    {
        int mid = l + (r - l) / 2;

        // check()函数的作用就是判断ans到底是在mid左边还是mid右边
        if (check(pages, mid, k))
        {
            r = mid;
        }

        else
        {
            l = mid;
        }
    }

    if (check(pages, l, k))
    {
        return l;
    }

    return r;
}

int print()
{
    int ans = copyBooks(pages, k);
    printf("%d", ans);
    return 0;
}


int main()
{
    copyBooks(pages, k);
    print();
}

下面的题则是Leetcode中的题:

162. Find Peak Element

这道题使用了迭代二分查找算法。

思路:

  1. 首先我们要明白,一个数组中,可以有2个或者多个峰值元素。
  2. 假如当前中间这个元素mid比右边的第一个元素大,那就说明当前元素满足了当峰值元素的一半条件,我们找到了一个所谓的“备胎”,这样子就不用再往右看了!
    这时候,将当前元素当成屏障。也就是把右边界移至当前元素的位置(r = mid)。这样可以将新的mid往屏障前面推,得到新的元素。
  3. 得到的新的元素mid如果比右边的元素小,那么说明当前这个元素不能当做峰值元素,那么就继续往右找,因为L < R,所以一定可以找到一个元素,要不然就是上面2中定的呢个元素,要不然就是L在右移的过程中,碰到了比上面2中更大的元素,这样更大的就是答案。
  4. 第一次就可以判断出,答案是在左边范围还是右边范围!然后不断缩小范围!
// 迭代二分查找
class Solution 
{
public:
    int findPeakElement(vector<int>& nums) 
    {
        int l = 0;
        int r = nums.size() - 1;

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

        return l;
    }
};

Lintcode75. 寻找峰值
再用下九章的解法,不过九章对应的是

两个题不太一样,我就分开总结吧。今天有点累,也没精力总结两道题了。后面一定要总结!!

Lintcode上通过了,但是时间复杂度好高!
完整代码:


class Solution 
{
public:
    int findPeak(vector<int>& A) 
    {
        // if (A.size() <= 1) return 0;
        
        int start = 1;
        int end = A.size() - 2;

        while(start + 1 < end)
        {
            int mid = start + (end - start) / 2;
            // mid的求法和二分答案的不一样
            if (A[mid] > A[mid + 1])
            {
                end = mid;
            }
            else
            {
                start = mid;
            }
        }

        if (A[start] < A[end])
        {
            return end;
        }
        else
        {
            return start;
        }
    }
};
  • 首先,数据上,LintCode 保证数据,第一个数比第二个数小倒数第一个数比到倒数第二个数小
  • 每次mid求出的中间元素,如果start和end之间有4个元素,偶数个,则中间元素是较小的呢个,因为(end-start)/2是向下取整。

二分法。

每次取中间元素,如果中间元素大于左右,则这就是peek。
否则取大的一边。
如果左右两个都比自身大,可以随便取一边。
最终会找到peek。

正确性证明:

A[0] < A[1] && A[n-2] > A[n-1] 所以一定存在一个peek元素。
二分时,选择大的一边, 则留下的部分仍然满足1 的条件,即最两边的元素都小于相邻的元素。所以仍然必然存在peek。
二分至区间足够小,长度为3, 则中间元素就是peek。

核心就是:如果mid元素比右边大了,那么这个mid元素就作为新的右边界。只需要找左边比它小的元素了。
如果mid元素比右边小了,则这个mid元素作为新的左边界。
while条件是(start + 1 < end),也就是剩start、mid、end最后三个元素的时候。
我们这么看,最后一次进while循环的情况,一定是这样——
start = start, end = start + 2,
然后
mid = start + 1
这样,如果中间元素比右边大,那么中间元素作为峰值元素候选人,end = mid。
这时就剩下2个元素,如果左大,那就是左边的元素,因为左边的元素再往左的呢些元素,已经被pass掉了,它们就是因为比右边的小,才被pass的。

如果中间元素比右边小,则end对应的元素是峰值元素,我们再比较一次start和end,如果start大,则start也比mid大,start是峰值。
如果是end,那么end是峰值,因为end右边的所有元素也是因为比它小,所以pass掉了。


69. Sqrt(x)

技巧:

  1. 直接对答案可能存在的区间进行二分 => 二分答案
  2. 判断区间的时候一个小技巧: mid * mid == x 中使用乘法可能会溢出,写成 mid == x / mid 即可防止溢出,不需要使用long或者BigInteger
  3. 剩下的是一样的,用下面这个代码的话,最后剩下的,就一定是两个元素,start和end。
  4. 如果end的平方比x大,则说明end不符合,返回start。如果end的平方比x小,那么则返回end。因为end是最接近sqrt(x)的值。

不过这道题虽然会了,但是对于start和end平方后的大小和x之间的比较还是不太明白。
譬如如果end平方比x大,返回了start,你怎么证明start的平方不会比x大?


33. Search in Rotated Sorted Array

这道题可能会总结得有些水,确实有些困了。

完整代码:

class Solution 
{
public:
    int search(vector<int>& nums, int target) 
    {
        if (nums.empty() || nums.size() == 0)
        {
            return -1;
        }

        int start = 0;
        int end = nums.size() - 1;
        
        while(start + 1 < end)
        {
            int mid = start + (end - start) / 2;
            if (nums[mid] == target)
            {
                return mid;
            }

            if (nums[start] < nums[mid])
            {
                if (nums[start] <= target && target <= nums[mid])
                {
                    end = mid;
                }
                else
                {
                    start = mid;
                }       
            }
            else
            {
                if (nums[mid] <= target && target <= nums[end])
                {
                    start = mid;
                }
                else
                {
                    end = mid;
                }
            }
        }

        if (nums[start] == target)
        {
            return start;
        }

        if (nums[end] == target)
        {
            return end;
        }

        return -1;
    }
};

首先,我不明白这个解法,为什么是通过start和mid的大小,来划分两个区间

也许是因为,我们的测试数据,是将一个升序数组的子数组反转了一下。
所以整个数组变换后,就是分成了2段,2段都是各自升序的。
所以我们先看start元素和Mid谁大,如果start比mid小,就先看start~mid这个区间。如果target在这个区间,则将end移动到mid的位置,也就是向左缩小了查找范围。
如果target不在这个区间,则将start移动到mid的位置,则是向右缩小了查找范围。

同理,如果start对应的元素比mid的大,将将注意力转移到mid~end这个区间。
如果target在这个区间,则将start向右移动到mid,向右缩小范围。
不在,则将end向左移动到mid,向左缩小范围。

最后,目标还是会缩小在start和end这俩身上,谁对应上了,就返回谁的下标。都没对应上,返回-1。


278. First Bad Version

完整代码:

// Forward declaration of isBadVersion API.
bool isBadVersion(int version);

class Solution 
{
public:
    int firstBadVersion(int n) 
    {
        int start = 1;
        int end = n;

        while(start + 1 < end)
        {
            int mid = start + (end - start) / 2;
            if (isBadVersion(mid))
            {
                end = mid;
            }
            else
            {
                start = mid;
            }
        }

        if (isBadVersion(start))
        {
            return start;
        }
        return end;
    }
};

用循环不变式来确定怎么写二分。

初始状态:答案肯定在[1,n]之间, 保证边界l不大于r。

每次循环:

  • isBadVersion(mid) == false 答案肯定在[mid+1,r] 之间。
  • isBadVersion(mid) == true 答案肯定在[l, mid] 之间。

确定输入条件防止死循环:

  • 条件1中: l = mid+1, 那mid的值是l-1的话求解区域维持不变,就会死循环,然而除非r < l 不然mid不可能比l小, 所以不可能发生。
  • 条件2中: r = mid, 那mid的值为r的话求解区域维持不变,会发生死循环。往回推 l = r 时有mid = l = r, 那么需要保证l != r 即 l < r 不会死循环。
    所以while的条件至少要保证l < r, 如果想放松点 l+1 < r 也可以 。

终止条件:

  • 给定 l < r的条件则l r 终止时指向相同元素(l == r), 返回l或r都没有区别。
  • 如果一定要用放松一点的条件 l+1 < r, 终止时边界指向相邻元素 比比返回需要的那个。

最后除了(l+1 < r)的代码外,再多一种——(l < r)这个版本的代码

class Solution {
public:
    int findFirstBadVersion(int n) {
        // write your code here
        int l = 1, r = n;
        while (l < r) {
            int mid = l + (r-l) / 2;
            if (isBadVersion(mid) == false) {
                l = mid+1;
            } else {
                r = mid;
            }
        }
        return l; // return r is valid as well
    }
};

两个写法的区别有两处:

  1. while循环中,start = mid 和 l = mid + 1。
  2. 最后返回答案的时候——
if (isBadVersion(start))
        {
            return start;
        }
        return end;

 return l;

因为第二种写法,l的更新一直是l = mid + 1。所以最后跳出while循环后,只剩下l对应的元素了。
第一种写法,跳出while循环后,最后剩start和end,所以再多判断一次。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,922评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,591评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,546评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,467评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,553评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,580评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,588评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,334评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,780评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,092评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,270评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,925评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,573评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,194评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,437评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,154评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容