bzoj 4540: [Hnoi2016]序列 (莫队+ST表+单调栈|线段树)

15 篇文章 0 订阅
11 篇文章 0 订阅

题解

传送门

题目大意:给定长度为 n 的序列:a1,a2,,an,记为 a[1:n] 。类似地, a[l:r]1lrN 是指序列: al,al+1,,ar1,ar 。若 1lstrn ,则称 a[s:t] a[l:r] 的子序列。现在有 q 个询问,每个询问给定两个数l r 1lrn,求 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。

题解1

莫队+单调栈+ST表 时间复杂度 O(nn)
考虑每个点会成为最小值的区间有什么特点,设当前位置为x,然后用l表示最靠左的大于等于x的位置(也可以说是最靠右的小于x的位置的下一个位置),r表示最靠右的大于等于x的位置。
那么如果区间的最小值是x,那么区间的左端点一定在[l,x],右端点一定在[x,r]
对于每个点的l,r我们可以通过单调栈 O(n) 的求解。
假设现在的询问区间是[ls,rs-1],那么我么考虑加入rs有什么影响?我们实际上是加入了(r-l+1)个区间,现在我们需要知道这些区间的最小值分别是什么。首先我们先确定出[ls,rs]中最小值的位置x,这个的话可以建立ST表,然后每次O(1)的查询。那么对于起点在[ls,x],终点在rs的区间,最小值一定是a[x]这是没问题的,可以O(1)计算。那么对于起点在[x+1,rs]的区间怎么计算呢?其实对于每个点来说控制的一定是一段连续的区间,到现在上面预处理的l,r就可以排上用场了。起点在[rs,r[rs]]的区间的最小值是rs,那么我们可以维护一个类似前缀和的东西。sumr[x]=sumr[r[x]]+(x-r[x])*a[x],那么新增的区间的总值就是sumr[rs]-sumr[mn] mn表示的是区间最小值得位置。
对于l的加入也用类似的方法就可以完美的解决了。

代码1

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#define N 100003
#define LL long long 
using namespace std;
int m,n,st[20][N],l[N],belong[N],top,stack[N],nxt[N],last[N],a[N];
LL ans,c[N],sumr[N],suml[N];
struct data{
    int l,r,id;
}q[N];
int cmp(data a,data b){
    return belong[a.l]<belong[b.l]||belong[a.l]==belong[b.l]&&a.r<b.r;
}
void init()
{
    top=0; stack[++top]=1;
    for (int i=2;i<=n;i++) {
        while (top&&a[stack[top]]>=a[i]) top--;
        last[i]=stack[top]; stack[++top]=i;
    }
    top=0; stack[0]=n+1; stack[++top]=n; nxt[n]=n+1;
    for (int i=n-1;i>=1;i--) {
        while (top&&a[stack[top]]>=a[i]) top--;
        nxt[i]=stack[top]; stack[++top]=i;
    }
    for (int i=1;i<=n;i++) sumr[i]=sumr[last[i]]+(LL)(i-last[i])*a[i];
    for (int i=n;i>=1;i--) suml[i]=suml[nxt[i]]+(LL)(nxt[i]-i)*a[i];
}
int query(int x,int y)
{
    int k=l[y-x];
    if (a[st[k][x]]<=a[st[k][y-(1<<k)+1]]) return st[k][x];
    else return st[k][y-(1<<k)+1];
}
void changer(int x,int y,int val)
{
    int mn=query(x,y); LL sum=(LL)(mn-x+1)*a[mn];
    sum+=sumr[y]-sumr[mn]; ans+=(LL)val*sum;
}
void changel(int x,int y,int val)
{
    int mn=query(x,y); LL sum=(LL)(y-mn+1)*a[mn];
    sum+=suml[x]-suml[mn]; ans+=(LL)val*sum;
}
int main()
{
    freopen("a.in","r",stdin);
//  freopen("my.out","w",stdout);
    scanf("%d%d",&n,&m);
    for (int i=1;i<=n;i++) scanf("%d",&a[i]),st[0][i]=i;
    for (int i=1;i<=17;i++)
     for (int j=1;j<=n;j++)
      if (j+(1<<i)-1<=n) {
        if (a[st[i-1][j]]<=a[st[i-1][j+(1<<(i-1))]]) st[i][j]=st[i-1][j];
        else st[i][j]=st[i-1][j+(1<<(i-1))];
      }
    int j=0;
    for (int i=1;i<=n;i++) {
        if (1<<(j+1)<=i) j++;
        l[i]=j;
    }
    int block=ceil(sqrt(n));
    for (int i=1;i<=n;i++) belong[i]=(i-1)/block+1;
    for (int i=1;i<=m;i++) scanf("%d%d",&q[i].l,&q[i].r),q[i].id=i;
    sort(q+1,q+m+1,cmp);
    init();
    int l=1; int r=1; ans=a[1];
    for (int i=1;i<=m;i++) {
        while (q[i].r>r) changer(l,++r,1);
        while (q[i].l<l) changel(--l,r,1);
        while (q[i].l>l) changel(l++,r,-1);
        while (q[i].r<r) changer(l,r--,-1);
        c[q[i].id]=ans;
    }
    for (int i=1;i<=m;i++) printf("%I64d\n",c[i]);
}


题解2

线段树 时间复杂度 O(nlogn) 但是常数巨大比上面 O(nn) 的还要慢。。。。。
我们将询问离线,然后将询问按照终点排序,然后从1到n依次遍历每一个位置,并且同时计算答案。
假设我们现在遍历到的位置为x,对于每个位置我们都维护两个值 sum[now] , val[now] (x后面的点初始值为0).
下面给出 sum val 的定义。 val[now]=min{a[k]} k[now,x] 字面意思就是区间[now,x]的最小值。 sum[now]=xi=1val[now] 表示的是所有历史版本的 val[now] 的和,换句话说就是起点在now,终点在[now,x]的所有最小值的和。
那么对于每个询问l,r来说,当x=r时 ans=ri=lsum[i] 。 好啦现在我们的问题就是如何快速的求 ans .这种区间求知问题容易想到用线段树来维护。
我们现在将每个位置的定义扩展到线段树上,对于每个点now代表[l,r]一区间。
Val[now]=ri=lval[now] , Sum[now]=xi=1Val[now] ,我们考虑新加入一个位置后,怎么更新前面点的值。
加入a[x],那么对于x最靠左的大于等于x的位置L ,L 到x这段区间的val值都变成了a[x],所有可以用线段树的区间修改Val。对于Sum来说 Sum=sum+Val 但是如果只是这样做的话,我们的区间标记十分不好维护,所以我们考虑更改修改的方式,建立转移的矩阵,因为矩阵是满足结合律的。
维护四个标记a,b,c,d
Val=Vala+b(rl+1)
Sum=Sum+cVal+d(rl+1)

[lenValSum] * 100ba0dc1 因为区间的长度len是不变的,所以乘上矩阵后的结果其实就是上面的两个式子。
对于两个标记的问题,运用矩阵的结合律就可以了。
100x.bx.a0x.dx.c1 * 100y.by.a0y.dy.c1 = 100y.b+x.by.ax.ay.a0y.d+x.d+x.by.cx.c+y.cx.a1 不过对于我们来说其实没必要真正的维护矩阵,只要维护a,b,c,d的值就可以了,每次标记重叠的时候用上面矩阵的计算方式合并即可。初始时所有位置的四个标记的值都是a=1,b=c=d=0.
对于区间Val的修改,我们在对应的区间下放a=0,b=a[x],c=0,d=0.对于全局的Sum的更新我们下放a=1,b=0,c=1,d=0.

代码2

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#define N 100003
#define LL long long
using namespace std;
struct data{
    int l,r,id;
}q[N];
struct node{
    LL a,b,c,d;
    void clear() { a=1,b=c=d=0;}
}delta[N*4];
int n,m,top,st[N]; 
LL sum[N*4],val[N*4],a[N],ans[N];
node operator +(node x,node y) { return (node){x.a*y.a,y.b+x.b*y.a,x.c+y.c*x.a,x.d+y.d+x.b*y.c}; }
int cmp(data a,data b){
    return a.r<b.r;
}
void update(int now)
{
    val[now]=val[now<<1]+val[now<<1|1];
    sum[now]=sum[now<<1]+sum[now<<1|1];
}
void build(int now,int l,int r)
{
    val[now]=sum[now]=0; delta[now].clear();
    if (l==r) return;
    int mid=(l+r)/2;
    build(now<<1,l,mid); 
    build(now<<1|1,mid+1,r);
    update(now);
}
void add(int now,int l,int r,node t){
    LL len=(LL)(r-l+1);
    sum[now]+=t.c*val[now]+t.d*len;
    val[now]=t.a*val[now]+t.b*len;
    delta[now]=delta[now]+t;
}
void pushdown(int now,int l,int r)
{
    int mid=(l+r)/2; node t=delta[now];
    if (t.a!=1||t.b||t.c||t.d) {
        add(now<<1,l,mid,t); add(now<<1|1,mid+1,r,t);
        delta[now].clear();
    }
}
void qjchange(int now,int l,int r,int ll,int rr,node t)
{
    if (ll<=l&&r<=rr) {
        add(now,l,r,t);
        return;
    }
    int mid=(l+r)/2;
    pushdown(now,l,r);
    if (ll<=mid) qjchange(now<<1,l,mid,ll,rr,t);
    if (rr>mid) qjchange(now<<1|1,mid+1,r,ll,rr,t);
    update(now);
}
LL qjsum(int now,int l,int r,int ll,int rr)
{
    if (ll<=l&&r<=rr) return sum[now];
    int mid=(l+r)/2; pushdown(now,l,r);
    LL ans=0;
    if (ll<=mid) ans+=qjsum(now<<1,l,mid,ll,rr);
    if (rr>mid) ans+=qjsum(now<<1|1,mid+1,r,ll,rr);
    return ans;
}
int main()
{
    freopen("a.in","r",stdin);
//  freopen("my.out","w",stdout);
    scanf("%d%d",&n,&m);
    for (int i=1;i<=n;i++) scanf("%I64d",&a[i]);
    for (int i=1;i<=m;i++) scanf("%d%d",&q[i].l,&q[i].r),q[i].id=i;
    sort(q+1,q+m+1,cmp);
    top=0; build(1,1,n); int j=1;
    for (int i=1;i<=n;i++) {
        while (top&&a[st[top]]>=a[i]) top--;
        node tmp=(node){0,a[i],0,0}; 
        qjchange(1,1,n,st[top]+1,i,tmp);
        tmp=(node){1,0,1,0}; st[++top]=i;
        add(1,1,n,tmp);
        while (j<=m&&q[j].r==i) ans[q[j].id]=qjsum(1,1,n,q[j].l,q[j].r),j++;
    }
    for (int i=1;i<=m;i++) printf("%I64d\n",ans[i]);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值