4540: [Hnoi2016]序列
Time Limit: 20 Sec Memory Limit: 512 MBSubmit: 1758 Solved: 834
[Submit][Status][Discuss]
Description
给定长度为n的序列:a1,a2,…,an,记为a[1:n]。类似地,a[l:r](1≤l≤r≤N)是指序列:al,al+1,…,ar-
1,ar。若1≤l≤s≤t≤r≤n,则称a[s:t]是a[l:r]的子序列。现在有q个询问,每个询问给定两个数l和r,1≤l≤r
≤n,求a[l:r]的不同子序列的最小值之和。例如,给定序列5,2,4,1,3,询问给定的两个数为1和3,那么a[1:3]有
6个子序列a[1:1],a[2:2],a[3:3],a[1:2],a[2:3],a[1:3],这6个子序列的最小值之和为5+2+4+2+2+2=17。
Input
输入文件的第一行包含两个整数n和q,分别代表序列长度和询问数。接下来一行,包含n个整数,以空格隔开
,第i个整数为ai,即序列第i个元素的值。接下来q行,每行包含两个整数l和r,代表一次询问。
Output
对于每次询问,输出一行,代表询问的答案。
Sample Input
5 2 4 1 3
1 5
1 3
2 4
3 5
2 5
Sample Output
17
11
11
17
HINT
1 ≤N,Q ≤ 100000,|Ai| ≤ 10^9
分析:非常好的一道题.
先看看这道题有什么特点:只有查询操作,查询操作带有浓浓的数学味道.
关于多次查询问题,最简单的做法就是先预处理,把所有可能的答案用数组存下来. 这道题中l,r可能高达10w,存不下.那就省掉一维,试着维护前缀和?似乎可以,但是好像不太好推出一个区间[l,r]的答案.
只有查询,没有修改,可以想到一个经典算法:莫队算法. 那么现在的问题就变成了从[L,R-1]转移到[L,R],答案会发生什么变化.(其它的转移可以类比).
显然只需要考虑以R为右端点,左端点在[L,R]的区间的贡献. 因为有影响的是当前区间的最小值,所以先找到[L,R]的最小值的位置pos. 对于左端点在[L,pos]的区间,答案肯定是a[pos]. 那么左端点在[pos + 1,R]的区间的答案是什么呢?
不太容易想到. 维护一个前缀和. 令fl[i]表示以i为右端点,左端点在[1,i]的区间的答案之和,l[i]表示i左边第一个大于i的数的位置. 那么fl[i] = fl[l[i]] + a[l] * (i - l[i]). l[i]可以用单调栈来处理. 左端点在[pos + 1,R]的区间的答案和就是fl[R] - fl[pos].因为a[pos] <= a[R],如果左端点跨过了pos,一定会被fl[pos]统计到,所以满足区间相减的性质.
剩下的几种操作也是类似的. 10w的数据,只有60分.
怎么办呢? 这种多次查询的问题要么每次能够很快地回答查询,通常利用数据结构,要么利用维护的前缀/后缀信息来计算得到答案. 考虑第二种方法.
一个区间可以理解为一个左端点和右端点组成的二元组(l,r),对二元组分类来求.
还是先找到[l,r]的最小值的位置pos. 将[l,r]内的区间分成3类:
1.左端点右端点分别在pos左右的.
2.左端点右端点都在pos左侧的.
3.左端点右端点都在pos右侧的.
对于第一类区间的答案,由于a[pos]是[l,r]的最小值了,那么这一类区间的答案之和就是a[pos] * (pos - l + 1) * (r - pos + 1).
另外两类本质上是一样的,弄清楚一类怎么统计就好了.
考虑第3类. 先对fl数组求个前缀和suml, 因为现在是直接统计答案而不是用 莫队的增量法只考虑一个端点的影响了. 那么右端点在pos右侧的答案之和就是suml[r] - suml[pos]. 这保证了右端点是合法的,但是左端点可能在pos左边,怎么办呢?
和莫队算法的思想是一样的. 因为a[pos]是最小的. 那么跨过pos的区间的答案一定是a[pos].现在只需要减去左端点在pos左侧的答案即可,它们的答案是fl[pos] * (r - pos). 如果把fl[pos]看作a[pos] * pos在左边能控制的点的范围(权值比a[pos]大的点). 那么就相当于合法的点对数*贡献,有点乘法原理的意思.
如何查询最小值的位置?要实现O(1)查询最值,ST表即可. 注意,这里的ST表不再是记录值,而是记录下标.
so,这道题就被完美的解决了,最终的时间复杂度是O(nlogn).
回顾一下这道题.
先知道这个问题的特征是什么:没有修改操作,只有多次查询操作.
多次查询问题怎么办?要么每次查询的足够快,要么直接预处理所有可能的区间的答案,要么维护前缀/后缀信息能快速查询.
查询的足够快?上数据结构!事实上这道题确实有线段树的做法,只是我不会......
预处理所有可能的区间的答案?数组开不下,显然不可以.
维护前缀/后缀信息快速查询答案?有点难想......
但是别忘了,解决这类题目有一个算法--莫队算法. 利用增量法,固定一个端点,另一个端点在一个区间内的答案和. 10w的数据会T.
那么就直接求区间的答案就好了. 将区间看作一个二元组,进行分类,分别用维护的信息来求就好啦.
总的思路就是这样的,还是很清晰的.
60分莫队:
#include <cstdio> #include <cmath> #include <cstring> #include <iostream> #include <algorithm> using namespace std; typedef long long ll; const ll maxn = 100010,inf = 0x7fffffff; ll n,Q,top,sta[maxn],a[maxn],l[maxn],r[maxn],fl[maxn],fr[maxn],block; ll pos[maxn][20],L = 1,R = 0,anss[maxn],ans; struct node { ll l,r,id; }q[maxn]; bool cmp(node a,node b) { ll alpos = (a.l - 1) / block + 1,blpos = (b.l - 1) / block + 1; if (alpos == blpos) return a.r < b.r; return alpos < blpos; } void pre() { top = 0; for (ll i = 1; i <= n; i++) { while(top && a[sta[top]] > a[i]) top--; if (!top) l[i] = 0; else l[i] = sta[top]; sta[++top] = i; } memset(sta,0,sizeof(sta)); top = 0; for (ll i = n; i >= 1; i--) { while(top && a[sta[top]] > a[i]) top--; if (!top) r[i] = n + 1; else r[i] = sta[top]; sta[++top] = i; } fl[1] = a[1]; for (ll i = 2; i <= n; i++) fl[i] = fl[l[i]] + (i - l[i]) * a[i]; fr[n] = a[n]; for (ll i = n - 1; i >= 1; i--) fr[i] = fr[r[i]] + (r[i] - i) * a[i]; for (ll i = 1; i <= n; i++) pos[i][0] = i; for (ll j = 1; j <= 19; j++) for (ll i = 1; i + (1 << j) - 1 <= n; i++) { if (a[pos[i][j - 1]] < a[pos[i + (1 << (j - 1))][j - 1]]) pos[i][j] = pos[i][j - 1]; else pos[i][j] = pos[i + (1 << (j - 1))][j - 1]; } } ll query(ll l,ll r) { ll t = (ll)((log(r - l + 1)) / log(2.0)); if (a[pos[l][t]] < a[pos[r - (1 << t) + 1][t]]) return pos[l][t]; return pos[r - (1 << t) + 1][t]; } void add1(ll l,ll r,ll v) { if (l > r) return; ll temp = query(l,r); ans += v * (temp - l + 1) * a[temp]; ans += v * (fl[r] - fl[temp]); } void add2(ll l,ll r,ll v) { if (l > r) return; ll temp = query(l,r); ans += v * (r - temp + 1) * a[temp]; ans += v * (fr[l] - fr[temp]); } void solve() { for (ll i = 1; i <= Q; i++) { ll l = q[i].l,r = q[i].r; while (R < r) { ++R; add1(L,R,1); } while(R > r) { add1(L,R,-1); R--; } while (L < l) { add2(L,R,-1); L++; } while (L > l) { L--; add2(L,R,1); } anss[q[i].id] = ans; } } int main() { scanf("%lld%lld",&n,&Q); block = sqrt(n); for (ll i = 1; i <= n; i++) scanf("%lld",&a[i]); pre(); for (ll i = 1; i <= Q; i++) { scanf("%lld%lld",&q[i].l,&q[i].r); q[i].id = i; } sort(q + 1,q + 1 + Q,cmp); solve(); for (ll i = 1; i <= Q; i++) printf("%lld ",anss[i]); return 0; }
100分算法:
#include <cstdio> #include <cmath> #include <cstring> #include <iostream> #include <algorithm> using namespace std; typedef long long ll; const ll maxn = 100010,inf = 0x7fffffff; ll n,Q,top,sta[maxn],a[maxn],l[maxn],r[maxn],fl[maxn],fr[maxn],block; ll pos[maxn][20],L = 1,R = 0,anss[maxn],ans,suml[maxn],sumr[maxn]; struct node { ll l,r,id; }q[maxn]; bool cmp(node a,node b) { ll alpos = (a.l - 1) / block + 1,blpos = (b.l - 1) / block + 1; if (alpos == blpos) return a.r < b.r; return alpos < blpos; } void pre() { top = 0; for (ll i = 1; i <= n; i++) { while(top && a[sta[top]] > a[i]) top--; if (!top) l[i] = 0; else l[i] = sta[top]; sta[++top] = i; } memset(sta,0,sizeof(sta)); top = 0; for (ll i = n; i >= 1; i--) { while(top && a[sta[top]] > a[i]) top--; if (!top) r[i] = n + 1; else r[i] = sta[top]; sta[++top] = i; } fl[1] = a[1]; for (ll i = 2; i <= n; i++) fl[i] = fl[l[i]] + (i - l[i]) * a[i]; fr[n] = a[n]; for (ll i = n - 1; i >= 1; i--) fr[i] = fr[r[i]] + (r[i] - i) * a[i]; for (ll i = 1; i <= n; i++) suml[i] = suml[i - 1] + fl[i]; for (ll i = n; i >= 1; i--) sumr[i] = sumr[i + 1] + fr[i]; for (ll i = 1; i <= n; i++) pos[i][0] = i; for (ll j = 1; j <= 19; j++) for (ll i = 1; i + (1 << j) - 1 <= n; i++) { if (a[pos[i][j - 1]] < a[pos[i + (1 << (j - 1))][j - 1]]) pos[i][j] = pos[i][j - 1]; else pos[i][j] = pos[i + (1 << (j - 1))][j - 1]; } } ll query(ll l,ll r) { ll t = (ll)((log(r - l + 1)) / log(2.0)); if (a[pos[l][t]] < a[pos[r - (1 << t) + 1][t]]) return pos[l][t]; return pos[r - (1 << t) + 1][t]; } int main() { scanf("%lld%lld",&n,&Q); block = sqrt(n); for (ll i = 1; i <= n; i++) scanf("%lld",&a[i]); pre(); for (ll i = 1; i <= Q; i++) { ll l,r; scanf("%lld%lld",&l,&r); ll temp = query(l,r); ans = 0; ans += a[temp] * (temp - l + 1) * (r - temp + 1); ans += suml[r] - suml[temp] - fl[temp] * (r - temp); ans += sumr[l] - sumr[temp] - fr[temp] * (temp - l); printf("%lld ",ans); } return 0; }