题目链接
CF1039E Summer Oenothera Exhibition
题目大意
给定一个长度为 (n) 的序列 (a_i) 和一个数字 (w),(q) 次询问,每次给出 (k),要求将 (a_i) 分成若干段,满足一段内的极差 (max-minleq w-k), 求最小段数。
(1leq n,qleq 10^5),(a_i,k,wleq 10^9)
时限 (7000;ms)
思路
这个题主要有两个做法,一个是 (O(n^{frac{5}{3}}+n^{frac{4}{3}}log n)) 的分块做法,一个是 (O(nsqrt nlog 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}-igeq A),且 (suf_i) 和 (i) 在同一条链上,另设 (cnt_i) 为从 (i) 跳到 (suf_i) 一共跳了多少次,这样查询答案时直接跳 (suf_i) 即可,每次答案加上 (cnt_i)。由于只需要关注 ([i,i+A)) 内的情况,我们的 (nxt_i) 也只处理这么多,当 (nxt_igeq i+A) 时就不管它了。
我们需要维护的 (nxt_iin[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_igeq i+A),这里的 (nxt) 我们没有去维护,那就用倍增的方式去找 (nxt_i),用 (ST) 表来维护区间极差。而现在我们每一步的距离都至少为 (A),从而查询的时间复杂度是 (O(qfrac{n}{A}log n)) 的。
目前总时间复杂度 (O(nA^2+qfrac{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] {nlog n})),然而这个连 (O(n^2)) 的暴力都不如诶!
观察式子,左边的 (O(n^{frac{5}{3}})) 已经够通过此题了,复杂度瓶颈在于右边的倍增查询,那咋办呢?目前可行的做法似乎只有减少倍增次数了,观察先前的算法,对于 (nxt_igeq i+A) 的部分,我们是直接弃疗的,问题就出在这里,如果我们再多维护一点 (nxt_i) 呢?
注意到当 (nxt_igeq i+A) 时,(nxt_i) 的变化就不会影响任何一个点的 (suf) 或 (cnt) 了,就是说如果要维护的话,我们可以直接 (O(1)) 地做。
于是再设一个块长 (B),当 (i+Aleq 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)) 的优秀复杂度。
然后就做完了,总结一下,这个分块的做法是一种分三类的分治:
- (nxt_i-i<n^{frac{1}{3}}):块内路径压缩加速
- (n^{frac{1}{3}}leq nxt_i-i<n^{frac{2}{3}}):暴力维护 (nxt_i) 的跳转情况
- (nxt_i-ileq n^{frac{2}{3}}):用倍增配合 (ST) 表找到下一个跳转位置
实现细节
- 可以用一个小根堆维护 (i) 到 (nxt_i) 之间的极差,方便每次 (k) 变化时更新对应的 (nxt_i)。
- 当 (Aleq 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
", 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^2log n)) 的,还没暴力快,但是此时查询的时间复杂度只有 (O(nlog n)) (默认 (n,q) 同阶),于是考虑用根号分治平衡两者的时间复杂度。
这时间复杂度关系看起来就知道最后应该两者都是 (O(nsqrt 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(nsqrt nlog n)) 的,于是我们用 (LCT) 加根号分治的做法解决了本题。虽然跑起来比做法一要慢,但是脱离数据范围从时间复杂度上来看,这个做法是更优的。
实现细节
- 这里需要保证 (nxt_i) 任意时刻都是 (i) 的父亲,也就是说 (LCT) 的操作不能进行 (make\_root),所以 (link) 和 (cut) 的时候直接 (access) 然后修改就可以了,同时操作的两个节点的父子关系也要注意一下。
- (O(nsqrt n log n)) 的运算量达到了 (5e8),需要注意 (LCT) 的常数优化(就算如此 (O(n^2)) 还能卡过去真是一件离谱的事情),如 (splay) 的时候不要用 STL 的 (stack),而要手写,还有由于已经保证了所有操作的合法性,在 (link,cut) 的时候就不要再判了。((TLE) 到怀疑人生 *2)
- 由于 (nxt) 会更新 (O(nsqrt 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
", ans[i]);
return 0;
}
完结撒花。