bzoj4540 [Hnoi2016]序列

4540: [Hnoi2016]序列

Time Limit: 20 Sec  Memory Limit: 512 MB
Submit: 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 5
5 2 4 1 3
1 5
1 3
2 4
3 5
2 5

Sample Output

28
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\n",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\n",ans);
    }

    return 0;
}

 

posted @ 2018-03-15 21:26  zbtrs  阅读(279)  评论(0编辑  收藏  举报