1 条题解
-
0
思路与算法
每张账单分配唯一编号,分别放入最小堆和最大堆,并用alive[id]标记未支付。每天插入新账单后,从最小堆顶清除所有已支付编号,取出当日最小账单并标记为已支付;随后清除最大堆顶已支付编号,取出最大账单并标记。已支付项在另一个堆中暂留,只有来到堆顶时才丢弃,这就是惰性删除。
相同面额必须当成不同账单。用64位键(value<<21)|id同时保存面额和编号,面额优先比较,编号仅打破同面额的顺序。最多15000*100=1500000个编号,小于2^21,编号不会侵占面额位。两个堆预留总账单数上界,避免节点树结构的额外内存。
正确性说明
某时刻alive为真的编号恰是所有未支付账单:插入时加入,支付时准确删除一个编号。堆中包含所有仍存活的账单,可能还有失效项;清除失效堆顶后,堆顶就是所有存活账单的真正极值。先删除最小账单,再在剩余账单中取最大,确保同一张账单不会支付两次。每天保证至少两张可支付,第二次取出一定存在。其余存活项自然保留到下一天,因此所有输出与过程一致。
复杂度
令B为全部收到的账单数,B<=1500000。每个编号进入两个堆各一次、从各堆最多弹出一次,总时间O(B log(B+1)+N),两个堆及alive数组空间O(Bmax),Bmax=100N。数组初始化O(Bmax)也计入总开销,可写总时间O(Bmax+B log(B+1)+N)。输出N行。
两个预留的uint64堆各最多8Bmax字节,alive最多Bmax+1字节,合计约25.5MB,加上少量运行时仍在原64MiB内实际验证。面额小于10^9,移位编码小于2^51,不溢出64位。
易错点
必须删除编号而非删除所有相同金额;两个极值相等时也要支付两张不同账单。先删最小,再清理最大堆,避免重复选中同一编号。M允许0,旧账单跨天保留。编号要覆盖150万,不能只留20位。实际最大规模包含全重复、递增唯一值及随机高面额,独立答案由压缩坐标与Fenwick重数顺序统计计算。
参考代码(C++11)
#include <cstdint> #include <functional> #include <iostream> #include <queue> #include <vector> using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; cin >> n; using Key = uint64_t; vector<Key> lo, hi; lo.reserve(n * 100); hi.reserve(n * 100); priority_queue<Key, vector<Key>, greater<Key>> low(greater<Key>{}, move(lo)); priority_queue<Key, vector<Key>> high(less<Key>{}, move(hi)); vector<unsigned char> alive(n * 100 + 1); const Key mask = (Key(1) << 21) - 1; int id = 0; for (int day = 0; day < n; ++day) { int m; cin >> m; while (m--) { int value; cin >> value; ++id; alive[id] = 1; Key key = (Key(value) << 21) | id; low.push(key); high.push(key); } while (!alive[low.top() & mask]) low.pop(); Key a = low.top(); low.pop(); alive[a & mask] = 0; while (!alive[high.top() & mask]) high.pop(); Key b = high.top(); high.pop(); alive[b & mask] = 0; cout << (a >> 21) << ' ' << (b >> 21) << '\n'; } }
- 1