CF1039E Summer Oenothera Exhibition

题目链接

CF1039E Summer Oenothera Exhibition

题目大意

给定一个长度为 \(n\)​​ 的序列 \(a_i\)​​ 和一个数字 \(w\)​,\(q\)​ 次询问,每次给出 \(k\)​,要求将 \(a_i\)​ 分成若干段,满足一段内的极差 \(max-min\leq w-k\)​​, 求最小段数。

\(1\leq n,q\leq 10^5\)\(a_i,k,w\leq 10^9\)

时限 \(7000\;ms\)

思路

这个题主要有两个做法,一个是 \(O(n^{\frac{5}{3}}+n^{\frac{4}{3}}\log n)\)​​ 的分块做法,一个是 \(O(n\sqrt n\log n)\)\(LCT\)​ 加根号分治做法,不过它也可以被 \(O(nq)\)​ 指令集优化的做法卡过去。

做法一

不难想到贪心做法,我们从 \(a_1\)​ 开始,往后能取就取,若取不了就只能再分一段。这样朴素地做是 \(O(nq)\) 的,然后可以发现这个东西看起来非常可以分块,于是有如下思路:

\(nxt_i\) 为在当前 \(k\) 下,从 \(a_i\) 往后的第一个不可以从 \(i\)​ 到这里分为一段的位置,而刚才的贪心过程就相当于从 \(1\) 开始不断跳 \(nxt_i\)​ 直到序列末尾,而答案即为跳的次数。为了处理方便,将 \(k\) 按照 \(w-k\) 从小到大排序,这样可以保证 \(nxt_i\)​ 是一直往前延伸的。

设一个块长 \(A\)​,我们把一个块内的 \(nxt\)​​ 压缩起来,记 \(suf_i\)​ 为在 \(i\)​ 一直跳 \(nxt\)​ 的这条链上,最后一个 \(<i+A\)​ 的位置,即 \(suf_i-i<A\)​,\(nxt_{suf_i}-i\geq A\)​,且 \(suf_i\)​ 和 \(i\)​ 在同一条链上,另设 \(cnt_i\) 为从 \(i\) 跳到 \(suf_i\) 一共跳了多少次,这样查询答案时直接跳 \(suf_i\) 即可,每次答案加上 \(cnt_i\)。由于只需要关注 \([i,i+A)\) 内的情况,我们的 \(nxt_i\) 也只处理这么多,当 \(nxt_i\geq i+A\) 时就不管它了。

我们需要维护的 \(nxt_i\in[i,i+A)\),所以它最多会被修改 \(A\) 次,每次修改的时候相当于 \(i\) 以及它往前的链(实际上是一棵 \(i\) 为子节点,\(nxt_i\) 为父亲的树)从一条链上拆下来,拼到另一条链上,所以 \((i-A,i]\) 中和 \(i\) 在同一链上的点的 \(suf_i\)\(cnt_i\)​​ 都会发生变化,这个可以 \(O(A)\) 地进行修改。从而总变化的时间复杂度是 \(O(nA^2)\) 的。

回到查询,注意到跳 \(suf_i\)​ 时可能会出现 \(suf_i=i\)​ 的情况,即 \(nxt_i\geq i+A\)​,这里的 \(nxt\)​ 我们没有去维护,那就用倍增的方式去找 \(nxt_i\)​,用 \(ST\)​ 表来维护区间极差。而现在我们每一步的距离都至少为 \(A\)​,从而查询的时间复杂度是 \(O(q\frac{n}{A}\log n)\)​ 的。

目前总时间复杂度 \(O(nA^2+q\frac{n}{A}\log n)\),把 \(n\)\(q\) 当成同阶的,取 \(A=n^{\frac{1}{3}}\) 会有较小值 \(O(n^{\frac{5}{3}}+n^{\frac{5}{3}}\log n)\)(我也不知道为啥不取 \(A=\sqrt[3] {n\log n}\)),然而这个连 \(O(n^2)\) ​的暴力都不如诶!

观察式子,左边的 \(O(n^{\frac{5}{3}})\)​​ 已经够通过此题了,复杂度瓶颈在于右边的倍增查询,那咋办呢?目前可行的做法似乎只有减少倍增次数了,观察先前的算法,对于 \(nxt_i\geq i+A\) 的部分,我们是直接弃疗的,问题就出在这里,如果我们再多维护一点 \(nxt_i\) 呢?

注意到当 \(nxt_i\geq i+A\) 时,\(nxt_i\) 的变化就不会影响任何一个点的 \(suf\)\(cnt\) 了,就是说如果要维护的话,我们可以直接 \(O(1)\)​ 地做。

于是再设一个块长 \(B\),当 \(i+A\leq nxt_i<i+B\) 时,我们依然维护 \(nxt_i\),因为是 \(O(1)\) 的,且 \(nxt_i\) 在这种情况下最多被修改 \(O(B)\) 次,所以这一块的时间复杂度是 \(O(nB)\) 的。现在需要倍增的次数就降到了 \(O(\frac{n}{B})\),从而总时间复杂度变成了 \(O(nA^2+\frac{n^2}{A}+nB+\frac{n^2}{B}\log n)\),取 \(A=n^{\frac{1}{3}},B=n^{\frac{2}{3}}\)​,我们获得了 \(O(n^{\frac{5}{3}}+n^{\frac{4}{3}}\log n)\) 的优秀复杂度。

然后就做完了,总结一下,这个分块的做法是一种分三类的分治:

  1. \(nxt_i-i<n^{\frac{1}{3}}\):块内路径压缩加速
  2. \(n^{\frac{1}{3}}\leq nxt_i-i<n^{\frac{2}{3}}\):暴力维护 \(nxt_i\) 的跳转情况
  3. \(nxt_i-i\leq n^{\frac{2}{3}}\)​:用倍增配合 \(ST\) 表找到下一个跳转位置

实现细节

  • 可以用一个小根堆维护 \(i\)\(nxt_i\) 之间的极差,方便每次 \(k\) 变化时更新对应的 \(nxt_i\)
  • \(A\leq nxt_i-i<B\) 时不要把 \(i\) 丢小根堆里,否则这样子总共要更新的 \(nxt_i\)\(O(n^{\frac{5}{3}}\log n)\) 级别的,加之 C++ 的 priority_queue 常数大,是不可能卡过去的,笔者曾因为这个 \(TLE\) 到怀疑人生。正确的处理方式是等要用到 \(nxt_i\) 时,再更新这类情况的 \(nxt\)​ 值。

Code

// Decomposition solution 
// O(n^(5/3)+n^(4/3)logn)

#pragma GCC optimize("O3")
#pragma GCC target("sse,sse2,sse3,ssse3,sse4,popcnt,abm,mmx,avx,avx2,tune=native")
#pragma GCC optimize("inline","fast-math","unroll-loops","no-stack-protector")
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<queue>
#include<vector>
#include<fstream>
#define rep(i,a,b) for(int i = (a); i <= (b); i++)
#define per(i,b,a) for(int i = (b); i >= (a); i--)
#define N 100100
#define A 47    // N^(1/3)
#define B 2155  // N^(2/3)
#define LOG 18
#define PII pair<int, int>
#define fr first
#define sc second
#define Inf 0x3f3f3f3f
using namespace std;

inline int read(){
    int s = 0, w = 1;
    char ch = getchar();
    while(ch < '0' || ch > '9'){ if(ch == '-') w = -1; ch = getchar(); }
    while(ch >= '0' && ch <= '9') s = (s<<3)+(s<<1)+(ch^48), ch = getchar();
    return s*w;
}

struct query{
    int diff, id;
    bool operator < (const query b) const{ return diff < b.diff; }
} ask[N];

int a[N], ans[N];
int n, w, qy;

int stmx[LOG][N], stmn[LOG][N], log[1<<LOG];

int mn[N], mx[N], nxt[N], suf[N], cnt[N];
int chain[N];
priority_queue<PII, vector<PII>, greater<PII> > q;
vector<int> frward, backward;

void init(){
    per(i,n+1,1){
        stmn[0][i] = stmx[0][i] = a[i];
        rep(p,1,LOG) if(i+(1<<p) <= n+2){
            stmn[p][i] = min(stmn[p-1][i], stmn[p-1][i+(1<<(p-1))]);
            stmx[p][i] = max(stmx[p-1][i], stmx[p-1][i+(1<<(p-1))]);
        }
    }
    rep(i,0,LOG-1) log[1<<i] = i;
    rep(i,2,n) if(!log[i]) log[i] = log[i-1];
}

int extreme_diff(int l, int r){
    int p = log[r-l+1];
    return max(stmx[p][l], stmx[p][r-(1<<p)+1]) - min(stmn[p][l], stmn[p][r-(1<<p)+1]);
}

int dis(int j, int i){ return max(abs(a[j]-mn[i]), abs(a[j]-mx[i])); }

bool ok(int j, int i, int d){
    if(dis(j, i) <= d)
        mn[i] = min(mn[i], a[j]), mx[i] = max(mx[i], a[j]);
    else return false;
    return true;
}

void update_chain(int i, int d){
    frward.clear(), backward.clear();
    int tmp = nxt[i];
    while(tmp <= n && tmp-i < B && ok(tmp, i, d)) tmp++;
    nxt[i] = tmp; 
    tmp = i, cnt[i] = 0, frward.push_back(i);
    while(tmp <= n && nxt[tmp]-i < A) cnt[i]++, tmp = nxt[tmp], frward.push_back(tmp);
    suf[i] = tmp;
    chain[i] = 1, backward.push_back(i);


    int num = cnt[i];
    per(j, i, max(1, i-A+1)) if(chain[nxt[j]]){
        backward.push_back(j);
        while(!frward.empty() && frward.back()-j >= A) frward.pop_back(); 
        if(frward.empty()) break;
        cnt[j] = chain[nxt[j]]+frward.size()-1;
        chain[j] = chain[nxt[j]]+1;
        suf[j] = frward.back();
    }
    for(int j : backward) chain[j] = 0;
}

int main(){
    n = read(), w = read(), qy = read();
    rep(i,1,n) a[i] = read();
    a[n+1] = Inf;
    rep(i,1,qy) ask[i].diff = w-read(), ask[i].id = i;

    sort(ask+1, ask+qy+1);
    rep(i,1,n){
        nxt[i] = i+1, suf[i] = min(i+A-1, n+1);
        cnt[i] = min(n+1, suf[i]) - i, q.push({abs(a[nxt[i]]-a[i]), -i});
        mn[i] = mx[i] = a[i];
    }
    init();

    rep(p,1,qy){
        int d = ask[p].diff;
        while(!q.empty() && d >= q.top().fr){
            int i = -q.top().sc; q.pop();
            if(nxt[i]-i >= B || nxt[i] > n) continue;

            if(nxt[i]-i < A) update_chain(i, d);
            else while(nxt[i] < i+B && ok(nxt[i], i, d)) nxt[i]++;
            if(nxt[i] < i+A) q.push({dis(nxt[i], i), -i});
        }

        int i = 1, tot = 0;
        while(i <= n)
            if(suf[i] > i) tot += cnt[i], i = suf[i];
            else{
                tot++;
                while(nxt[i] < i+B && ok(nxt[i], i, d)) nxt[i]++;
                if(nxt[i]-i < B) i = nxt[i];
                else{
                    int j = i;
                    per(p,LOG-1,0){
                        int up = j+(1<<p);
                        if(up <= n+1 && extreme_diff(i, up) <= d) j = up;
                    }
                    i = j+1;
                }
            }
        ans[ask[p].id] = tot-1;
    }

    rep(i,1,qy) printf("%d\n", ans[i]);
    return 0;
}

 

做法二

(前面的分析和上一个做法相同,没看的可以翻上去看一眼)

刚才我们已经有了一个 \(i\)\(nxt_i\) 连边的概念,但是没有充分利用这个性质。多建一个节点 \(a_{i+1}=Inf\),把图画出来可以发现是一棵以 \(n+1\) 为根的树,而我们询问的即为从 \(1\)\(n+1\) 这条树链的长度,由于树的形态会改变,容易想到用 \(Lint\;Cut\;Tree\) 去维护这个信息。

此处大部分题解都提到了 弹飞绵羊 这道题,其实就是个简单序列建树然后 \(LCT\)​ 的板子题?不管了如果您感兴趣的话那也去看一眼吧。

由于节点的变化 \(O(n)\)​​​,暴力维护 \(LCT\)​​​ 是 \(O(n^2\log n)\)​​​ 的,还没暴力快,但是此时查询的时间复杂度只有 \(O(n\log n)\)​​​​​ (默认 \(n,q\)​ 同阶),于是考虑用根号分治平衡两者的时间复杂度。

这时间复杂度关系看起来就知道最后应该两者都是 \(O(n\sqrt n \log n)\)​ 的,具体来说,既然只维护 \(nxt_i\)\(O(\sqrt n)\) 次变化,那么我们就只在 \(nxt_i< i+\sqrt n\) 时在 \(LCT\) 上连边,否则就不管它了。这个时候可能会存在总共 \(O(\sqrt n)\)​ 棵 \(LCT\),在询问时,\(LCT\) 内从当前点 \(O(\log n)\)​ 跳到树根并统计链长,到了树根之后,再用 \(ST\) 表配合倍增找到下一个 \(nxt_i\),在树与树之间跳跃(此时答案要加 \(1\)),最终到达 \(n+1\) 节点。

处理变化,以及查询时树内统计和树间跳跃的时间复杂度都是 \(O(n\sqrt n\log n)\) 的,于是我们用 \(LCT\)​ 加根号分治的做法解决了本题。虽然跑起来比做法一要慢,但是脱离数据范围从时间复杂度上来看,这个做法是更优的。

实现细节

  • 这里需要保证 \(nxt_i\) 任意时刻都是 \(i\) 的父亲,也就是说 \(LCT\) 的操作不能进行 \(make\_root\),所以 \(link\)\(cut\) 的时候直接 \(access\)​ 然后修改就可以了,同时操作的两个节点的父子关系也要注意一下。
  • \(O(n\sqrt n \log n)\) 的运算量达到了 \(5e8\),需要注意 \(LCT\) 的常数优化(就算如此 \(O(n^2)\) 还能卡过去真是一件离谱的事情),如 \(splay\) 的时候不要用 STL 的 \(stack\)​,而要手写,还有由于已经保证了所有操作的合法性,在 \(link,cut\)​ 的时候就不要再判了。(\(TLE\) 到怀疑人生 *2)
  • 由于 \(nxt\) 会更新 \(O(n\sqrt n)\) 次,有 \(3.17e7\) 了,更新 \(nxt_i\)​ 的部分不能采取做法一用小根堆维护的方法,应把要改变的 \(nxt_i\)\(vector\) 存到对应的询问那里,找对应询问二分即可。(\(TLE\) 到怀疑人生 *3)

Code

// LCT and sqrt block solution
// O(n^(3/2)logn)

#pragma GCC optimize("O3")
#pragma GCC target("sse,sse2,sse3,ssse3,sse4,popcnt,abm,mmx,avx,avx2,tune=native")
#pragma GCC optimize("inline","fast-math","unroll-loops","no-stack-protector")
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<vector>
#define rep(i,a,b) for(int i = (a); i <= (b); i++)
#define per(i,b,a) for(int i = (b); i >= (a); i--)
#define N 101000
#define B 317
#define LOG 18
#define Inf 0x3f3f3f3f
#define l(x) t[x].c[0]
#define r(x) t[x].c[1]
#define f(x) t[x].fa
#define siz(x) t[x].siz
#define tree(x) LCT.S.t[x]
#define PII pair<int, int>
#define fr first
#define sc second

using namespace std;

inline int read(){
    int s = 0, w = 1;
    char ch = getchar();
    while(ch < '0' || ch > '9'){ if(ch == '-') w = -1; ch = getchar(); }
    while(ch >= '0' && ch <= '9') s = (s<<3)+(s<<1)+(ch^48), ch = getchar();
    return s*w;
}

struct Splay{
    struct node{
        int c[2], siz, fa, tag;
        node(){ c[0] = c[1] = siz = fa = tag = 0; }
    } t[N];
    int s[N];

    bool isroot(int x){
        return l(f(x)) != x && r(f(x)) != x;
    }
    void flip(int x){
        swap(l(x), r(x)), t[x].tag ^= 1;
    }
    void push_up(int x){ siz(x) = siz(l(x))+siz(r(x))+1; }
    void push_down(int x){
        if(t[x].tag) rep(i,0,1)
            if(t[x].c[i]) flip(t[x].c[i]);
        t[x].tag = 0;
    }

    void rotate(int x){
        int y = f(x), z = f(y);
        int dir1 = (r(y) == x), dir2 = (r(z) == y);
        if(!isroot(y)) t[z].c[dir2] = x;
        f(x) = z;
        int son = t[x].c[dir1^1];
        t[y].c[dir1] = son, f(son) = (son ? y : 0);
        t[x].c[dir1^1] = y, f(y) = x;
        push_up(y);
    }

    void splay(int x){
        int y, z, tmp = x, top = 0;
        while(!isroot(tmp)) s[++top] = tmp, tmp = f(tmp);
        s[++top] = tmp;
        while(top) push_down(s[top--]);
        while(!isroot(x)){
            y = f(x), z = f(y);
            if(!isroot(y)) rotate((l(y) == x)^(l(z) == y) ? x : y);
            rotate(x);
        }
        push_up(x);
    }
};

struct Link_Cut_Tree{
    Splay S;

    void access(int x){
        for(int y = 0; x; x = S.f(y = x))
            S.splay(x), S.r(x) = y, S.push_up(x);
    }

    void make_root(int x){
        access(x), S.splay(x), S.flip(x);
    }
    int find_root(int x){
        access(x), S.splay(x);
        while(S.l(x)) S.push_down(x), x = S.l(x);
        S.splay(x);
        return x;
    }

    void split(int x, int y){
        make_root(x), access(y), S.splay(y);
    }
    void link(int x, int y){
        access(x), S.f(x) = y;
    }
    void cut(int x, int y){
        access(x), S.splay(y);
        if(S.f(x) == y && S.r(y) == x)
            S.f(x) = S.r(y) = 0, S.push_up(y);
    }
} LCT;

int a[N], ans[N];
PII ask[N];
int n, w, qy;

int nxt[N], mn[N], mx[N];
int stmin[N][LOG], stmax[N][LOG], Log[1<<LOG];
vector<int> extend[N];

void init(){
    per(i,n+1,1){
        stmin[i][0] = stmax[i][0] = a[i];
        rep(p,1,LOG-1) if(i+(1<<p) <= n+2){
            stmin[i][p] = min(stmin[i][p-1], stmin[i+(1<<(p-1))][p-1]);
            stmax[i][p] = max(stmax[i][p-1], stmax[i+(1<<(p-1))][p-1]);
        }
    }
    rep(i,0,LOG-1) Log[1<<i] = i;
    rep(i,2,n) if(!Log[i]) Log[i] = Log[i-1];
}

int diff(int l, int r){
    int p = Log[r-l+1];
    return max(stmax[l][p], stmax[r-(1<<p)+1][p]) - min(stmin[l][p], stmin[r-(1<<p)+1][p]);
}

int dis(int i, int j){
    return max(abs(mx[i]-a[j]), abs(mn[i]-a[j]));
}
bool check(int i, int j, int d){
    if(dis(i, j) > d) return false;
    mx[i] = max(mx[i], a[j]), mn[i] = min(mn[i], a[j]);
    return true;
}

int main(){
    n = read(), w = read(), qy = read();
    rep(i,1,n) a[i] = read();
    a[n+1] = Inf, init();
    rep(i,1,qy) ask[i] = {w-read(), i};
    ask[qy+1] = {Inf, 0};

    sort(ask+1, ask+1+qy);
    rep(i,1,n){
        LCT.link(i, i+1), nxt[i] = i+1;
        mn[i] = mx[i] = a[i];
        extend[lower_bound(ask+1, ask+qy+2, make_pair(abs(a[i+1]-a[i]), 0)) - ask].push_back(i);
    }

    rep(p,1,qy){
        int d = ask[p].fr;
        for(int i : extend[p]){
            int j = nxt[i];
            while(j <= n && j < i+B && check(i, j, d)) j++;
            LCT.cut(i, nxt[i]);
            nxt[i] = j;
            if(j < i+B){
                LCT.link(i, nxt[i]);
                extend[lower_bound(ask+p+1, ask+qy+2, make_pair(dis(i, j), 0)) - ask].push_back(i);
            }
        }

        int i = 1, tot = 0;
        while(i <= n){
            int j = LCT.find_root(i);
            tot += tree(j).siz-1, i = j;
            if(j == n+1) break;
            per(p,LOG-1,0) if(j+(1<<p) <= n+1)
                if(diff(i, j+(1<<p)) <= d) j += (1<<p);
            i = j+1, tot++;
        }
        ans[ask[p].sc] = tot-1;
    }

    rep(i,1,qy) printf("%d\n", ans[i]);
    return 0;
}

 

完结撒花。

posted @ 2021-08-02 20:27  Neal_lee  阅读(108)  评论(1编辑  收藏  举报