1 条题解

  • 0
    @ 2025-10-22 15:42:25

    题解:最大化序列最小值

    题目描述

    给定一个长度为 nn 的正整数序列 AA,以及 mm 个区间 [li,ri][l_i, r_i] 和两个正整数 a,ka, k。我们需要从这 mm 个区间中选出恰好 kk 个区间,对每个选中的区间执行一次区间加 aa 的操作(每个区间最多只能选择一次)。目标是最大化操作后序列的最小值,即最大化 min{Ai}\min\{A_i\}

    问题分析

    这是一个典型的最大化最小值问题,通常可以使用二分答案的策略来解决。

    核心思路

    1. 二分答案框架:我们二分最终的序列最小值 xx,检查是否存在一种选择 kk 个区间的方法,使得操作后每个位置的值都 x\geq x

    2. 验证函数:对于给定的目标值 xx,我们需要验证是否能够通过最多 kk 次区间加操作,让所有 aixa_i \geq x

    算法详解

    1. 二分边界确定

    • 下界l=min(ai)l = \min(a_i),即序列中的最小值
    • 上界r=max(ai)+k×Ar = \max(a_i) + k \times A,即最大值加上可能的最大增益

    二分过程:

    while (l < r) {
        mid = (l + r + 1) >> 1;
        if (check(mid))
            l = mid;
        else
            r = mid - 1;
    }
    

    2. 验证函数 check(x)

    对于目标值 xx,我们需要判断是否能用不超过 kk 个区间使得所有位置满足 aixa_i \geq x

    2.1 需求计算

    对于每个位置 ii,计算需要增加的次数:

    $$\text{need}_i = \left\lceil \frac{x - a_i}{A} \right\rceil $$

    如果 xaix \leq a_i,则 needi=0\text{need}_i = 0

    2.2 贪心策略

    从左到右扫描每个位置 ii

    1. 维护候选区间:将所有左端点 =i= i 的区间加入优先队列(按右端点从大到小排序)

    2. 检查当前覆盖:用线段树维护每个位置当前被多少个选中的区间覆盖

    3. 补充覆盖:如果当前位置 ii 的覆盖次数 <needi< \text{need}_i,则从优先队列中选取区间进行补充:

      • 优先选择右端点最大的区间(最大化覆盖范围)
      • 丢弃右端点小于当前位置的无效区间
      • 每选择一个区间,计数器 cntkcnt_k 加 1
    4. 终止条件

      • 如果 cntk>kcnt_k > k,失败
      • 如果队列为空但仍不满足需求,失败

    3. 数据结构设计

    3.1 线段树

    用于维护每个位置当前的覆盖次数:

    • 功能:区间加、单点查询
    • 操作
      • add(l, r, 1):区间 [l,r][l, r] 覆盖次数 +1+1
      • query(i, 1):查询位置 ii 的覆盖次数

    3.2 优先队列

    按右端点从大到小排序,快速找到能覆盖当前位置且覆盖范围最远的区间。

    代码实现详解

    关键函数

    1. 线段树操作

    void Build(int l, int r, int p) {
        // 构建线段树
        node[p].l = l; node[p].r = r;
        node[p].lz = node[p].sum = 0ll;
        if (l == r) return;
        int mid = (l + r) >> 1;
        Build(l, mid, lc);
        Build(mid + 1, r, rc);
        pushup(p);
    }
    
    void add(int l, int r, int p) {
        // 区间加操作
        pushdown(p);
        if (l <= node[p].l && node[p].r <= r) {
            node[p].sum += (node[p].r - node[p].l + 1);
            ++node[p].lz;
            return;
        }
        int mid = (node[p].l + node[p].r) >> 1;
        if (l <= mid) add(l, r, lc);
        if (mid < r) add(l, r, rc);
        pushup(p);
    }
    
    long long query(int x, int p) {
        // 单点查询
        pushdown(p);
        if (node[p].l == x && node[p].r == x)
            return node[p].sum;
        int mid = (node[p].l + node[p].r) >> 1;
        if (x <= mid) return query(x, lc);
        return query(x, rc);
    }
    

    2. 验证函数

    bool check(int x) {
        Build(1, n, 1);
        int cnt_k = 0;
        while (!pq.empty()) pq.pop();
        
        for (int i = 1; i <= n; ++i) {
            // 计算当前点需要增加的次数
            long long rst = Ceil((long long)x - a[i], (long long)A);
            
            // 将左端点为i的区间加入优先队列
            for (int j = 0; j < (int)vec[i].size(); ++j) {
                int v = vec[i][j];
                pq.push(seg[v]);
            }
            
            // 补充覆盖直到满足需求
            while (query(i, 1) < rst) {
                if (pq.empty()) return 0;
                int l = pq.top().l, r = pq.top().r;
                pq.pop();
                ++cnt_k;
                if (cnt_k > k) return 0;
                if (r < i) continue;  // 无效区间
                add(l, r, 1);
            }
        }
        return cnt_k <= k;
    }
    

    3. 辅助函数

    long long Ceil(long long x, long long y) {
        if (x <= 0ll) return 0ll;
        return (x % y == 0 ? x / y : x / y + 1);
    }
    

    复杂度分析

    • 二分答案O(log(max(ai)+kA))O(\log(\max(a_i) + kA))
    • 验证函数O(nlogn+mlogm)O(n \log n + m \log m)
      • 线段树操作:O(logn)O(\log n)
      • 优先队列操作:O(logm)O(\log m)
      • 每个区间最多入队出队一次
    • 总复杂度O(T×(nlogn+mlogm)×logV)O(T \times (n \log n + m \log m) \times \log V),其中 VV 是值域范围

    关键点说明

    1. 为什么贪心策略有效?

    • 从左到右扫描:确保不遗漏任何位置的需求
    • 优先选择右端点大的区间:最大化覆盖范围,为后面的位置提供更多帮助
    • 及时丢弃无效区间:避免浪费操作次数

    2. 为什么使用线段树?

    需要快速知道每个位置当前的覆盖次数,以便判断是否需要额外区间。线段树提供了高效的区间修改和单点查询。

    3. 边界情况处理

    • Ceil函数:正确处理 xaix \leq a_i 的情况
    • 区间有效性检查:过滤右端点小于当前位置的区间
    • 操作次数限制:严格控制在 kk 次以内

    样例分析

    输入:

    1
    3 3 2 1
    1 3 2
    1 1
    1 3
    3 3
    

    过程:

    • 初始序列:[1,3,2][1, 3, 2]
    • 选择区间 [1,1][1,1][1,3][1,3] 进行加 11 操作
    • 结果序列:[1+1+1,3+1,2+1]=[3,4,3][1+1+1, 3+1, 2+1] = [3, 4, 3]
    • 最小值为 33

    总结

    这道题是二分答案 + 贪心 + 数据结构的经典组合:

    1. 二分答案确定目标最小值
    2. 贪心选择覆盖区间
    3. 线段树维护覆盖信息
    4. 优先队列优化区间选择

    这种思路在"最大化最小值"类问题中非常有效,是算法竞赛中的标准解法之一。通过合理的数据结构选择和贪心策略设计,我们能够在较大的数据范围内高效解决问题。

    • 1

    信息

    ID
    3681
    时间
    1000ms
    内存
    256MiB
    难度
    10
    标签
    递交数
    1
    已通过
    1
    上传者