前言
2024/9/30 upd:更新简化题意
模拟赛的 T3,因为没题解调了好久,现在自己写一个造福人民。
题目分析
简化题意
(这个简化题意为什么和原题一样长)
有一个空序列,有 q q q 次操作,共有以下三种操作:
-
join
将一个新的正整数加入到序列尾,其编号为它被加入时的join
操作的数量。每个数有一个属性 w w w。若 w w w 为 0 0 0,则该整数不可分;若 w w w 为 1 1 1,则该整数可分。 -
leave
移除编号为 i i i 的整数。 -
board
给定 b b b。从第一个整数 s 1 s_1 s1 开始依次考虑,直到考虑完整个序列或 b b b 为零。若 b ≥ s i b \ge s_i b≥si,则 b ← b − s i b \gets b - s_i b←b−si 并将 s i s_i si 移出序列;若 b < s i b < s_i b<si 且 s i s_i si 可分,则 s i ← s i − b s_i \gets s_i - b si←si−b 并 b ← 0 b \gets 0 b←0;否则考虑下一个整数。考虑完后,输出所有被移除的整数和被修改的整数。
给出所有操作,求每次操作 3 3 3 的询问。
思路
考虑
s
≤
10
s \le 10
s≤10 的情况:可以开
11
11
11 个桶子分别存可以分开的团队和不可以分开的大小为
1
1
1 到大小为
10
10
10 的团队。对于每个 join
操作,直接将其加入到对应的桶子里;对于每个 leave
操作,直接将离开的团队从其所对应的桶子中删掉;对于每个 board
操作,令车中剩下的空间为
b
b
b,重复在能分开的团队的桶子和大小为
1
1
1 到
b
b
b 的桶子中找排在最前面的团队并让它(或它的一部分)上车,直到车满或无人可上。
对于 1 ≤ s ≤ 200000 1 \le s \le 200000 1≤s≤200000 的情况下,上述做法如果暴力找排在最前面的团队的话必定超时,考虑对其优化。
发现这一部分所在做的事其实是在找每个桶子中最小数构成的数列的区间最小值(可以将其想象为每个桶子中的团队都按大小从小到大的顺序从上到下摆放,要从这 s + 1 s + 1 s+1 个桶子的最顶部的团队中找到一个最小的,它就是所有团队中排在最前面的那一个)。也就是说可以用线段树将总复杂度优化为 O ( n log n ) O(n \log n) O(nlogn)。
然后就是正解了。
参考代码
蒟蒻的码风有些烂,巨佬们请将就着看。
#include<bits/stdc++.h>
using namespace std;
#define LL long long // 不开 long long 见祖宗
// 方便起见,重定义了线段树左子节点和右子节点的编号
#define leftson u << 1
#define rightson (u << 1) + 1
const LL NR = 2e5;
const LL inf = 1e18; //无穷大,某个桶子中尚无团队时其在线段树上对应的节点的值就是 inf
// 线段树板子,不做过多解释
struct Node{
LL l, r;
LL Min, add;
};
struct Segtree{
Node tr[4 * NR + 10];
LL n;
LL root;
void pushup(LL u){
tr[u].Min = min(tr[leftson].Min, tr[rightson].Min);
}
void pushdown(LL u){
if(tr[u].add){
tr[leftson].add += tr[u].add;
tr[leftson].Min += tr[u].add;
tr[rightson].add += tr[u].add;
tr[rightson].Min += tr[u].add;
tr[u].add = 0;
}
}
void modify(LL u, LL l, LL r, LL d){
if(l <= tr[u].l && r >= tr[u].r){
tr[u].Min += (LL)d;
tr[u].add += d;
return;
}
pushdown(u);
LL mid = (tr[u].l + tr[u].r) >> 1;
if(l <= mid) modify(leftson, l, r, d);
if(r > mid) modify(rightson, l, r, d);
pushup(u);
}
LL query(LL u, LL l, LL r){
if(l <= tr[u].l && r >= tr[u].r){
return tr[u].Min;
}
pushdown(u);
LL mid = (tr[u].l + tr[u].r) >> 1;
pushup(u);
LL ret = inf;
if(l <= mid) ret = min(ret, query(leftson, l, r));
if(r > mid) ret = min(ret, query(rightson, l, r));
return ret;
}
void build(LL u, LL l, LL r){
if(l == r){
tr[u] = Node{l, r, inf, 0};
return ;
}
tr[u].l = l;
tr[u].r = r;
LL mid = (l + r) >> 1;
build(leftson, l, mid);
build(rightson, mid + 1, r);
pushup(u);
}
};
Segtree seg;
struct Team{ // 输出答案用,表示编号为 id 的团队走了 x 个人
LL x;
bool flag;
LL id;
};
vector<Team> v; // 用来统计答案
LL sz[NR + 10]; // 表示团队 i 还剩下 sz[i] 个人
LL pos[NR + 10]; // 表示团队 i 被存在了第 pos[i] 个桶子里
set<LL> st[NR + 10]; // 桶子
void leave(LL id){ //团队离开队列时与团队全部上车后需要将其删除
st[pos[id]].erase(id); // 从桶子中删除编号为 id 的团队
seg.modify(1, pos[id], pos[id], (st[pos[id]].empty() ? inf : *st[pos[id]].begin()) - seg.query(1, pos[id], pos[id])); // 更新这个桶子在线段树中对应的节点,注意若桶子为空,一定要在线段树中将其设为 inf
}
int main(){
seg.build(1, 0, NR); // 建树
LL T;
scanf("%lld", &T); // 读入
LL cnt = 0;
while(T--){
LL op;
scanf("%lld", &op);
if(op == 1){ // join
LL x, y;
scanf("%lld%lld", &x, &y);
cnt++;
sz[cnt] = x;
pos[cnt] = x;
if(y) pos[cnt] = 0;
st[pos[cnt]].emplace(cnt);
seg.modify(1, pos[cnt], pos[cnt], *st[pos[cnt]].begin() - seg.query(1, pos[cnt], pos[cnt]));
}
else if(op == 2){ // leave
LL x;
scanf("%lld", &x);
leave(x);
}
else if(op == 3){ // board
v.clear(); // 多测要清空
LL x;
scanf("%lld", &x);
while(x > 0){ // 模拟上车
LL tmp = seg.query(1, 0, x); // 找到最靠前的团队
if(tmp == inf) break; // 没有则 break
LL del = min(x, sz[tmp]); // 注意有的团队可以拆分,只要 x < sz[tmp] 时,团队一定都可以拆分(因为找的时候就保证了只要是不可拆的团队,其大小一定都小于 x)
v.emplace_back(Team{del, false, tmp}); // 计入答案
x -= del; // 更新车上剩余空间
sz[tmp] -= del; // 更新团队没上车的人的数量
if(sz[tmp] == 0) leave(tmp); // 若全部上车,将空的团队删掉并更新这个桶子中排在最前面的
}
printf("%lld\n", (LL)v.size()); // 输出答案
if(v.size() == 0) continue;
for(auto i : v){
printf("%lld %lld\n", i.id, i.x);
}
}
}
return 0;
}