Alice 的花瓶(线段树 + 二分)
Alice 的花瓶(线段树 + 二分)
本文用于整理 ACM 博弈论相关知识点与题目,方便自己和队友查阅。
持续更新中……
一、题目描述
有 N 个花瓶,编号 0 ~ N-1。Alice 有两种操作:
操作 1(放花):1 A F
- 从花瓶
A开始,依次尝试A, A+1, ..., N-1 - 遇到空花瓶就放一朵花,遇到有花的就跳过
- 一共放
F朵,放不完的丢弃 - 输出第一朵和最后一朵花所在的花瓶编号
- 如果一朵都放不下,输出
Can not put any one.
操作 2(清理):2 A B
- 清空花瓶
A ~ B中的所有花 - 输出被丢弃的花的数量
二、思路分析
线段树维护什么
每个花瓶有两种状态:0(空)或 1(有花)。
线段树每个节点维护:
v:区间内花的数量(区间和)lazy:区间覆盖标记,-1表示无标记,0/1表示把整个区间赋为0/1
操作 2:区间清理
直接查询区间 [A, B] 的花数,然后区间赋值为 0。
区间赋值为 0 的懒标记,正好对应「清空」。
操作 1:放花
从 A 开始,找第 1 个空位和第 k 个空位(k 是实际能放的花数)。
- 先计算
[A, N-1]中空位的总数 - 实际能放的花数
num = min(F, 空位总数) - 找第 1 个空位 → 第一朵花的位置
- 找第
num个空位 → 最后一朵花的位置 - 然后把
[第一朵, 最后一朵]区间赋值为1
如何找第 k 个空位:二分 + 线段树
关键转化:区间 [i, mid] 中空位的数量 = (mid - i + 1) - sum(i, mid)。
mid - i + 1是区间长度sum(i, mid)是区间内已有的花数- 两者相减就是空位数
由于空位数关于 mid 单调不减,可以二分 mid:
如果 (mid - i + 1) - sum(i, mid) >= k,说明第 k 个空位在 [i, mid] 内
否则在 [mid+1, ...] 内
三、算法流程
- 建树,所有花瓶初始为空(
v = 0) - 对每个操作:
- op = 1:
- 查询
[A, N-1]的花数,如果等于区间长度,说明全满,输出Can not put any one. - 否则计算空位数
num = 区间长度 - 花数 - 实际放花数
cnt = min(F, num) - 二分找第 1 个空位
L,第cnt个空位R - 输出
L R - 区间赋值
[L, R] = 1
- 查询
- op = 2:
- 查询
[A, B]的花数,输出 - 区间赋值
[A, B] = 0
- 查询
- op = 1:
四、AC 代码
#include<iostream>
#include<algorithm>
#include<cstdio>
using namespace std;
const int maxn = 5e4 + 5;
typedef long long ll;
struct node
{
int L, R;
ll v, lazy;
} seg[maxn * 4];
// 向上合并:父节点和 = 左右儿子和
void push_up(int p)
{
seg[p].v = seg[p << 1].v + seg[p << 1 | 1].v;
}
// 建树:初始全为空,lazy = -1 表示无标记
void build(int p, int L, int R)
{
seg[p].L = L, seg[p].R = R;
seg[p].v = 0, seg[p].lazy = -1;
if (L == R) return;
int mid = (L + R) >> 1;
build(p << 1, L, mid);
build(p << 1 | 1, mid + 1, R);
push_up(p);
}
// 下放懒标记:把区间赋值标记传给左右儿子
void pushdown(int p)
{
if (seg[p].lazy == -1) return;
int lc = p << 1, rc = p << 1 | 1;
seg[lc].lazy = seg[p].lazy;
seg[lc].v = seg[lc].lazy * (seg[lc].R - seg[lc].L + 1);
seg[rc].lazy = seg[p].lazy;
seg[rc].v = seg[rc].lazy * (seg[rc].R - seg[rc].L + 1);
seg[p].lazy = -1;
}
// 区间查询和
ll findsum(int p, int L, int R)
{
int l = seg[p].L, r = seg[p].R;
if (L <= l && r <= R) return seg[p].v;
pushdown(p);
int mid = (l + r) >> 1;
ll res = 0;
if (L <= mid) res += findsum(p << 1, L, R);
if (R > mid) res += findsum(p << 1 | 1, L, R);
return res;
}
// 区间赋值:把 [L, R] 全部赋为 k(0 或 1)
void update(int p, int L, int R, int k)
{
int l = seg[p].L, r = seg[p].R;
if (L <= l && r <= R)
{
seg[p].v = k * (r - l + 1);
seg[p].lazy = k;
return;
}
pushdown(p);
int mid = (l + r) >> 1;
if (L <= mid) update(p << 1, L, R, k);
if (R > mid) update(p << 1 | 1, L, R, k);
push_up(p);
}
int n, m;
// 从位置 i 开始,找第 k 个空位的位置
int findzero(int i, int k)
{
int L = i, R = n - 1;
int ans = 0;
while (L <= R)
{
int mid = (L + R) >> 1;
// [i, mid] 中空位数 = 区间长度 - 花数
if (mid - i + 1 - findsum(1, i, mid) >= k)
{
ans = mid;
R = mid - 1;
}
else L = mid + 1;
}
return ans;
}
int main()
{
int T;
cin >> T;
while (T--)
{
cin >> n >> m;
build(1, 0, n - 1);
int op;
for (int i = 0; i < m; i++)
{
cin >> op;
if (op == 1)
{
int lc, k;
cin >> lc >> k;
// 区间全满,插不下
if (findsum(1, lc, n - 1) == n - 1 - lc + 1)
{
printf("Can not put any one.\n");
continue;
}
else
{
// [lc, n-1] 中空位总数
int num = n - 1 - lc + 1 - findsum(1, lc, n - 1);
int L, R;
if (num >= k)
{
// 空位足够,放 k 朵
L = findzero(lc, 1);
R = findzero(lc, k);
}
else
{
// 空位不够,放到最后一个空位
L = findzero(lc, 1);
R = findzero(lc, num);
}
printf("%d %d\n", L, R);
update(1, L, R, 1); // 区间赋值 1
}
}
else
{
int a, b;
cin >> a >> b;
cout << findsum(1, a, b) << endl; // 输出丢弃的花数
update(1, a, b, 0); // 清空区间
}
}
cout << endl;
}
return 0;
}
五、复杂度
| 操作 | 时间复杂度 |
|---|---|
| 区间查询/更新 | O(log n) |
| 二分找空位 | O(log² n) |
| 单次操作 | O(log² n) |
| 总复杂度 | O(m log² n) |
n, m ≤ 50000,完全够用。
六、易错点
lazy初始为-1,表示无标记,不是0- 区间赋值时,
v = k * 区间长度 - 二分时判断条件是
空位数 >= k,不是> k - 输出格式:操作 1 输出
L R,操作 2 输出花数 - 每组测试数据后要输出一个空行
- 多组数据记得重新
build
七、相关题目
- HDU 4614 Vases and Flowers — 本题
- 洛谷 P2894 [USACO08FEB] Hotel G — 线段树 + 二分找空位
- 洛谷 P2572 [SCOI2010] 序列操作 — 区间赋值线段树
八、总结
| 要点 | 内容 |
|---|---|
| 数据结构 | 线段树(区间和 + 区间赋值) |
| 关键公式 | 空位数 = 区间长度 - 花数 |
| 找第 k 个空位 | 二分 + 线段树,O(log² n) |
| 懒标记 | 区间赋值标记,-1 表示无标记 |

浙公网安备 33010602011771号