LeetBook《高级算法》题解 II
[ code ]

因为博客有点过于长了,所以把 LeetBook《高级算法》题解分成了两部分。

《高级算法》的题目根据涉及到的知识点分为:数组和字符串,链表,树和图,回溯算法,排序和搜索,动态规划,数学,设计问题以及其他。这篇博客中记录了 动态规划,数学,设计问题,其他 这几个类别的部分题目的题解。

目前包含的题目如下:

动态规划

NO.140 单词拆分 II

题目: 给定一个字符串 s 和一个字符串字典 wordDict ,在字符串 s 中增加空格来构建一个句子,使得句子中所有的单词都在词典中。以任意顺序 返回所有这些可能的句子。

注意:词典中的同一个单词可能在分段中被重复使用多次。

一个例子:

输入:s = “catsanddog”, wordDict = [“cat”,”cats”,”and”,”sand”,”dog”]

输出:[“cats and dog”,”cat sand dog”]

题解:

虽然这道题被分到了动态规划类里面,但是看到这道题我的第一反应是用回溯算法来做。以下是采用回溯算法的题解:

1. 回溯算法:

用回溯算法结题的思路和上一题 单词搜索II 有点类似。首先将wordDict里面的所有字符串都存到哈希表里(如果想要更高效的话,也可以像上一题一样采用字典树来存储),然后从0下标开始,在s中截取不同长度的子串。如果wordDict里面包含这个子串,那么就暂时截去子串,然后对s中剩余的部分进行相同的操作,直到遍历完s中所有的字符。

此外还可以添加一个剪枝的操作。首先获取wordDict中字符串的最大长度maxlen,然后在截取s子字符串的时候限制子字符串长度不可以超过maxlen,这样能节约不少时间。

以下是代码:

class Solution {
public:
    unordered_map<string,int> dict;
    vector<string> rtn;
    int maxlen;
	
    vector<string> wordBreak(string s, vector<string>& wordDict) {
        //单词的最大长度
        maxlen = 0;
        for(string it: wordDict)
        {
            //存入哈希表,当然也可以构建字典树
            dict[it] = 1;
            int sz = it.size();
            maxlen = max(maxlen,sz);
        }
        string cur;
        dfs(s,cur,0);
        return rtn;
    }

    void dfs(string s, string cur, int st)
    {
        if(st == s.size())
        {
            //如果正好遍历到了s的末尾,说明cur是一个符合要求的答案,将其存入数组rtn
            int len = cur.size();
            cur = cur.substr(0,len-1);
            rtn.push_back(cur);
            return;
        }

        for(int ed = st; ed < s.size() && ed < (st + maxlen);ed++)
        {
            //截取不同长度的子串
            string tmp = s.substr(st,ed-st+1);
            if(dict[tmp] == 1)
            {
                //如果子串在worddict内,进入下一步
                string cur2 = cur;
                cur2 = cur2 + tmp;
                cur2 = cur2 + " ";
                dfs(s,cur2,ed+1);
            }
            //回溯
        }
    }
};

动态规划:

我去查了查这道题的简化版 NO.139 单词拆分。简化版中的题目只需要判断字符串s是否可以拆分,而不需要给出所有的拆分方案。

在139题中,假设s长度为n,可以用长为n的数组dp来记录下表范围为0到i的子串是否可以被拆分,如果可以,那么dp[i] = true,反之为false

同样的创建一个哈希表dict来存储wordDict里的单词(也可以用效率更高的字典树,但是我懒得写了),那么状态转移方程可以写为:

\[dp[i] = dp[j] \And dict.count(s.substr(j+1,i-j)), 0 \leq j < i\]

代码如下:

class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        //前缀树?
        int sz = s.size();
        vector<bool> dp(sz,false);
        unordered_map<string,int> map;
        int maxlen = 0;
        for(string it:wordDict)
        {
            map[it]++;
            int len = it.size();
            maxlen = max(maxlen,len);
        }
        //cout<<maxlen<<endl;
        if(map.count(s))
            return true;
            
        //首先寻找被拆分出的第一个单词
        for(int i = 1; i <= s.size();i++)
        {
            string tmp = s.substr(0,i);
            if(map.count(tmp))
            {
                dp[i-1] = true;
            }
        }
        int i = 0;
        while(i<sz && dp[i] == false)
            i++; 
        if(i==sz)
            return false;
        for(; i < sz;i++)
        {
            if(!dp[i])
            {
                int cur = max(0,i-maxlen);
                for(int j = i-1; j >= cur;j--)
                {
                    string tmp = s.substr(j+1,i-j);
                    dp[i] = dp[j] && map.count(tmp);
                    if(dp[i])
                        break;
                }
            }
        }
        return dp[sz-1];
    }
};

如果想在139题的基础上完成140题的题解,那么就要在dp[i]为true的时候记录下对应的拆分方案。

设计问题

NO. 295 数据流的中位数

题目:

中位数是有序列表中间的数。如果列表长度是偶数,中位数则是中间两个数的平均值。

例如,

[2,3,4] 的中位数是 3

[2,3] 的中位数是 (2 + 3) / 2 = 2.5

设计一个支持以下两种操作的数据结构:

题解:

这道题可以用优先队列来解。用两个有点队列queMax和queMin来存储输入的数字,其中queMax是小顶堆,存储较大的那一半数字,而queMin是大顶堆,存储较小的那一半,这样能够保证queMax和queMin的顶端一直都是排序在最中间的两个数。(这种做法似乎在很多题目中都出现过)

如果两个有些队列都为空,那么数据率先进入queMax。如果输入的数据大于queMax的顶端(也就是最小值),那么将其push进queMax,反之push到queMin里。

此外,为了保证两个队列的长度差不超过1,每输入一个新的数字,就要检查一下长度,如果其中某一个队列过长,就将其顶端数字push到另一个队列中。

在输出中位数时,如果两个队列长度相等,那么返回二者顶端元素的均值,否则返回长度较长者的顶端元素。

以下是代码:

class MedianFinder {
public:
    priority_queue<int, vector<int>, less<int>> queMin;
    priority_queue<int, vector<int>, greater<int>> queMax;

    MedianFinder() 
    {

    }

    //queMin 存储较小的那一半数字,queMax存储较大的一半
    void addNum(int num) {
        if (queMin.empty() || num <= queMin.top()) {
            queMin.push(num);
            if (queMax.size() + 1 < queMin.size()) {
                queMax.push(queMin.top());
                queMin.pop();
            }
        } else {
            queMax.push(num);
            if (queMax.size() > queMin.size()) {
                queMin.push(queMax.top());
                queMax.pop();
            }
        }
    }

    double findMedian() {
        if (queMin.size() > queMax.size()) {
            return queMin.top();
        }
        return (queMin.top() + queMax.top()) / 2.0;
    }
};

数学

NO. 179 最大数

题目:

给定一组非负整数 nums,重新排列每个数的顺序(每个数不可拆分)使之组成一个最大的整数。

注意:输出结果可能非常大,所以你需要返回一个字符串而不是整数。

示例:

输入:nums = [10,2] 输出:”210”

题解:

这道题我一开始没想出来做法,但是看了题解之后发现其实挺简单的……首先为了避免溢出,需要将输入的整数全部转为字符串存入一个vector中。由于C++能够对字符串进行排序,比如”21” > “12”,我们可以自定义一个vector排序函数,对于任意两个字符串a和b,如果a+b > b+a,就将a排在b的前面。最后将排序完成的字符串数组按照顺序输出即可。

代码:

class Solution {
public:
    string largestNumber(vector<int>& nums) {
        //将整数数组转换为字符串数组,排序后拼接
        vector<string> nums_new;
        for(int it: nums)
        {
            nums_new.push_back(to_string(it));
        }
        sort(nums_new.begin(),nums_new.end(),comp);
        string rtn;
        for(string it: nums_new)
            rtn += it;

        if(rtn[0] == '0')
            return "0";
        return rtn;

    }
	
	//排序函数
    static bool comp (const string& a, const string& b)
    {
        return (a+b) > (b+a);
    }
};

其他

NO. 406 根据身高重建队列

题目:

假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。

请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。

题解:

这道题难度是中等,但是我完全不会,看了官方题解才知道怎么做(感到智熄)。感觉这道题和计算右侧小于当前元素的个数有些类似,一个是根据右侧小于当前元素的个数推断出原来的顺序,另一个则相反。后者可以用插入排序的方法来解决。

要解决这道题,首先对people中的数组按照身高从小到大进行排序,如果有两个人身高相同,那么将hi较大的那个排在前面。

这样一来,排在前面的元素对应着矮个子的人。对于排序后的people[0],由于不会有人比他更矮了,所以其对应的hi值就是排在他前面的人的个数。此时我们新建一个与people大小相同的数组rtn存放结果,从rtn.begin()开始数hi个位置,那么people[0]应该被放在第hi+1个空位置上。对于people[0]之后的元素的操作也是相同的,都是将其放在第hi+1个空位置上。对于身高相同的两个人,其中必有一个人的hi值较大,我们在对people进行排序时,把hi较大的那个人排在前面,这样在遍历排序后的people时,可以先预留出足够的位置。

代码如下:

class Solution {
public:
    vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
        //按照身高从小到大排序,如果有人身高相同,那么ki较大的那一方放在前面
        //因为当身高相同时,ki较大的那一方应该在原序列中靠后,那么应该先对其进行排序
        sort(people.begin(),people.end(),comp);
        int n = people.size();
        vector<vector<int>> rtn(n);
        for(int i = 0; i < n; i++)
        {
            int k = people[i][1];
            int j = -1;
            while(k>=0 && j < n)
            {
                j++;
                if(rtn[j].empty())
                    k--;
            }
            rtn[j] = people[i];
        }
        return rtn;

    }

    static bool comp(vector<int> a, vector<int> b)
    {
        if(a[0] < b[0] || (a[0] == b[0] && a[1] > b[1]))
            return true;
        return false;
    }
};

其他

NO.41 接雨水

题目:

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

例子:

enter description here

输入:height = [0,1,0,2,1,0,1,3,2,1,2,1] 输出:6 (蓝色部分的就是能够接到的雨水)

题解:

这是一道比较热门的题目了(似乎在hot 100里?),可以用动态规划或者单调栈来做。

1. 动态规划

对于位于i下标的柱子,其能够接到雨水的量r[i]取决于左右两侧接到雨水的高度,为:r[i] = min(r[i-1],r[i+1])-height[i]。

我们首先从左向右遍历,获取每个柱子左侧雨水能达到的最大高度,再从右向左遍历,获得右侧雨水能达到的最大高度,就可以通过以上公式来解决问题了:

class Solution {
public:
    int trap(vector<int>& height) {
        int n = height.size();
        if (n == 0) {
            return 0;
        }
        vector<int> l(n,0);
        vector<int> r(n,0);
        //初始化最靠边的柱子
        l[0] = height[0];
        r[n-1] = height[n-1];

        //遍历获取左右两侧的雨水高度
        for(int i = 1;i < n; i++)
        {
            l[i] = max(l[i-1],height[i]);
            r[n-i-1] = max(r[n-i],height[n-i-1]);
        }

        int rtn = 0;

        //去左右雨水高度中的较小值
        for(int i = 0; i < n; i++)
        {
            rtn += min(l[i],r[i])-height[i];
        }
        return rtn;
    }
};

2. 单调栈

单调栈内的元素从栈底到栈顶是单调递增/减的。以单调递减栈为例,其顶端元素应该是栈内最小的元素。如果新加入的元素tmp比栈顶元素大,那么应该不停地pop出栈顶元素,直到新的栈顶元素大于tmp,再将tmp存入栈中。

对于第i根柱子,只有其前后方各找到一根比它更高的柱子,才可以接水。本题维护一个单调递减栈,从左到右遍历数组,当单调栈内的元素数量大于等于2,且当前遍历到的柱子高度大于栈顶元素,就得到了一个可以接水的区域(栈顶元素最低,其左侧右侧都有一个偏高的柱子)。此时接雨水的高度为:min(height[left],height[right])-height[top],其中left就是栈顶元素下一个的元素(大于等于栈顶)。而雨水的宽度为right-left-1。

将top出栈后可以获得left的高度,此后left就变成了新的top。

代码:

class Solution {
public:
    int trap(vector<int>& height) {
        int rtn = 0;
        stack<int> stk;
        int n = height.size();
        for (int i = 0; i < n; ++i) {
            //当前柱子高度大于栈顶元素
            while (!stk.empty() && height[i] > height[stk.top()]) {
                int top = stk.top();
                stk.pop();
                if (stk.empty()) {
                    break;
                }
                //栈中元素数量大于等于2
                int left = stk.top();
                int w = i - left - 1;
                int h = min(height[left], height[i]) - height[top];
                rtn += w * h;
            }
            stk.push(i);
        }
        return rtn;
    }
};

NO. 84 柱状图中的最大矩形

题目:

给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。

求在该柱状图中,能够勾勒出来的矩形的最大面积。

示例:

enter description here

如上图所示的输入,最大矩形面积应为10。

题解:

这道题同样可以用单调栈来求解。我们选中第i根柱子,如果想要找到包含第i根柱子的最大矩形,那么需要分别向左和向右遍历寻找第一根高度小于它的柱子。第i根柱子的高度为矩形的高,这两根柱子之间的距离即为矩形的宽。

上述寻找左右边界的过程可以采用单调递增栈来实现,如果当前遍历到的元素小于栈顶元素,那么说明找到了第一根高度小于栈顶元素的柱子。

对于每一根柱子,我们都计算包含这根柱子的矩形的最大面积,最后返回这些矩形面积的最大值。

代码:

class Solution {
public:
    int largestRectangleArea(vector<int>& heights) {
        if(heights.size() == 1)
            return heights[0];
        //首尾都加上0,避免无法计算到边界情况
        heights.push_back(0);
        heights.insert(heights.begin(),0);
        int n = heights.size();

        //记录包含每一根柱子的矩形最大值
        vector<int> area(n,0);
        stack<int> st; //记录左右边界
        //从左向右遍历,寻找右边界
        for(int i = 0; i < n;i++)
        {
            while(!st.empty() && heights[st.top()] > heights[i])
            {
                int tmp = (i-st.top())*heights[st.top()];
                area[st.top()] += tmp;
                st.pop();
            }
            //如果此时stack内非空,那么顶端元素是向左遍历的第一根小于i的柱子
            //这里宽度-1,避免第i根柱子的面积被计算2遍。
            if(!st.empty())
                area[i] += (i-st.top()-1)*heights[i];
            st.push(i);
        }

        int rtn = 0;
        for(int it:area)
            rtn = max(rtn,it);
        return rtn;
    }
};