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 中的所有花
  • 输出被丢弃的花的数量

对应题目:HDU 4614 Vases and Flowers

二、思路分析

线段树维护什么

每个花瓶有两种状态: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, ...] 内

三、算法流程

  1. 建树,所有花瓶初始为空(v = 0)
  2. 对每个操作:
    • 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

四、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

七、相关题目

八、总结

要点 内容
数据结构 线段树(区间和 + 区间赋值)
关键公式 空位数 = 区间长度 - 花数
找第 k 个空位 二分 + 线段树,O(log² n)
懒标记 区间赋值标记,-1 表示无标记
posted @ 2026-10-14 16:10  leaderling  阅读(7)  评论(0)    收藏  举报