题解:AtCoder AT_awc0063_e Number of Blocks in an Interval

【题目来源】

AtCoder:E - Number of Blocks in an Interval

【题目描述】

Takahashi manages a coloring sheet consisting of \(N\) cells arranged in a horizontal row. The cells are numbered from \(1\) to \(N\) from left to right.
高桥管理着一个由 \(N\) 个单元格横向排列组成的填色纸。单元格从左到右编号为 \(1\)\(N\)

Each cell \(i\) is painted with color \(C_i\). In a sequence of cells, a maximal consecutive interval of the same color that cannot be extended further is called a block. The number of blocks in an interval \([L, R]\) is the number of blocks when the cell sequence \(C_L, C_{L+1}, \dots, C_R\) is divided into blocks.
每个单元格 \(i\) 被涂上了颜色 \(C_i\)。在单元格序列中,一个无法再扩展的最大、连续、同颜色的区间被称为。区间 \([L, R]\)块数是将单元格序列 \(C_L, C_{L+1}, \dots, C_R\) 划分为块后得到的块的数量。

For example, if the sequence is \(1, 1, 2, 2, 2, 1\), the blocks are \((1, 1), (2, 2, 2), (1)\), giving \(3\) blocks, so the number of blocks is \(3\).
例如,如果序列是 \(1, 1, 2, 2, 2, 1\),则块为 \((1, 1), (2, 2, 2), (1)\),共 \(3\) 个块,因此块数为 \(3\)

Aoki performs a total of \(Q\) operations of the following two types. Process each operation in order and answer the queries.
青木将执行总共 \(Q\) 次以下两种类型的操作。按顺序处理每个操作,并回答查询。

  • 1 L R X: Change the color of all cells in the interval \([L, R]\) to \(X\).
    1 L R X:将区间 \([L, R]\) 内所有单元格的颜色更改为 \(X\)
  • 2 L R: Output the number of blocks in the current interval \([L, R]\).
    2 L R:输出当前区间 \([L, R]\) 的块数。

【输入】

\(N\) \(Q\)
\(C_1\) \(C_2\) \(\dots\) \(C_N\)
\(T_1\)
\(T_2\)
\(\vdots\)
\(T_Q\)

  • The first line contains the number of cells \(N\) and the number of operations \(Q\), separated by a space.
  • The second line contains the initial colors of each cell \(C_1, C_2, \dots, C_N\), separated by spaces.
  • The \(i\)-th of the following \(Q\) lines contains the \(i\)-th operation \(T_i\). Each operation is in one of the following formats:
  • 1 L R X: Change the color of all cells in the interval \([L, R]\) to \(X\).
  • 2 L R: A query to find the number of blocks in the current interval \([L, R]\).

【输出】

For each operation 2 L R, output the number of blocks in that interval, one per line.

【输入样例】

6 6
1 1 2 2 2 1
2 1 6
2 2 5
1 3 5 1
2 1 6
1 2 4 3
2 1 6

【输出样例】

3
2
1
3

【核心思想】

  1. 问题分析:给定长度为 \(N\) 的颜色序列 \(C_1, C_2, \dots, C_N\),需要支持两种操作:区间染色(将 \([L, R]\) 染成颜色 \(X\))和查询区间 \([L, R]\) 内的块数(连续同色段的数量)。这是一个线段树 + 懒标记问题,关键在于维护区间端点值和块数,并正确处理区间合并。

  2. 算法选择

    • 线段树(Segment Tree):每个节点维护区间左端点值 lc、右端点值 rc、块数 cnt、懒标记 dt
    • 懒标记(Lazy Propagation):支持区间染色的延迟更新,将区间赋值为同一种颜色
    • 区间合并:合并两个子区间时,如果左子区间的右端点值等于右子区间的左端点值,则块数减 \(1\)(两段合并为一段)
  3. 关键步骤

    • 节点定义{l, r, lc, rc, dt, cnt},其中 l, r 为区间范围,lc, rc 为左右端点颜色,dt 为懒标记(\(-1\) 表示无标记),cnt 为块数
    • pushup(向上更新)
      • tr[u].lc = tr[u<<1].lctr[u].rc = tr[u<<1|1].rc
      • tr[u].cnt = tr[u<<1].cnt + tr[u<<1|1].cnt
      • tr[u<<1].rc == tr[u<<1|1].lc,则 tr[u].cnt--
    • pushdown(下传懒标记)
      • 若当前节点有懒标记,将左右子节点都赋值为该颜色
      • 子节点 lc = rc = dtcnt = 1dt = 父节点dt
      • 清除当前节点懒标记
    • build(建树):叶子节点 cnt = 1lc = rc = w[l]
    • update(区间更新):区间赋值,设置懒标记,lc = rc = dcnt = 1
    • query(区间查询)
      • 完全包含直接返回节点
      • 横跨左右子树时,合并左右结果,注意处理边界颜色相同的情况
  4. 时间/空间复杂度

    • 时间复杂度:\(O(Q \log N)\),每次更新和查询沿树高进行
    • 空间复杂度:\(O(N)\),线段树需要 \(4N\) 个节点空间
  5. 线段树维护区间块数的核心思想

    • 端点值维护:每个节点记录区间的左右端点颜色,用于合并时判断连续性
    • 跨区间合并:合并两个区间时,检查中间边界颜色是否相同,相同则块数减 \(1\)
    • 懒标记优化:区间染色时直接设置懒标记,避免遍历到叶子节点,实现 \(O(\log N)\) 区间修改
    • 信息可合并性:块数满足区间可加性,通过端点值处理边界情况即可正确合并
    • 适用于区间染色、区间连续段统计、区间合并类问题

【算法标签】

线段树

【代码详解】

#include <bits/stdc++.h>
using namespace std;

#define int long long
const int N = 200005;

int n, q;
int w[N];

// 线段树节点结构
struct Node
{
    int l, r;      // 节点表示的区间 [l, r]
    int lc, rc;    // 区间左端点的值,区间右端点的值
    int dt;        // 懒标记,-1 表示没有懒标记
    int cnt;       // 区间内连续颜色段的数量
} tr[N * 4];

// 向上更新节点信息
void pushup(int u)
{
    // 左端点值来自左子节点
    tr[u].lc = tr[u << 1].lc;
    // 右端点值来自右子节点
    tr[u].rc = tr[u << 1 | 1].rc;
    // 合并左右子节点的颜色段数量
    tr[u].cnt = tr[u << 1].cnt + tr[u << 1 | 1].cnt;
    // 如果左子节点的右端点值等于右子节点的左端点值,说明中间是连续的,需要合并
    if (tr[u << 1].rc == tr[u << 1 | 1].lc)
    {
        tr[u].cnt--;
    }
}

// 向下传递懒标记
void pushdown(int u)
{
    auto &root = tr[u], &l = tr[u << 1], &r = tr[u << 1 | 1];
    // 如果有懒标记
    if (root.dt != -1)
    {
        // 将懒标记传递给左子节点
        l.dt = root.dt;
        l.lc = root.dt;
        l.rc = root.dt;
        l.cnt = 1;  // 区间内所有值相同,只有一个颜色段

        // 将懒标记传递给右子节点
        r.dt = root.dt;
        r.lc = root.dt;
        r.rc = root.dt;
        r.cnt = 1;  // 区间内所有值相同,只有一个颜色段

        // 清除当前节点的懒标记
        root.dt = -1;
    }
}

// 构建线段树
void build(int u, int l, int r)
{
    if (l == r)  // 叶子节点
    {
        tr[u] = {l, r, w[l], w[l], -1, 1};  // 单个元素,颜色段数量为1
    }
    else
    {
        tr[u] = {l, r, 0, 0, -1, 0};  // 初始化非叶子节点
        int mid = (l + r) >> 1;
        // 递归构建左右子树
        build(u << 1, l, mid);
        build(u << 1 | 1, mid + 1, r);
        // 向上更新节点信息
        pushup(u);
    }
}

// 区间更新
void update(int u, int l, int r, int d)
{
    // 如果当前节点区间完全包含在更新区间内
    if (tr[u].l >= l && tr[u].r <= r)
    {
        // 设置懒标记
        tr[u].dt = d;
        // 更新区间左右端点值
        tr[u].lc = d;
        tr[u].rc = d;
        // 整个区间变为同一种颜色,颜色段数量为1
        tr[u].cnt = 1;
    }
    else
    {
        // 向下传递懒标记
        pushdown(u);
        int mid = (tr[u].l + tr[u].r) >> 1;
        // 递归更新左子区间
        if (l <= mid)
        {
            update(u << 1, l, r, d);
        }
        // 递归更新右子区间
        if (r > mid)
        {
            update(u << 1 | 1, l, r, d);
        }
        // 向上更新节点信息
        pushup(u);
    }
}

// 区间查询
Node query(int u, int l, int r)
{
    // 如果当前节点区间完全包含在查询区间内
    if (tr[u].l >= l && tr[u].r <= r)
    {
        return tr[u];
    }
    else
    {
        // 向下传递懒标记
        pushdown(u);
        int mid = (tr[u].l + tr[u].r) >> 1;

        // 如果查询区间完全在左子树
        if (r <= mid)
        {
            return query(u << 1, l, r);
        }
        // 如果查询区间完全在右子树
        if (l > mid)
        {
            return query(u << 1 | 1, l, r);
        }

        // 查询区间横跨左右子树
        Node left = query(u << 1, l, r);
        Node right = query(u << 1 | 1, l, r);
        Node res;

        // 合并左右子树的结果
        res.lc = left.lc;           // 左端点值来自左子树
        res.rc = right.rc;          // 右端点值来自右子树
        res.cnt = left.cnt + right.cnt;  // 合并颜色段数量

        // 如果左子树的右端点值等于右子树的左端点值,说明中间连续,需要合并
        if (left.rc == right.lc)
        {
            res.cnt--;
        }

        return res;
    }
}

signed main()
{
    cin >> n >> q;
    for (int i = 1; i <= n; i++)
    {
        cin >> w[i];
    }
    // 构建线段树
    build(1, 1, n);

    while (q--)
    {
        int op, l, r, x;
        cin >> op >> l >> r;
        if (op == 1)  // 更新操作
        {
            cin >> x;
            update(1, l, r, x);
        }
        else  // 查询操作
        {
            cout << query(1, l, r).cnt << endl;
        }
    }
    return 0;
}

【运行结果】

6 6
1 1 2 2 2 1
2 1 6
3
2 2 5
2
1 3 5 1
2 1 6
1
1 2 4 3
2 1 6
3
posted @ 2026-06-15 09:50  团爸讲算法  阅读(7)  评论(0)    收藏  举报