全局偏移把全体加减工资压成 O(1),固定值域上用树状数组按名次答第 k 大,扣薪只扫描长度恰为 k 的阈值区间批量删人。
OJ: roj
题目 ID: 1567
难度:提高
标签:数据结构树状数组python
创建: 2026-09-30 18:51
更新: 2026-10-07 13:50
形式化题目
维护一个工资多重集合
:若 ,把 插入 ;否则该员工不入职,不计入任何统计。 : 中每个元素加上 。 : 中每个元素减去 ,随后所有 的元素被删除,删除人数计入"离开总数"。 :若 则回答 ,否则回答 中第 大的元素。
处理完全部命令后,输出离开总数(不含入职时即低于下界的人)。
样例(
9 10
I 60
I 70
S 50
F 2
I 30
S 15
A 5
F 1
F 210
20
-1
2逐命令演进如下,可以同时看到三件事:
| 命令 | 在职存储值 |
实际工资 |
输出 / 离开 | |
|---|---|---|---|---|
| 0 | 60 | 60 | ||
| 0 | 60, 70 | 60, 70 | ||
| 60, 70 | 10, 20 | 离开 0 人 | ||
| 60, 70 | 10, 20 | 10 | ||
| 60, 70, 80 | 10, 20, 30 | |||
| 80 | 15 | 离开 2 人 | ||
| 80 | 20 | |||
| 80 | 20 | 20 | ||
| 80 | 20 |
第三次命令的
正解
思路
朴素做法与瓶颈。 直接用数组存所有人的工资:
第一步:全局偏移,
第二步:删除只扫一个长度为
第三步:值域树状数组顶替平衡树。 剩下的需求是:插入一个人、按名次找第 x - X_MIN + 1,用树状数组维护人数前缀和:
- 插入:
cnt[x]加一,树状数组单点, ; :第 大 第 小,用 binary lifting 在树状数组上按名次二分, ,答案再加回 ; - 离开:在第二步扫描到的每个位置上取回
cnt[x],树状数组单点减掉人数。
于是四类命令全部
代码
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-09-30 18:51
# update_at: 2026-09-30 18:54
import sys
X_MIN = -100_000 # 存储值 x = 实际工资 - bias 的理论下界:k ≥ 0、bias ≤ 1e5
X_MAX = 200_000 # 理论上界:k ≤ 1e5、bias ≥ -1e5
POS_SIZE = X_MAX - X_MIN + 2 # 树状数组下标 1..POS_SIZE-1,比值域多留 1 位
TREE = [0] * POS_SIZE # 树状数组:按存储值 x 的名次统计人数
def update(pos: int, delta: int) -> None:
"""树状数组第 pos 个下标增减 delta 人(pos 从 1 开始)。"""
while pos < POS_SIZE:
TREE[pos] += delta
pos += pos & -pos
def kth(rank: int) -> int:
"""最小的下标 pos,使前缀人数 ≥ rank;rank 从 1 开始,即第 rank 小。"""
pos = 0
step = 1 << (POS_SIZE.bit_length() - 1) # 不小于 POS_SIZE 的最高 2 次幂
while step:
nxt = pos + step
if nxt < POS_SIZE and TREE[nxt] < rank:
pos = nxt
rank -= TREE[nxt]
step >>= 1
return pos + 1
def solve() -> None:
data = iter(sys.stdin.buffer.read().split())
n = int(next(data)) # 命令条数
floor = int(next(data)) # 工资下界 min
cnt: dict[int, int] = {} # 每个存储值 x 当前的人数
staff = 0 # 在职人数,等于树状数组总和
bias = 0 # 全局偏移:实际工资 = 存储值 x + bias,A/S 只改它
leave = 0 # 因扣薪低于下界离开的人数(不含入职即低于下界者)
out: list[str] = []
for _ in range(n):
cmd = next(data)
k = int(next(data))
if cmd == b'I':
# 入职即低于下界的直接走人:不建档也不计入离开总数
if k >= floor:
x = k - bias # 存储值,保证不变量:所有在职 x ≥ floor - bias
cnt[x] = cnt.get(x, 0) + 1
staff += 1
update(x - X_MIN + 1, 1)
elif cmd == b'A':
bias += k # 全员加薪:只抬偏移,无人低于下界
elif cmd == b'S':
bias -= k
# 下界阈值 t = floor - bias 随扣薪恰好抬高 k;由不变量只需扫描 [t-k, t)
hi = floor - bias
for x in range(max(hi - k, X_MIN), min(hi, X_MAX + 1)):
gone = cnt.pop(x, 0) # 区间内不在职的值弹出为 0,不影响结果
if gone:
staff -= gone
leave += gone
update(x - X_MIN + 1, -gone)
else: # F k:第 k 大的工资,等价于第 staff-k+1 小的存储值
if k > staff:
out.append("-1")
else:
x = kth(staff - k + 1) - 1 + X_MIN # 名次二分回存储值
out.append(str(x + bias)) # 平移回实际工资
out.append(str(leave))
sys.stdout.write("\n".join(out) + "\n")
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-05 08:50
* update_at: 2026-10-05 08:50
*/
// 正解:全局偏移 + 值域树状数组(名次二分求第 k 大)+ 扣薪时只扫长度为 k 的阈值区间。
// 实际工资 = 存储值 x + bias,A/S 只改 bias;
// 不变量:在职者的存储值 x >= t = min - bias(入职时 k >= min 保证,扣薪后即时清理破坏者)。
#include <cstdio>
typedef long long ll;
const int X_MIN = -100000; // 存储值下界:k >= 0、bias <= 1e5
const int X_MAX = 200000; // 存储值上界:k <= 1e5、bias >= -1e5
const int POS_SIZE = X_MAX - X_MIN + 2; // 树状数组下标 1..POS_SIZE-1,比值域多留 1 位
ll mn; // 工资下界 min
ll bias = 0; // 全局偏移:实际工资 = 存储值 x + bias,A/S 只改它
ll staff_num = 0; // 在职人数,等于树状数组总和
ll leave = 0; // 因扣薪低于下界离开的总数(不含入职即低于下界者)
ll cnt[POS_SIZE]; // 原数组:cnt[pos] = 存储值 x = pos + X_MIN - 1 当前的人数
ll tree[POS_SIZE]; // 树状数组:按存储值名次维护人数前缀和
// 树状数组第 pos 个下标增减 delta 人
void update(int pos, ll delta) {
for (; pos < POS_SIZE; pos += pos & (-pos))
tree[pos] += delta;
}
// 树状数组上名次二分:返回第 rank 小者的树状数组下标(rank 从 1 开始)
// 经典 binary lifting:从高位往低位试跳,走到"前缀人数 < rank"的最右位置
int kth(ll rank) {
int pos = 0;
for (int step = 1 << 18; step >= 1; step >>= 1) { // 2^18 > POS_SIZE,足够跳全程
int nxt = pos + step;
if (nxt < POS_SIZE && tree[nxt] < rank) {
pos = nxt;
rank -= tree[nxt];
}
}
return pos + 1; // 第 rank 小者所在下标
}
// 处理一条 S 命令:bias 降 k 后,阈值 t = mn - bias 恰好升高 k,
// 由不变量"旧在职者都 >= t - k",只需扫描存储值区间 [t-k, t),
// 把其中还挂着的人全部清掉,其余人一定仍合法
void do_s(ll k) {
bias -= k;
ll hi = mn - bias; // 新阈值 t(存储值视角,左闭右开的右端点)
ll lo = hi - k; // 旧阈值,旧在职者都 >= lo
if (lo < X_MIN)
lo = X_MIN;
if (hi > (ll)X_MAX + 1)
hi = X_MAX + 1;
for (ll x = lo; x < hi; ++x) { // 左闭右开:只扫恰好这一段
int pos = int(x - X_MIN + 1);
ll gone = cnt[pos]; // 该存储值上在职的人数
if (gone > 0) {
cnt[pos] = 0;
staff_num -= gone;
leave += gone;
update(pos, -gone); // 树状数组同步减掉这些人
}
}
}
int main() {
ll n;
scanf("%lld %lld", &n, &mn);
for (ll t = 1; t <= n; ++t) {
char op[4];
ll k;
scanf("%s %lld", op, &k);
if (op[0] == 'I') {
// 入职即低于下界:不建档也不计入离开总数
if (k >= mn) {
int pos = int(k - bias - X_MIN + 1);
cnt[pos]++;
staff_num++;
update(pos, 1);
}
} else if (op[0] == 'A') {
bias += k; // 全员加薪:只抬偏移,不会有人低于下界
} else if (op[0] == 'S') {
do_s(k);
} else { // F k:第 k 大 = 第 staff-k+1 小的存储值
if (k > staff_num) {
printf("-1\n");
} else {
int pos = kth(staff_num - k + 1);
ll x = pos + X_MIN - 1; // 名次二分回存储值
printf("%lld\n", x + bias); // 平移回实际工资
}
}
}
printf("%lld\n", leave);
return 0;
}复杂度
- 时间复杂度:
,其中 是值域, 是命令数, 是所有 命令的扫描总量;与 C++ 平衡树解法同阶。 - 空间复杂度:
:树状数组 个整数加 cnt字典至多项;实测峰值 42.1 MB,限制 128 MB。
总结
这道题的三个观察各消除一层成本:全体加减用全局偏移收成