文章目录
- 1801. 积压订单中的订单总数
- 方法1:模拟+优先队列
- part1
- priority_queue的使用
- part2
- 求余
- 代码
1801. 积压订单中的订单总数
LeetCode: 1801. 积压订单中的订单总数
中等 \color{#FFB800}{中等} 中等
给你一个二维整数数组
orders
,其中每个orders[i] = [price_i, amount_i, orderType_i]
表示有
amount_i
笔类型为orderType_i
、价格为price_i
的订单。订单类型
orderType_i
可以分为两种:
0
表示这是一批采购订单buy
1
表示这是一批销售订单sell
注意,
orders[i]
表示一批共计amount_i
笔的独立订单,这些订单的价格和类型相同。对于所有有效的i
,由orders[i]
表示的所有订单提交时间均早于orders[i+1]
表示的所有订单。存在由未执行订单组成的 积压订单 。积压订单最初是空的。提交订单时,会发生以下情况:
- 如果该订单是一笔采购订单
buy
,则可以查看积压订单中价格 最低 的销售订单sell
。如果该销售订单sell
的价格 低于或等于 当前采购订单buy
的价格,则匹配并执行这两笔订单,并将销售订单sell
从积压订单中删除。否则,采购订单buy
将会添加到积压订单中。- 反之亦然,如果该订单是一笔销售订单
sell
,则可以查看积压订单中价格 最高 的采购订单buy
。如果该采购订单buy
的价格 高于或等于 当前销售订单sell
的价格,则匹配并执行这两笔订单,并将采购订单buy
从积压订单中删除。否则,销售订单sell
将会添加到积压订单中。输入所有订单后,返回积压订单中的 订单总数 。由于数字可能很大,所以需要返回对
10^9 + 7
取余的结果。
示例 1:
输入:orders = [[10,5,0],[15,2,1],[25,1,1],[30,4,0]]
输出:6
解释:输入订单后会发生下述情况:
- 提交 5 笔采购订单,价格为 10 。没有销售订单,所以这 5 笔订单添加到积压订单中。
- 提交 2 笔销售订单,价格为 15 。没有采购订单的价格大于或等于 15 ,所以这 2 笔订单添加到积压订单中。
- 提交 1 笔销售订单,价格为 25 。没有采购订单的价格大于或等于 25 ,所以这 1 笔订单添加到积压订单中。
- 提交 4 笔采购订单,价格为 30 。前 2 笔采购订单与价格最低(价格为 15)的 2 笔销售订单匹配,从积压订单中删除这 2 笔销售订单。第 3 笔采购订单与价格最低的 1 笔销售订单匹配,销售订单价格为 25 ,从积压订单中删除这 1 笔销售订单。积压订单中不存在更多销售订单,所以第 4 笔采购订单需要添加到积压订单中。
最终,积压订单中有 5 笔价格为 10 的采购订单,和 1 笔价格为 30 的采购订单。所以积压订单中的订单总数为 6 。
示例 2:
输入:orders = [[7,1000000000,1],[15,3,0],[5,999999995,0],[5,1,1]]
输出:999999984
解释:输入订单后会发生下述情况:
- 提交 109 笔销售订单,价格为 7 。没有采购订单,所以这 109 笔订单添加到积压订单中。
- 提交 3 笔采购订单,价格为 15 。这些采购订单与价格最低(价格为 7 )的 3 笔销售订单匹配,从积压订单中删除这 3 笔销售订单。
- 提交 999999995 笔采购订单,价格为 5 。销售订单的最低价为 7 ,所以这 999999995 笔订单添加到积压订单中。
- 提交 1 笔销售订单,价格为 5 。这笔销售订单与价格最高(价格为 5 )的 1 笔采购订单匹配,从积压订单中删除这 1 笔采购订单。
最终,积压订单中有 (1000000000-3) 笔价格为 7 的销售订单,和 (999999995-1) 笔价格为 5 的采购订单。所以积压订单中的订单总数为 1999999991 ,等于 999999984 % (109 + 7) 。
提示:
1 <= orders.length <= 10^5
orders[i].length == 3
1 <= price_i, amount_i <= 10^9
orderType_i
为0
或1
方法1:模拟+优先队列
part1
按题意:如果是 buy
订单,我们要寻找 价格
最低的 sell
订单进行操作;如果是 sell
订单,我们要寻找 价格
最高的 buy
订单进行操作。若满足相应的条件,则可以视为当前订单与目标订单 “抵消” 。
所以我们需要对两种订单进行排序。由于题目要求订单 order[i]
的出现时间比 order[i+1]
早,因此我们需要在算法中,按序处理订单。这里就需要用到 模拟 的方法。同时,sell
序列和 buy
序列中的元素会随着订单的出现和变化,因此我们选择使用 优先队列(堆)来排序。
对于 模拟 的过程,我们只需要创建一个循环,在循环中遍历订单即可。对于 排序 ,要寻找 价格
最低的 sell
订单则 sellBacking
是一个 小根堆 ;要寻找 价格
最高的 buy
订单则 buyBacking
是一个 大根堆 。C++的标准库中,在头文件 queue
中给出了优先队列(堆)这个数据结构 priority_queue
。其默认为大根堆。
有了数据结构之后,我们应思考这个堆里面元素的类型。我们首先也许会将 价格
作为一个整型存放到堆里面,毕竟 按价格排序 。但是题目中给出要求 1 <= amount_i <= 10^9
,也就是说一批订单 order[i]
内最高可达到 10的9次方 笔订单。这就意味这我们会在堆中压入 109 个整型,又要循环 109 次来处理 一批 订单。这就一来,一批 订单就要需要消耗 109 * sizeof(int) 约为 3.7GB 内存空间,更何况订单最多有 105 批(1 <= orders.length <= 10^5
)。所以这个简单的想法是不现实的。
我们要把 价格
和 数量
组合为一个二元组 std::pair
存入堆中,即堆的元素类型为 std::pair<int, int>
(题目给出的函数定义中,价格和数量即为 int
)。
priority_queue的使用
(优先队列,或者说小根堆、大根堆,若不使用 priority_queue
实现,可以跳过)
在C++中,我们在定义 优先队列 变量的时候,若 元素类型复杂 或 元素间比较方式需自定义,则需要给出排序的函数,告诉这个堆怎么去进行两个元素的比较从而确定它们的先后关系。因为我们只需要对 价格
进行排序,而 数量
不作为排序的依据。如 sell
订单的 比较函数 如下:
// pair<price, amount>
auto sellCompare = [](
const std::pair<int, int> &left,
const std::pair<int, int> &right
)
{ return left.first > right.first; };
在优先队列对元素进行排序的时候,每次会拿出两个元素,调用这个函数来确定先后关系。同时,上述代码在定义 二元组 的时候,将 价格
放在了二元组的第一个元素处,因此我们实现了对 left
和 right
的 first
成员进行比较。
在cppreference上,我们可以查看 priority_queue 的详细内容。有句话是这么告诉我们的:
可用用户提供的 Compare 更改顺序,例如,用 std::greater 将导致最小元素作为 top() 出现。
Compare
即为我们提供的 比较函数 ,例如上面代码里的 sellCompare
函数。std::greater
可以理解为这个函数的比较方式是: “左边”的元素 left
“大于” “右边”的元素 right
,例如上面代码里的返回语句 left.first > right.first
。当 “左边大于右边” 这个条件成立(比较函数
返回 true
)时,此时这个堆是一个 小根堆 。
这样一来,我们提供了 sellBacking
这个堆的比较函数,通过这个比较函数,使得了这个堆成为了 小根堆。同时我们也告诉了这个堆怎么比较元素,那就是比较两边的 first
。
接下来我们就正式去定以两个堆变量 sellBacking
和 buyBacking
。在cppreference上给出了 priority_queue
的模板参数:
template<
class T,
class Container = std::vector<T>,
class Compare = std::less<typename Container::value_type>
> class priority_queue;
首先,第一个模板参数是这个优先队列的元素类型 T
,在本题中即为二元组 std::pair<int, int>
。
如果元素类型 T
是一般的类型,比如说 float
、int
之类,那么后面两个参数就不用写明。但是我们这里的元素类型是一个比较复杂的 二元组 ,并且元素的比较方式也是自己定义(比较 first
成员)。这两个因素都导致我们需要提供自己的比较函数,继而需要写明后面两个模板参数。第二个参数是用来存放这些元素的容器类型,我们可以使用 std::vector
数组,写为 std::vector<std::pair<int, int> >
。第三个参数是比较函数它本身的类型,我们可以用 decltype
函数得到这个类型,写为 decltype(sellCompare)
。
最后,在构造函数中传入这个比较函数作为参数即可。
关于小根堆 sellBacking
和大根堆 buyBacking
的定义如下:
// pair<price, amount>
typedef std::pair<int, int> Record;
auto sellCompare = [](const Record &left, const Record &right)
{ return left.first > right.first; };
priority_queue<Record, vector<Record>, decltype(sellCompare)> sellBacking{sellCompare};
auto buyCompare = [](const Record &left, const Record &right)
{ return left.first < right.first; };
priority_queue<Record, vector<Record>, decltype(buyCompare)> buyBacking{buyCompare};
- 通过比较函数
sellCompare
中 左边“大于”右边 的写法,使得sellBacking
为小根堆; - 通过比较函数
buyCompare
中 左边“小于”右边 的写法,使得buyBacking
为大根堆。
part2
(sellBacking
为小根堆,buyBacking
为大根堆)
以 buy
订单为例,每次接受到一批存放在 order[i]
里的 buy
订单,我们要去访问 sellBacking
的堆顶。接下来会有以下情况出现:
- 不满足条件(
sell
的价格 低于或等于 当前采购订单buy
的价格),将buy
订单压入buyBacking
。 -
- 满足条件,但
buy
订单 不够 抵消完 堆顶的sell
订单。此时需要更改sellBacking
堆的堆顶元素,将sell
订单的数量减去已经抵消的数量。(priority_queue
不能直接修改堆顶,需要弹出、修改、再压入)。 buy
订单 足够 抵消完 堆顶的sell
订单。此时需要弹出堆顶。若buy
订单还有剩余,那么还需要对弹出后的 新堆顶 进行 再次匹配 。
- 满足条件,但
- 无论如何到了最后,若
buy
订单的数量依旧有剩余,则压入buyBacking
。
同理,sell
订单的处理过程与上述基本相同。
求余
对于数 a
、b
和 mod
,(a + b) % mod
== (a % mod + b % mod) % mod
即 “和的余数” 等于 “余数的和再求余”
代码
最后的代码如下:
#include <vector>
#include <queue>
using namespace std;
class Solution
{
public:
// pair<price, amount>
typedef std::pair<int, int> Record;
int getNumberOfBacklogOrders(vector<vector<int>> &orders)
{
auto sellCompare = [](const Record &left, const Record &right)
{ return left.first > right.first; };
priority_queue<Record, vector<Record>, decltype(sellCompare)> sellBacking{sellCompare};
auto buyCompare = [](const Record &left, const Record &right)
{ return left.first < right.first; };
priority_queue<Record, vector<Record>, decltype(buyCompare)> buyBacking{buyCompare};
for (const vector<int> &order : orders)
{
const int &price = order[0];
int amount = order[1];
const int &orderType = order[2];
if (orderType == 0)
{ // buy order
int sellPrice = 0, sellAmount = 0;
while (!sellBacking.empty() && sellBacking.top().first <= price && amount > 0)
{
sellPrice = sellBacking.top().first;
sellAmount = sellBacking.top().second;
sellBacking.pop();
amount -= sellAmount;
}
if (amount > 0)
{
buyBacking.emplace(price, amount);
}
else
{
sellAmount = std::abs(amount);
sellBacking.emplace(sellPrice, sellAmount);
}
}
else
{ // sell order
int buyPrice = 0, buyAmount = 0;
while (!buyBacking.empty() && buyBacking.top().first >= price && amount > 0)
{
buyPrice = buyBacking.top().first;
buyAmount = buyBacking.top().second;
buyBacking.pop();
amount -= buyAmount;
}
if (amount > 0)
{
sellBacking.emplace(price, amount);
}
else
{
buyAmount = std::abs(amount);
buyBacking.emplace(buyPrice, buyAmount);
}
}
}
constexpr int mod = 1000000007;
int ans = 0;
for (; !sellBacking.empty(); sellBacking.pop())
ans = (ans + sellBacking.top().second) % mod;
for (; !buyBacking.empty(); buyBacking.pop())
ans = (ans + buyBacking.top().second) % mod;
return ans;
}
};
复杂度分析:
-
时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)。其中,
n
为数组orders
的长度。首先需要遍历orders
数组($O(n));其次在遍历过程中,对优先队列(堆)的每次压入、弹出操作均为 O ( l o g n ) O(logn) O(logn)。总的来说,时间复杂度为两者乘积 O ( n l o g n ) O(nlogn) O(nlogn)。 -
空间复杂度: O ( n ) O(n) O(n)。优先队列需要的 O ( n ) O(n) O(n)的空间。
参考结果
Accepted
69/69 cases passed (188 ms)
Your runtime beats 87.63 % of cpp submissions
Your memory usage beats 55.67 % of cpp submissions (5.9 MB)