题解:AtCoder AT_awc0089_e Painting the Fence

【题目来源】

AtCoder:E - Painting the Fence

【题目描述】

Takahashi has been hired to paint a fence consisting of \(N\) boards. The boards are numbered from \(1\) to \(N\), and initially all boards are unpainted.

Aoki gives Takahashi \(M\) painting instructions. The \(i\)-th instruction \((1 \leq i \leq M)\) is "paint all boards from board \(L_i\) to board \(R_i\) (inclusive)." If a board is painted by multiple instructions, it ends up in the same state as if it were painted once.

Due to his limited stamina, Takahashi must ignore exactly \(K\) consecutive instructions out of the \(M\) instructions. Specifically, he chooses an integer \(s\) satisfying \(1 \leq s \leq M - K + 1\), ignores the \(K\) instructions from the \(s\)-th to the \((s + K - 1)\)-th (not executing them), and executes all the remaining \(M - K\) instructions.

Takahashi wants to maximize the number of boards that are ultimately painted by optimally choosing the interval of instructions to ignore. Find the maximum number of boards that are ultimately painted.

高桥被雇来粉刷一面由 \(N\) 块木板组成的栅栏。木板编号从 \(1\)\(N\),初始时所有木板都没有被粉刷。

青木给了高桥 \(M\) 个粉刷指令。第 \(i\) 个指令(\(1 \leq i \leq M\))是"粉刷从木板 \(L_i\) 到木板 \(R_i\)(含端点)的所有木板"。如果一块木板被多个指令粉刷,它的最终状态与只被粉刷一次相同。

由于体力有限,高桥必须忽略 \(M\) 个指令中恰好 \(K\) 个连续的指令。具体来说,他选择一个满足 \(1 \leq s \leq M - K + 1\) 的整数 \(s\),忽略从第 \(s\) 个到第 \((s + K - 1)\) 个的 \(K\) 个指令(不执行它们),并执行所有剩余的 \(M - K\) 个指令。

高桥希望通过最优地选择要忽略的指令区间,来最大化最终被粉刷的木板数量。求最终被粉刷的木板的最大数量。

【输入】

\(N\) \(M\) \(K\)
\(L_1\) \(R_1\)
\(L_2\) \(R_2\)
\(\vdots\)
\(L_M\) \(R_M\)

The first line contains the number of boards \(N\), the number of instructions \(M\), and the number of consecutive instructions to ignore \(K\), separated by spaces. The \(i\)-th \((1 \leq i \leq M)\) of the following \(M\) lines contains the left endpoint \(L_i\) and right endpoint \(R_i\) of the range to paint in the \(i\)-th instruction, separated by spaces.

【输出】

Output the maximum number of boards that are ultimately painted as an integer, when the consecutive \(K\) instructions to ignore are chosen optimally.

【输入样例】

10 4 2
1 3
4 6
2 5
8 10

【输出样例】

7

【核心思想】

  1. 问题分析:给定 \(N\) 块木板和 \(M\) 个粉刷区间 \([L_i, R_i]\),需要恰好忽略 \(K\) 个连续的指令,使得最终被粉刷的木板数量最大化。这是一个离散化 + 扫描线 + 差分问题,关键在于将问题转化为"选择 \(K\) 个连续区间,使得覆盖范围最大"。

  2. 算法选择

    • 离散化:将区间端点坐标压缩,减少处理范围
    • 扫描线:从左到右扫描,维护当前覆盖当前位置的区间集合
    • 差分数组:记录每个起始位置选择 \(K\) 个连续区间能覆盖的长度
    • 关键观察:对于每个位置,如果当前覆盖的区间在编号上是连续的且跨度不超过 \(K\),则可以通过调整起始位置来最大化覆盖
  3. 关键步骤

    • 读取输入\(N\)(木板数量)、\(M\)(指令数量)、\(K\)(忽略的指令数)、\(M\) 个区间 \([L_i, R_i]\)
    • 离散化:将所有区间端点和边界加入数组,排序去重
    • 构建事件:对于每个区间,在左端点加入"进入事件",在右端点+1加入"离开事件"
    • 扫描线处理(遍历每个离散化段):
      • 处理离开事件:从集合中移除区间
      • 处理进入事件:向集合中添加区间
      • 如果当前段未被覆盖:diff[0] -= len
      • 如果当前段被覆盖且区间连续(跨度 \(\leq K\)):
        • 计算可额外选择的区间数 \(x = K - (mx - mn + 1)\)
        • 更新差分数组:diff[mn + 1] += lendiff[max(0, mn - x)] -= len
    • 计算答案:对差分数组求前缀和,找到最大覆盖长度
    • 输出结果:最大覆盖木板数
  4. 时间/空间复杂度

    • 时间复杂度:\(O(M \log M)\),主要是离散化和扫描线操作
    • 空间复杂度:\(O(M)\),存储离散化数组和事件
  5. 离散化 + 扫描线 + 差分的核心思想

    • 离散化压缩:将大范围坐标压缩到小范围,降低复杂度
    • 扫描线维护:用集合维护当前覆盖的区间,动态更新覆盖状态
    • 差分记录:用差分数组记录每个起始位置选择 \(K\) 个区间能覆盖的长度
    • 连续区间性质:如果覆盖当前段的区间在编号上连续且跨度不超过 \(K\),则可以通过选择不同的 \(K\) 个连续区间来覆盖
    • 适用于区间覆盖、范围查询、最优选择类问题

【算法标签】

离散化

【代码详解】

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

// 定义长整型别名,便于处理大数据
#define int long long

// 定义数组最大容量
const int N = 200005;

// 全局变量声明
int n;                  // 总长度(坐标范围)
int m;                  // 区间数量
int k;                  // 可选择的区间数量上限
int l[N];               // 每个区间的左端点
int r[N];               // 每个区间的右端点
int diff[N];            // 差分数组,用于统计每个位置被覆盖的变化量

// 离散化辅助数组
vector<int> alls;

// 二分查找:在离散化数组中找到x对应的索引
int find(int x)
{
    int l = 0, r = alls.size() - 1;
    while (l < r)
    {
        int mid = (l + r) >> 1;
        if (alls[mid] >= x)
            r = mid;
        else
            l = mid + 1;
    }
    return l;
}

// 主函数入口(使用signed避免与long long冲突)
signed main()
{
    // 读取总长度、区间数量和可选择区间上限
    cin >> n >> m >> k;

    // 读取所有区间,并将端点加入离散化数组
    for (int i = 0; i < m; i++)
    {
        cin >> l[i] >> r[i];
        alls.push_back(l[i]);       // 区间左端点
        alls.push_back(r[i] + 1);   // 区间右端点+1(用于差分)
    }
    // 加入全局边界
    alls.push_back(1);
    alls.push_back(n + 1);

    // 离散化:排序去重
    sort(alls.begin(), alls.end());
    alls.erase(unique(alls.begin(), alls.end()), alls.end());

    // 记录每个离散化位置上的区间进出事件
    vector<vector<int>> in(alls.size());    // 进入事件
    vector<vector<int>> out(alls.size());   // 离开事件

    for (int i = 0; i < m; i++)
    {
        int lid = find(l[i]);           // 左端点离散化后的索引
        int rid = find(r[i] + 1);       // 右端点+1离散化后的索引
        in[lid].push_back(i);           // 在左端点处加入区间
        out[rid].push_back(i);          // 在右端点+1处移除区间
    }

    // 扫描线:维护当前覆盖的区间集合
    set<int> S;                         // 存储当前覆盖当前位置的区间编号
    for (int i = 0; i + 1 < alls.size(); i++)
    {
        int len = alls[i + 1] - alls[i]; // 当前段的长度

        // 处理离开事件:移除在当前段结束时离开的区间
        for (auto x : out[i])
            S.erase(x);

        // 处理进入事件:加入在当前段开始时进入的区间
        for (auto x : in[i])
            S.insert(x);

        // 如果当前段没有被任何区间覆盖
        if (S.empty())
        {
            diff[0] -= len;             // 未被覆盖的部分,选择任意k个区间都无法覆盖
        }
        else
        {
            // 获取当前覆盖区间的最小和最大编号
            int mn = *S.begin();        // 最小编号的区间
            int mx = *S.rbegin();       // 最大编号的区间

            // 如果这些区间在编号上是连续的(跨度不超过k)
            if (mx - mn + 1 <= k)
            {
                int x = k - (mx - mn + 1);  // 还可以额外选择的区间数量
                diff[mn + 1] += len;        // 从mn+1开始可以覆盖这段
                diff[max(0LL, mn - x)] -= len; // 最多可以向左扩展到mn-x
            }
        }
    }

    // 计算最终答案:通过差分数组前缀和找到最大覆盖长度
    int ans = 0;
    for (int i = 0; i + k <= m; i++)
    {
        if (i)
            diff[i] += diff[i - 1];     // 前缀和累加
        ans = max(ans, n + diff[i]);    // 更新最大覆盖长度
    }

    // 输出结果
    cout << ans << endl;

    return 0;
}

【运行结果】

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