「IOI2021」分糖果


题目

点这里看题目。

分析

有一定难度的题目,但是可以说问题的限制是比较常见的。

Subtask 3

也就在这个 subtask 上面有所突破

所有糖果盒子的容量相等,不妨设这个值为 \(c\)

问题的结构是“区间修改、单点查询”,这里我们可以扫描序列,在端点处插入或删除修改,从而具体地拿出每个糖果盒子面对的修改。

接着考虑修改本身的特点。我们可以将多个修改的接连进行等价看作函数复合,则不难发现修改没有交换律,因此我们必须保证解决询问时,修改是有序的

不难想到直接使用线段树来维护修改集合,这样我们需要做的是快速函数复合。

先来考虑一个更简单的模型:给定一个修改序列,如何求出初始糖果数为 \(w\) 时,经过修改后的最终值?

称某一次操作后 \(p+v_j>c_i\) 为“碰上壁”,某一次操作后 \(p+v_j<0\) 为碰下壁。首先,对于修改序列,我们可以找出 \(w\) 的区间 \([l_0,r_0]\),使得当且仅当 \(w\in [l_0,r_0]\) 时,\(w\) 不会碰壁,此时可以直接算出结果为 \(w+\sum v\)

否则 \(w\) 会碰壁,但是如果我们可以找出 \(w\) 最后一次碰壁的位置,我们就可以直接越过所有碰壁过程而计算最终结果。这个东西一时半会儿算不出来,我们间接地先考虑第一次碰壁,这个很容易用二分求解。如果第一次碰了上壁,则值得注意的一点是,\(w=c\) 出发也一定会在同一个位置碰上壁,因此在这次碰壁之后,\(w=c\) 的路线和 \(w\) 原本的路线就是一致的,我们可以直接调用 \(w=c\) 这条路线最后一次碰壁的位置(假如我们可以做到),从而算出 \(w\) 的结果。

第一次碰了下壁的情况类似,调用 \(w=0\) 的即可。因此,对于一个序列,我们需要求出:

  • \([l_0,r_0]\)\(\sum v\)
  • \(w=c\)\(w=0\) 时最后一次碰上壁的位置;

这些信息可以在线段树上很方便地合并(合并过程需要二分),因此可以 \(O(q\log_2^2n)\) 解决。

比正解难写,比正解慢 ??

正解

前面的扫描线思路是没有问题的,问题在于我们没有必要动态维护答案,只需要解决 \(n\) 次询问。

我们尝试改写一下答案的形式。还是认为我们有了一个长度为 \(m\) 修改序列,第 \(k\) 次的参数为 \(v_k\)

两个限制我们先挑简单的解决,那当然是 \(\max\{0,p+v_k\}\) 的限制容易。考虑按照操作编号和操作结束后糖果数构成坐标,并且画出轨迹。则每次碰下壁一定会导致轨迹上移,最终结果更大。因此,我们设 \(f(c,k)\) 为容量限制为 \(c\),从第 \(k\) 个操作开始,且不考虑下界时的最终糖果数,则答案为 \(\max_{1\le k\le m}\{f(c,k)\}\)

再考虑 \(f(c,k)\) 怎么求。类似的,我们发现每次碰下壁就会导致轨迹下移,且最终结果更小。设 \(s_k=\sum_{j=k}^mv_j\),则可以得到 \(f(c,k)=\min\{s_k,\min_{k\le j\le m}s_j+c\}\)

最终我们需要求的是:

\[\max_{1\le k\le m}\{\min\{s_k,\min_{k\le j\le m}s_j+c\}\} \]

想个办法把 \(\max\) 化掉,所以我们可以尝试随便提两个元素出来比大小。取 \(1\le k_1\le k_2\le m\),则一定有 \(\min_{k_1\le j\le m}s_j\le \min_{k_2\le j\le m}\)。如果还有 \(s_{k_1}\le s_{k_2}\),则 \(k_1\) 劣于 \(k_2\),我们不需要考虑 \(k_1\)

因此,有效的 \(k_2\) 对应了 \(s\) 的后缀最大值。当然,更简单的做法是直接求:

\[\max_{1\le k\le m}\{\min\{\max_{k\le j\le m}s_k,\min_{k\le j\le m}s_j + c\}\} \]

注意到 \(\min\) 中的两项都是单调的,并且彼此异调,因此 \(\max\) 一定取在大小关系交换处。线段树上维护后缀最值,二分找出断点即可。

时间复杂度为 \(O((n+q)\log n)\)

主体思路

graph TB lmt1(区间修改单点查询)-->pnt1(扫描线) lmt2(复合性,保序要求)-->pnt2(线段树) pnt1-->pnt2 subgraph "Subtask 3" pnt3(试图合并修改)-->pnt4(观察第一次碰壁)-->pnt10(碰壁单调性) end pnt2-->pnt3 subgraph std pnt5(数学化目标)-->pnt6(用'枚举'方式改写限制) lmt3(min,max 限制)--分步处理限制-->pnt6 pnt6--整理-->pnt7(初步的求值式子)==主动消项,用单调性去掉没用的项==>pnt8(简化的求值式子) pnt8--异调 min 最值-->pnt9(线段树二分断点) end pnt2-->pnt9 pnt9-->final(正解)

代码

#include "candies.h"

#include 
#include 

#define rep( i, a, b ) for( int i = (a) ; i <= (b) ; i ++ )
#define per( i, a, b ) for( int i = (a) ; i >= (b) ; i -- )

typedef long long LL;
typedef std :: vector Vec;

namespace Studio {
    const  LL INF = 1e18;
    const int MAXN = 2e5 + 5;
    
    template
    _T MIN( const _T a, const _T b ) {
        return a < b ? a : b;
    }

    template
    _T MAX( const _T a, const _T b ) {
        return a > b ? a : b;
    }

    std :: vector que[MAXN];

    LL su[MAXN << 2], sufMn[MAXN << 2], sufMx[MAXN << 2];

    int qL[MAXN], qR[MAXN], qV[MAXN]; 
    LL ans[MAXN];

    int C[MAXN];

    int N, Q;

    inline void Upt( const int x ) {
        su[x] = su[x << 1] + su[x << 1 | 1];
        sufMn[x] = MIN( sufMn[x << 1] + su[x << 1 | 1], sufMn[x << 1 | 1] );
        sufMx[x] = MAX( sufMx[x << 1] + su[x << 1 | 1], sufMx[x << 1 | 1] );
    }

    void Build( const int x, const int l, const int r ) {
        if( l > r ) return ;
        if( l == r ) {
            su[x] = sufMn[x] = sufMx[x] = 0;
            return ;
        }
        int mid = ( l + r ) >> 1;
        Build( x << 1, l, mid );
        Build( x << 1 | 1, mid + 1, r );
        Upt( x );
    }

    void Update( const int x, const int l, const int r, const int p, const int nVal ) {
        if( l == r ) {
            su[x] = sufMn[x] = sufMx[x] = nVal;
            return ;
        }
        int mid = ( l + r ) >> 1;
        if( p <= mid ) Update( x << 1, l, mid, p, nVal );
        else Update( x << 1 | 1, mid + 1, r, p, nVal );
        Upt( x );
    }

    LL Query( const int x, const int l, const int r, const int p, const int c, 
              const LL s, const LL mn, const LL mx ) {
        if( l == r ) return MIN( MAX( mx, sufMx[x] + s ), MIN( sufMn[x] + s, mn ) + c );
        int mid = ( l + r ) >> 1;
        if( p <= mid ) return Query( x << 1, l, mid, p, c, 
                                     s + su[x << 1 | 1], MIN( mn, s + sufMn[x << 1 | 1] ), MAX( mx, s + sufMx[x << 1 | 1] ) );
        return Query( x << 1 | 1, mid + 1, r, p, c, s, mn, mx );
    }

    int Search( const int x, const int l, const int r, const int c, const LL s, const LL mn, const LL mx ) {
        if( l == r ) return l;
        int mid = ( l + r ) >> 1;
        if( MAX( s + sufMx[x << 1 | 1], mx ) < MIN( s + sufMn[x << 1 | 1], mn ) + c )
            return Search( x << 1, l, mid, c, 
                           s + su[x << 1 | 1], MIN( s + sufMn[x << 1 | 1], mn ), MAX( s + sufMx[x << 1 | 1], mx ) );
        return Search( x << 1 | 1, mid + 1, r, c, s, mn, mx );
    }

    void Work() {
        rep( i, 1, Q ) 
            que[qL[i] + 1].push_back( i ),
            que[qR[i] + 2].push_back( - i );
        Build( 1, 0, Q );
        rep( i, 1, N ) {
            int n = que[i].size();
            rep( j, 0, n - 1 ) {
                if( que[i][j] > 0 )
                    Update( 1, 0, Q, que[i][j], qV[que[i][j]] );
                else Update( 1, 0, Q, - que[i][j], 0 );
            }
            ans[i] = - INF;
            int brk = Search( 1, 0, Q, C[i], 0, INF, - INF );
            if( brk ) ans[i] = MAX( ans[i], Query( 1, 0, Q, brk, C[i], 0, INF, - INF ) );
            if( brk < Q ) ans[i] = MAX( ans[i], Query( 1, 0, Q, brk + 1, C[i], 0, INF, - INF ) );
            ans[i] = MAX( 0ll, MIN( ans[i], ( LL ) C[i] ) );
        }
    }
}

Vec distribute_candies( Vec c, Vec l, Vec r, Vec v ) {
    static std :: vector ret;
    
    Studio :: N = c.size();
    Studio :: Q = l.size();
    rep( i, 0, Studio :: N - 1 ) Studio ::  C[i + 1] = c[i];
    rep( i, 0, Studio :: Q - 1 ) Studio :: qL[i + 1] = l[i];
    rep( i, 0, Studio :: Q - 1 ) Studio :: qR[i + 1] = r[i];
    rep( i, 0, Studio :: Q - 1 ) Studio :: qV[i + 1] = v[i];
    Studio :: Work();
    rep( i, 0, Studio :: N - 1 ) ret.push_back( Studio :: ans[i + 1] );
    return ret;
}