解法一:莫队+RMQ
看到“子序列的子序列”这样的问题,首先想到莫队,即离线排序之后不断移动指针。下面以
[l,r−1]
[
l
,
r
−
1
]
转移到
[l,r]
[
l
,
r
]
为例,分析转移的方法。
可以看出,
[l,r]
[
l
,
r
]
的结果比
[l,r−1]
[
l
,
r
−
1
]
的结果多了
∑ri=lminrj=ia[j]
∑
i
=
l
r
min
j
=
i
r
a
[
j
]
,也就是
[l,r]
[
l
,
r
]
,
[l+1,r]
[
l
+
1
,
r
]
,
[l+2,r]
[
l
+
2
,
r
]
,
...
.
.
.
,
[r−1,r]
[
r
−
1
,
r
]
,
[r,r]
[
r
,
r
]
这
r−l+1
r
−
l
+
1
个子序列的最小值之和。
可以看出,如果设
x=maxr−1i=1i[a[i]<a[r]]
x
=
max
i
=
1
r
−
1
i
[
a
[
i
]
<
a
[
r
]
]
(也就是
x
x
等于满足且
a[i]<a[r]
a
[
i
]
<
a
[
r
]
的最大的
i
i
),那么左端点在区间内,右端点为
r
r
的所有个子序列的最小值都是
a[r]
a
[
r
]
。
同样也可以得到:如果设
y
y
等于满足且
a[i]<a[x]
a
[
i
]
<
a
[
x
]
的最大的
i
i
,那么左端点在区间内,右端点为
r
r
的所有个子序列的最小值都是
a[x]
a
[
x
]
。
这样,预处理出
pre[i]
p
r
e
[
i
]
等于满足
j<i
j
<
i
且
a[j]<a[i]
a
[
j
]
<
a
[
i
]
的最大的
j
j
(可以用单调栈得出),那么可以预处理一个类似于前缀和的东西,即
这时候就很容易得出,如果设 k k 初始等于,并且不断地使 k=pre[k] k = p r e [ k ] 可以让 k k 变成,那么:
而如果不能使 k k 变成,怎么办呢?考虑利用RMQ,找到 [l,r] [ l , r ] 内 位置最右的最小值的位置 w w ,这样左端点在内,右端点为 r r 的所有个子序列的最小值都是 a[w] a [ w ] 。
此时就一定能使 k k 变成。所以:
对于其他的转移也是类似了。但要注意一个细节:处理从 [l+1,r] [ l + 1 , r ] 转移到 [l,r] [ l , r ] 时,必须找出 [l,r] [ l , r ] 内位置 最左而不是最右的最小值位置,或者 suf[i] s u f [ i ] 记录满足 j>i j > i 且 a[j]≤a[i] a [ j ] ≤ a [ i ] 的最小的 j j 而不是。
复杂度 O(nn−−√) O ( n n )
代码:
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
inline int read() {
int res = 0; bool bo = 0; char c;
while (((c = getchar()) < '0' || c > '9') && c != '-');
if (c == '-') bo = 1; else res = c - 48;
while ((c = getchar()) >= '0' && c <= '9')
res = (res << 3) + (res << 1) + (c - 48);
return bo ? ~res + 1 : res;
}
typedef long long ll;
const int N = 1e5 + 5, LogN = 20;
int n, m, S, a[N], pre[N], suf[N], RMQ[N][LogN], Log[N], stk[N], top;
ll now, suml[N], sumr[N], res[N];
void init_rmq() {
int i, j; Log[0] = -1;
for (i = 1; i <= n; i++) Log[i] = Log[i >> 1] + 1, RMQ[i][0] = i;
for (j = 1; j <= 18; j++)
for (i = 1; i + (1 << j) - 1 <= n; i++) {
int l = RMQ[i][j - 1], r = RMQ[i + (1 << j - 1)][j - 1];
RMQ[i][j] = a[l] < a[r] ? l : r;
}
stk[top = 0] = 0; for (i = 1; i <= n; i++) {
while (top && a[stk[top]] >= a[i]) top--;
pre[stk[++top] = i] = stk[top - 1];
}
stk[top = 0] = n + 1; for (i = n; i; i--) {
while (top && a[stk[top]] > a[i]) top--;
suf[stk[++top] = i] = stk[top - 1];
}
for (i = 1; i <= n; i++) suml[i] = suml[pre[i]] + 1ll * a[i] * (i - pre[i]);
for (i = n; i; i--) sumr[i] = sumr[suf[i]] + 1ll * a[i] * (suf[i] - i);
}
int qmin(int l, int r) {
int x = Log[r - l + 1], y = RMQ[l][x], z = RMQ[r - (1 << x) + 1][x];
return a[y] < a[z] ? y : z;
}
struct cyx {
int l, r, bl, id; ll ans;
} que[N];
inline bool comp(const cyx &a, const cyx &b) {
if (a.bl != b.bl) return a.bl < b.bl;
return a.r < b.r;
}
ll queryle(int l, int r) {
int x = qmin(l, r); if (x == l) return 1ll * a[x] * (r - l + 1);
return 1ll * a[x] * (r - x + 1) + sumr[l] - sumr[x];
}
ll queryri(int l, int r) {
int x = qmin(l, r); if (x == r) return 1ll * a[x] * (r - l + 1);
return 1ll * a[x] * (x - l + 1) + suml[r] - suml[x];
}
int main() {
int i; n = read(); m = read(); S = sqrt(n);
for (i = 1; i <= n; i++) a[i] = read(); init_rmq();
for (i = 1; i <= m; i++) que[i].l = read(), que[i].r = read(),
que[i].id = i, que[i].bl = (que[i].l - 1) / S + 1;
sort(que + 1, que + m + 1, comp); int l = 1, r = 0;
for (i = 1; i <= m; i++) {
int tl = que[i].l, tr = que[i].r;
while (r < tr) now += queryri(l, ++r);
while (l > tl) now += queryle(--l, r);
while (r > tr) now -= queryri(l, r--);
while (l < tl) now -= queryle(l++, r);
que[i].ans = now;
}
for (i = 1; i <= m; i++) res[que[i].id] = que[i].ans;
for (i = 1; i <= m; i++) printf("%lld\n", res[i]);
return 0;
}
解法二:扫描线+线段树
同样,利用单调栈,预处理出:
prei
p
r
e
i
:在
i
i
的左边,值比小并且与
i
i
最近的位置。
:在
i
i
的右边,值比小或与
ai
a
i
相等,并且与
i
i
最接近的位置。
求整个序列的最小值之和,可以反过来考虑每个作为最小值出现了多少次。
整个序列的最小值之和为:
把问题抽象到一个 n×n n × n 的二维平面上,对于每个坐标 (i,j) ( i , j ) ,
如果 i≤j i ≤ j ,则坐标 (i,j) ( i , j ) 上的数为区间 [i,j] [ i , j ] 的最小值。
如果 i>j i > j ,则坐标 (i,j) ( i , j ) 上的数为 0 0 。
这样一个询问相当于询问横坐标为 [l,n] [ l , n ] ,纵坐标为 [l,r] [ l , r ] 的矩形内的数之和。
预处里可以看成 n n 次修改:每次修改把横坐标为,纵坐标为 [i,sufi−1] [ i , s u f i − 1 ] 的矩形内的所有数加上 ai a i 。
这样(可能?)可以用二维线段树求解,但可以发现这样十分不可做。
考虑 扫描线+线段树。用一条垂直于 x x 轴的扫描线从右往左扫描,因为询问的横坐标范围都是的形式,因此可以将询问按照横坐标范围的左端点( l l )从大到小排序,扫描线扫到时,就可以回答对应的询问了。
但是对于修改还是不好操作。因此考虑用线段树维护,在每个位置维护一个一次函数:
使得扫描线扫到 x=l x = l 时, y y 坐标为的数之和为 kil+bi k i l + b i 。
可以把一个修改拆成两个:
1、一个修改进入扫描线( x=i x = i )时,区间 [i,sufi−1] [ i , s u f i − 1 ] 内的 k k 减去, b b 加上。
2、一个修改离开扫描线( x=prei x = p r e i )时,区间 [i,sufi−1] [ i , s u f i − 1 ] 内的 k k 加上, b b 减去。
将修改也按照进入或离开扫描线的位置从大到小排序,就能支持这个操作了。
复杂度 O(nlogn) O ( n log n )
代码:
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define p2 p << 1
#define p3 p << 1 | 1
using namespace std;
inline int read() {
int res = 0; bool bo = 0; char c;
while (((c = getchar()) < '0' || c > '9') && c != '-');
if (c == '-') bo = 1; else res = c - 48;
while ((c = getchar()) >= '0' && c <= '9')
res = (res << 3) + (res << 1) + (c - 48);
return bo ? ~res + 1 : res;
}
typedef long long ll;
const int N = 1e5 + 5, M = N << 1, L = M << 1;
int n, m, tot, a[N], pre[N], suf[N], top, stk[N]; ll ans[N];
struct cyx {
int t, l, r; ll dx, dy; cyx() {}
cyx(int _t, int _l, int _r, ll _dx, ll _dy) :
t(_t), l(_l), r(_r), dx(_dx), dy(_dy) {}
} orz[M];
struct pyz {int l, r, id;} que[N]; ll sumx[L], sumy[L], addx[L], addy[L];
inline bool comp1(cyx &a, cyx &b) {return a.t > b.t;}
inline bool comp2(pyz &a, pyz &b) {return a.l > b.l;}
void down(int p) {
addx[p2] += addx[p]; addx[p3] += addx[p]; addx[p] = 0;
addy[p2] += addy[p]; addy[p3] += addy[p]; addy[p] = 0;
}
void upt(int l, int r, int p) {
int mid = l + r >> 1; sumx[p] = sumx[p2] + sumx[p3] + addx[p2] *
(mid - l + 1) + addx[p3] * (r - mid); sumy[p] = sumy[p2] + sumy[p3]
+ addy[p2] * (mid - l + 1) + addy[p3] * (r - mid);
}
void change(int l, int r, int s, int e, ll dx, ll dy, int p) {
if (l == s && r == e) return (void) (addx[p] += dx, addy[p] += dy);
int mid = l + r >> 1; down(p);
if (e <= mid) change(l, mid, s, e, dx, dy, p2);
else if (s >= mid + 1) change(mid + 1, r, s, e, dx, dy, p3);
else change(l, mid, s, mid, dx, dy, p2),
change(mid + 1, r, mid + 1, e, dx, dy, p3); upt(l, r, p);
}
ll ask(int l, int r, int s, int e, int x, int p) {
if (l == s && r == e) return (sumx[p] + addx[p] * (r - l + 1))
* x + sumy[p] + addy[p] * (r - l + 1);
int mid = l + r >> 1; down(p); ll res;
if (e <= mid) res = ask(l, mid, s, e, x, p2);
else if (s >= mid + 1) res = ask(mid + 1, r, s, e, x, p3);
else res = ask(l, mid, s, mid, x, p2) + ask(mid + 1, r, mid + 1, e, x, p3);
return upt(l, r, p), res;
}
int main() {
int i, j; n = read(); m = read(); for (i = 1; i <= n; i++) a[i] = read();
stk[top = 0] = 0; for (i = 1; i <= n; i++) {
while (top && a[stk[top]] >= a[i]) top--;
pre[stk[++top] = i] = stk[top - 1];
}
stk[top = 0] = n + 1; for (i = n; i; i--) {
while (top && a[stk[top]] > a[i]) top--;
suf[stk[++top] = i] = stk[top - 1];
}
for (i = 1; i <= n; i++) {
orz[++tot] = cyx(i, i, suf[i] - 1, -a[i], 1ll * (i + 1) * a[i]);
orz[++tot] = cyx(pre[i], i, suf[i] - 1, a[i], -1ll * (pre[i] + 1) * a[i]);
}
for (i = 1; i <= m; i++) que[i].l = read(), que[i].r = read(), que[i].id = i;
sort(orz + 1, orz + tot + 1, comp1); sort(que + 1, que + m + 1, comp2);
for (i = j = 1; i <= m; i++) {
while (j <= tot && orz[j].t >= que[i].l) change(1, n, orz[j].l,
orz[j].r, orz[j].dx, orz[j].dy, 1), j++;
ans[que[i].id] = ask(1, n, que[i].l, que[i].r, que[i].l, 1);
}
for (i = 1; i <= m; i++) printf("%lld\n", ans[i]);
return 0;
}