LeetCode之路:455. Assign Cookies

一、引言

这是一道非常非常有趣的题目,有趣到我睡前看到了这道题,拼着明天上班没精神的后果都要撑着把这篇博客写完。

那么这道题究竟哪里有趣了呢?同样是一道题意简单、做出来也简单,甚至于最高票答案的思路都是跟我们的思路一样一样的题目;但是这道题却与众不同,其看似轻松平常,却可以用一般人想不到的方式来完成这个问题,实在让人有些感叹不已。

说了这么多前言,还是让我们看看题目吧:

Assume you are an awesome parent and want to give your children some cookies. But, you should give each child at most one cookie. Each child i has a greed factor gi, which is the minimum size of a cookie that the child will be content with; and each cookie j has a size sj. If sj >= gi, we can assign the cookie j to the child i, and the child i will be content. Your goal is to maximize the number of your content children and output the maximum number.

Note:
You may assume the greed factor is always positive.
You cannot assign more than one cookie to one child.

Example 1:
Input: [1,2,3], [1,1]
Output: 1
Explanation: You have 3 children and 2 cookies. The greed factors of 3 children are 1, 2, 3.
And even though you have 2 cookies, since their size is both 1, you could only make the child whose greed factor is 1 content.
You need to output 1.

Example 2:
Input: [1,2], [1,2,3]
Output: 2
Explanation: You have 2 children and 3 cookies. The greed factors of 2 children are 1, 2.
You have 3 cookies and their sizes are big enough to gratify all of the children,
You need to output 2.

这道题的英文描述非常的多,看似非常复杂,其实内涵的逻辑非常简单,这里为了方便阅读,我还是翻译一下吧:

假如你是一个非常好的家长想要给你的孩子们分配一些曲奇饼干。但是呢,你又必须至少给每一个孩子至少一块整的曲奇饼干。每一个孩子 i 都拥有一个贪婪指数 gi,这个 gi 呢,是这个孩子能够满足的曲奇饼干的单位数量值;每块曲奇饼干 j 都有一个数量值 sj 。如果 sj >= gi,也就是曲奇饼干的数量值大于了孩子的能够满足的数量值,我们就可以将这块曲奇饼干分配给这个孩子,这个孩子也将得到满足。我们的任务就是尽可能的满足最多的孩子,并且算出能够满足的最多的孩子的数量。

注意:
你可以假定贪婪指数永远为正整数。
你不能向一个孩子分配更多的曲奇饼干。

例子1:
输入:[1, 2, 3] ,[1, 1]
输出: 1
解释:你现在有 3 个孩子,2 块曲奇饼干,其中 3 个孩子的贪婪指数分别为 1、2、3, 但是你现在只有 2 块曲奇饼干,而且这两块曲奇饼干的数量都为 1,因此你只能满足 1 个孩子,所以你需要输出 1。

例子2:
输入:[1, 2], [1, 2, 3]
输出:2
解释:你现在拥有 2 个孩子, 3 块曲奇饼干,孩子们的贪婪指数分别为 1、2,你现在拥有 3 块曲奇饼干并且都可以满足孩子们的需求,所以你需要输出 2。

(天哪,这道题我翻译得好累 T_T)

根据题意,当我们去掉了复杂的背景描述后,其实就是一个非常简单的要求:

我们得到了两个数组,要求数组 1 中小于等于数组 2 中元素的个数。

将上面这么复杂的描述抽象成了这么简单的一句话,我们的工作量其实就完成了一半了,剩下的就是如何完成这个逻辑了。

二、模范家长:用曲奇饼干喂孩子们

那么这道题应该怎么做呢?

让我们来看这么一个逻辑:

最小的曲奇饼干必然要分配给贪婪指数最小的孩子,最大的曲奇饼干必然要分配给贪婪指数最大的孩子;至于最后究竟能成功分配几个孩子,再容后计算

当我们想明白了这么一个逻辑,我们需要做的第一步就很明确了,那就是对这两个数组进行排序。怎么排序都是可以的,这里我选择了从小到大的排序。

那当我们排好了序,之后如何模拟曲奇饼干的分配过程呢?

其实这个问题很简单:

  1. 首先,我们拿着最小的曲奇饼干去贪婪指数最小的孩子,看是否能够满足他;如果不能满足,则拿着稍大一点的曲奇饼干去喂他,看能否满足他……直到,我们找到了第一块可以满足贪婪指数最小的孩子的曲奇饼干,我们完成了第一步的操作

  2. 然后,我们在第一步中已经找到了最小的曲奇饼干并且满足了贪婪指数最小的孩子,那么这一块曲奇饼干就已经属于这个孩子了,这个孩纸和饼干都不能再进入分配过程了;然后我们继续拿着稍大一块曲奇饼干去喂贪婪指数稍大一些的孩子……一直重复着这样的逻辑即可

  3. 最后,当我们发现手上已经没有任何一块曲奇饼干可以满足我们当前需要满足的孩子了,或者我们手上仍有曲奇饼干,但是孩子已经全部被满足了,那么我们就可以说,分配过程到此结束了

上面的逻辑分析的非常清晰明了,因此代码也就出来了:

// my solution 1 , runtime = 43 ms
class Solution1 {
public:
    int findContentChildren(vector<int>& g, vector<int>& s) {
        int content_count = 0;
        int cookie = 0, child = 0;
        sort(g.begin(), g.end());
        sort(s.begin(), s.end());
        while (true) {
            while (cookie != s.size() && child != g.size() && s[cookie] < g[child]) ++cookie;
            if (cookie < s.size() && child < g.size()) {
                ++content_count;
                ++cookie;
                ++child;
            }
            else {
                break;
            }
        }
        return content_count;
    }
};

这段代码有必要解释下:

  1. 首先,我进行了两个数组从小到大的排序,方便后续的分配过程

  2. 其次,我设计了一个外层循环,模拟每次的分配过程(直到有一块曲奇饼干被分配出去或者分配过程结束)

  3. 最后,我在循环中设计了一个内层 while 循环,用于寻找当前第一块可以满足当前孩子的曲奇饼干

  4. 其中需要注意的是判断分配结束条件:要么曲奇饼干没有了,要么曲奇饼干没有能够满足孩子的了,要么孩子已经全部被满足了

清楚了上面的逻辑,其实这道题代码的编写也不是那么困难。

那么,这道题到目前为止,也没有像引言一样说的那么有趣嘛,不就是一道中规中矩的题目吗?

哈哈,有趣的东西当然要放到后面讲解了!

三、嘿嘿嘿,没想到吧:std::priority_queue 的横空出世

前面我们已经说到了这么一点,为了方便后面的分配过程,我们使用了排序操作。

那么这里我们就要思考了,有没有更佳的能够代替有序数组的东西呢?

当我点开了 Discuss 栏目查看最高票答案的时候,我震惊了:

C++ Two Priority_Queue Solution

使用优先级队列?
使用优先级队列?!
使用优先级队列!!!

一连串的疑惑,让我们先来复习下相关的内容:

queue(线性表)
队列是一种特殊的线性表,是一种先进先出(FIFO)的数据结构。它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列中没有元素时,称为空队列。

了解了 queue,我们再来看下 priority_queue 的内容:

priority_queue (优先级队列)
首先它是一个 queue,只允许在低端加入元素,并从顶端取出元素,除此之外别无其他取出元素的方式(故 priority_queue 不提供遍历功能,也不提供迭代器);再次它具有 priority,即 queue 中的元素具有一定的 priority;其内的元素自动按照元素的权值排列,权值最高者,排在最前面。

了解了 priority_queue 的内容后,我们来看看 STL 中对于 priority_queue 的支持:

std::priority_queue
定义于头文件 <queue>
想要了解具体内容请点击这里 C++参考手册之 std::priority_queue

对于 std::priority_queue 的用法,有一点需要特别注意:

当默认初始化的时候,默认队头为权值最大的元素,也就是递减排列的顺序,也就是说当你打印出来时,第一个元素是最大的,也就是 pop 的第一个元素是最大的元素。

但是你可以更改这种排列顺序:

std::priority_queue<int, std::vector<int>, std::greater<int> > q2;
for(int n : {1,8,5,6,3,4,0,9,7,2})
   q2.push(n);
print_queue(q2);

这是 C++ 参考手册中的示例代码,这处代码的输出结果是:

0 1 2 3 4 5 6 7 8 9

我们可以看到,std::priority_queue的构造函数中,第一个参数为元素的类型,第二个参数为队列使用的底层数据结构,第三个参数为元素的比较方式,这里使用了 std::greater 仿函数,实现了队列的升序排列(也就是说,当你打印出来的时候,第一个 pop 的值也就是队头的值是最小权值)。

还想要详细了解的同学可以自行搜索相关资料,这里已经完成了能够完成这道题目的相关知识储备,那就让我们开始使用这个新奇的玩意儿 std::priority_queue 做题吧:

  1. 首先,我们使用给定的两个数组初始化两个优先级队列(此时我们已经得到了两个从队头到队尾从小到大排列的优先级队列)

  2. 然后,我们设计一个外层循环,只要两个队列不为空,循环就一直继续

  3. 再然后,我们访问曲奇饼干的队头(数量最少的那份曲奇饼干),去判断它能否满足孩子们的队头(也就是贪婪数值最小的孩子),如果不可以的话,我们就删除这块曲奇饼干,查看下一块稍大的曲奇饼干能够满足当前孩子…….;或者满足了当前孩子,我们删除当前的曲奇饼干和当前孩子,拿着下一份稍大的曲奇饼干去喂下一个贪婪值较大的孩子……

  4. 直到两个队列有其一为空,循环结束

怎么样!

使用 std::priority_queue虽然没有带来性能上的提升,也没有带来任何逻辑处理上的区别,但是却带来了一点不可替代的好处:

程序的数据处理逻辑更加地语义化了

如果我们只是使用一个简简单单的数组,代码的阅读者可能并不会想到这里逻辑处理时排序的重要性;但是我们使用了优先级队列,我们的代码阅读者就能够清晰地得知,这里代码的作者想要使用一种排序思维,来完成业务想要的需求,或者说这里作者进行了 pop 操作,想要删除一个元素,是因为这个曲奇饼干已经被分配了等等。

最后奉上我根据 std::priority_queue 写出来的代码:

// perfect solution use priority_queue , runtime = 46 ms
class Solution3 {
public:
    int findContentChildren(vector<int>& g, vector<int>& s) {
        priority_queue<int, vector<int>, greater<int>> priority_g(g.begin(), g.end());
        priority_queue<int, vector<int>, greater<int>> priority_s(s.begin(), s.end());
        int content_count = 0;
        while (!priority_g.empty() && !priority_s.empty()) {
            if (priority_g.top() <= priority_s.top()) {
                priority_g.pop();
                priority_s.pop();
                ++content_count;
            }
            else {
                priority_s.pop();
            }
        }
        return content_count;
    }
};

我觉得,代码真美 ^_^

四、总结

这是一道非常非常有趣的题目,让我了解到了 priority_queue 的妙用。

曾几何时,我觉得只要有 std::mapstd::setstd::vector ,好像我的世界就完美了。

可惜我现在发现我错了:

世界那么大,除了 map、set、vector,在未知的山的那边海的对岸,或者还有这像 priority_queue 这样的新奇的存在

为了这道题,我拼着明天上班犯困的极大风险,也要把这篇博客赶出来,因为我看到了 priority_queue 这样的存在,我是那么的兴奋!

世界真的很大,真的很大,我们只是一个小小的水手,我们不能满足于面前的这片小港湾,还有着这样或那样的美好事物在等待着我们。

加油吧,为了未知的美好!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值