*II. CF1117G Recursive Queries
摘自学习笔记 简单树论 笛卡尔树部分例题 II.
hot tea.
实际上题目转化一下,把区间的贡献算到单点上,就是求对 ([l,r]) 的元素建出笛卡尔树,求每个节点的 (dep / size) 之和:
做法 1:
但是这样做反而麻烦了,考虑每个位置究竟作为哪个区间的最大值出现:设 (p_i) 表示 (i) 左边第一个比 (a_i) 大的元素的位置,(q_i) 表示 (i) 右边第一个,显然 (i) 作为 ([p_i+1,q_i-1]) 的最大值出现,因此答案为 (sum_{i=l}^rmax(r,q_i-1)-min(p_i+1,l)+1)。
将答案拆成 (max) 和 (min) 两部分来算,考虑将 (r) 从 (n) 拖到 (1) 的过程中,每个点究竟对应 (q_i-1) 还是 (r) 只会改变一次。因此将询问离线下来,对于对应 (q_i-1) 的部分,直接树状数组维护查询区间 (q_i-1) 的和,而对于对应 (r) 的部分,再开一个树状数组维护有多少个对应 (r) 的数,区间求和并乘以 (r) 即可。对于 (min) 同理。时间复杂度 (mathcal{O}((n+q)log n))。
const int N = 1e6 + 5;
int n, q, a[N], l[N], r[N];
int stc[N], *top = stc, p[N];
ll ans[N];
vpii qu[N];
vint buc[N];
struct BIT {
ll c[N];
void clear() {mem(c, 0, N);}
void add(int x, int v) {while(x <= n) c[x] += v, x += x & -x;}
ll query(int x) {ll s = 0; while(x) s += c[x], x -= x & -x; return s;}
ll query(int l, int r) {return query(r) - query(l - 1);}
} tr1, tr2;
int main(){
cin >> n >> q;
for(int i = 1; i <= n; i++) a[i] = read();
for(int i = 1; i <= q; i++) l[i] = read();
for(int i = 1; i <= q; i++) r[i] = read(), qu[r[i]].pb(l[i], i);
for(int i = n; i; i--) {
while(*top && a[*top] < a[i]) top--;
p[i] = *top ? *top : n + 1; *++top = i;
buc[p[i] - 1].pb(i), tr1.add(i, p[i] - 1);
}
for(int i = n; i; i--) {
for(auto it : qu[i]) ans[it.se] = tr1.query(it.fi, i) + tr2.query(it.fi, i) * i;
for(auto it : buc[i]) tr1.add(it, 1 - p[it]), tr2.add(it, 1);
}
tr1.clear(), tr2.clear(), top = stc;
for(int i = 1; i <= n; i++) qu[i].clear(), buc[i].clear();
for(int i = 1; i <= q; i++) qu[l[i]].pb(r[i], i);
for(int i = 1; i <= n; i++) {
while(*top && a[*top] < a[i]) top--;
p[i] = *top ? *top : 0; *++top = i;
buc[p[i] + 1].pb(i), tr1.add(i, p[i] + 1);
}
for(int i = 1; i <= n; i++) {
for(auto it : qu[i]) ans[it.se] -= tr1.query(i, it.fi) + tr2.query(i, it.fi) * i;
for(auto it : buc[i]) tr1.add(it, - 1 - p[it]), tr2.add(it, 1);
}
for(int i = 1; i <= q; i++) print(ans[i] + r[i] - l[i] + 1), pc(' ');
return flush(), 0;
}
做法 2:
对于一个区间 ([l,r]),它的笛卡尔树可以通过前缀 ([1,r]) 的笛卡尔树得到。求出 ([l,r]) 最大值位置 (p) 后,(p) 显然是 ([l,r]) 笛卡尔树的根。由于一个子树对应的下标连续,因此 ([p+1,r]) 对答案的贡献可以通过求它们在 ([1,r]) 的笛卡尔树上的 (dep) 之和减去 (dep_p imes (r-p)) 得到,即 (p) 的右子树一定是 ([p+1,r]) 的笛卡尔树。为什么 ([l,p-1]) 不行呢?因为受到了 ([1,l-1]) 的影响,即一个节点的父亲很有可能不在 ([l,p-1]) 而在 ([1,l-1]) 当中。
怎么办呢,问题不大,倒过来再做一遍就行了。求区间最大值可以在单调栈维护的笛卡尔树的右链上二分。
整理一下,在加入节点时我们需要支持区间修改:不断弹出小于当前值的栈顶直到大于当前值,设其下标为 (p),那么 (p) 的右子树对应的下标 ([p+1,r]) 的深度加上 (1)。查询时需要支持区间查询。BIT 即可,时间复杂度 (mathcal{O}((n+q)log n))。
启发:对于和最值有关的题目,将询问按照区间最大值分割成两个互不相关的查询可以有效简化问题。同时,左右两端第一个大于 / 小于当前值的位置有很好的性质,要好好利用(HNOI2016 序列)。
const int N = 1e6 + 5;
int n, q, stc[N], *top = stc, a[N], l[N], r[N];
ll ans[N];
vpii qu[N];
struct BIT {
ll c[N], ic[N];
void clear() {mem(c, 0, N), mem(ic, 0, N);}
void add(int x, ll v) {
ll vv = x * v;
while(x <= n) c[x] += v, ic[x] += vv, x += x & -x;
}
ll query(int x) {
ll s = 0, sv = 0, xx = x;
while(x) s += c[x], sv += ic[x], x -= x & -x;
return s * (xx + 1) - sv;
}
void add(int l, int r, int v) {add(l, v), add(r + 1, -v);}
ll query(int l, int r) {return query(r) - query(l - 1);}
} tr;
int main(){
cin >> n >> q;
for(int i = 1; i <= n; i++) a[i] = read();
for(int i = 1; i <= q; i++) l[i] = read();
for(int i = 1; i <= q; i++) r[i] = read(), qu[r[i]].pb(l[i], i);
for(int i = 1; i <= n; i++) {
while(*top && a[*top] < a[i]) top--;
if(*top) tr.add(i, i, tr.query(*top, *top));
tr.add(*top + 1, i, 1), *++top = i;
for(auto it : qu[i]) {
int l = 1, r = top - stc, lp = it.fi;
while(l < r) {
int m = l + r >> 1;
stc[m] < lp ? l = m + 1 : r = m;
}
int p = stc[l], d = tr.query(p, p) - 1;
ans[it.se] = tr.query(p, i) - 1ll * (i - p + 1) * d;
}
}
tr.clear(), top = stc;
for(int i = 1; i <= n; i++) qu[i].clear();
for(int i = 1; i <= q; i++) qu[l[i]].pb(r[i], i);
for(int i = n; i; i--) {
while(*top && a[*top] < a[i]) top--;
if(*top) tr.add(i, i, tr.query(*top, *top));
tr.add(i, *top ? *top - 1 : n, 1), *++top = i;
for(auto it : qu[i]) {
int l = 1, r = top - stc, rp = it.fi;
while(l < r) {
int m = l + r >> 1;
stc[m] > rp ? l = m + 1 : r = m;
}
int p = stc[l], d = tr.query(p, p) - 1;
ans[it.se] += tr.query(i, p - 1) - 1ll * (p - i) * d;
}
}
for(int i = 1; i <= q; i++) print(ans[i]), pc(' ');
return flush(), 0;
}
两种方法效率差别不是很大。