题解:
法一:
[bzoj4826][HNOI2017]影魔
直接转化成区间内单点的贡献,
分开p1,p2考虑
而min(ai,aj),max(ai,aj)要考虑固定一个点、
对于p1,固定i为较小值。发现,这个j只有L[i]或R[i]满足。
对于p2,差分。找j满足max(j+1~i-1)<max(ai,aj)
固定i为较大值。发现,这个j的范围是[L[i]+1,i-1],或者[i+1,R[i]-1]。
具体处理询问的时候,对边界l,r取min,取max即可。(见上面的博客)
发现列出来的式子,
区间、统计小于某个数的个数/这些数的总和,主席树即可。
细节处理好即可。
代码:
#include<bits/stdc++.h> #define reg register int #define il inline #define numb (ch^'0') #define mid ((l+r)>>1) using namespace std; typedef long long ll; il void rd(int &x){ char ch;x=0;bool fl=false; while(!isdigit(ch=getchar()))(ch=='-')&&(fl=true); for(x=numb;isdigit(ch=getchar());x=x*10+numb); (fl==true)&&(x=-x); } namespace Miracle{ const int N=200000+5; int n,m; int a[N],le[N],ri[N]; int sta[N],top; struct segmenttree{ ll sum[N*20],sz[N*20]; int ls[N*20],rs[N*20]; int rt[N]; int cnt; void ins(int &x,int y,int l,int r,int c){ x=++cnt; sum[x]=sum[y]+c;sz[x]=sz[y]+1; ls[x]=ls[y],rs[x]=rs[y]; if(l==r)return; if(c<=mid) ins(ls[x],ls[y],l,mid,c); else ins(rs[x],rs[y],mid+1,r,c); } ll ql(int x,int y,int l,int r,int c){//num >= c int d=sz[x]-sz[y]; if(!d) return 0; if(l==r) return d*(l>=c); if(mid>=c) return sz[rs[x]]-sz[rs[y]]+ql(ls[x],ls[y],l,mid,c); else return ql(rs[x],rs[y],mid+1,r,c); } ll qr(int x,int y,int l,int r,int c){//num <= c int d=sz[x]-sz[y]; if(!d) return 0; if(l==r) return d*(r<=c); if(mid<c) return sz[ls[x]]-sz[ls[y]]+qr(rs[x],rs[y],mid+1,r,c); else return qr(ls[x],ls[y],l,mid,c); } ll qmin(int x,int y,int l,int r,int c){ int d=sz[x]-sz[y]; if(!d) return 0; if(l==r) return d*min(l,c); if(mid>=c) return (sz[rs[x]]-sz[rs[y]])*c+qmin(ls[x],ls[y],l,mid,c); else return sum[ls[x]]-sum[ls[y]]+qmin(rs[x],rs[y],mid+1,r,c); } ll qmax(int x,int y,int l,int r,int c){ int d=sz[x]-sz[y]; if(!d) return 0; if(l==r) return d*max(r,c); if(mid<c) return (sz[ls[x]]-sz[ls[y]])*c+qmax(rs[x],rs[y],mid+1,r,c); else return sum[rs[x]]-sum[rs[y]]+qmax(ls[x],ls[y],l,mid,c); } }L,R; int p1,p2; int main(){ rd(n);rd(m);rd(p1);rd(p2); for(reg i=1;i<=n;++i) rd(a[i]); for(reg i=1;i<=n;++i){ while(top&&a[sta[top]]<a[i]) ri[sta[top--]]=i; sta[++top]=i; } while(top) ri[sta[top--]]=n+1; for(reg i=n;i>=1;--i){ while(top&&a[sta[top]]<a[i]) le[sta[top--]]=i; sta[++top]=i; } while(top) le[sta[top--]]=0; for(reg i=1;i<=n;++i){ ++le[i],++ri[i];//warning!!! //cout<<i<<" : "<<le[i]<<" "<<ri[i]<<endl; L.ins(L.rt[i],L.rt[i-1],1,n+2,le[i]); R.ins(R.rt[i],R.rt[i-1],1,n+2,ri[i]); } int l,r; while(m--){ rd(l);rd(r); ll t1=L.ql(L.rt[r],L.rt[l-1],1,n+2,l+1)+R.qr(R.rt[r],R.rt[l-1],1,n+2,r+1); //cout<<" t1 "<<t1<<endl; ll t2=R.qmin(R.rt[r],R.rt[l-1],1,n+2,r+2)-L.qmax(L.rt[r],L.rt[l-1],1,n+2,l)-2*(r-l+1); printf("%lld ",t1*p1+(t2-t1)*p2); } return 0; } } int main(){ Miracle::main(); return 0; } /* Author: *Miracle* Date: 2018/12/8 19:27:35 */
法二:
[Hnoi2017]影魔 (单调栈+扫描线+线段树)
这个还是比较有意思的。虽然不如法一优秀。(离线算法,而且还难写)
还是考虑贡献。
但是这次我们不枚举端点,我们枚举c,也就是[i+1,j-1]的最大值
那么,对于i,考虑i作为这个最大值的能贡献出的数对具体是哪些。(注意,具体是哪些,法一我们只关心个数,但是法二要考虑是哪些位置)
对于p1,发现就贡献一个数对(L[i],R[i])(当然,(i,i+1)不会考虑到,要特殊处理)
对于p2,发现,贡献([L[i]+1,i-1],R[i])以及(L[i],[i+1,R[i]-1])这些数对。
(证明不重不漏的话,每个数对在枚举到中间的最大值的时候都会考虑到)
然后,数对抽象成平面直角坐标系中的一个点
(图片来自上面博客)
满足条件的加入的点就是一些点和线段
发现,询问其实是求位于x属于[l,r],y属于[l,r]的点的个数。
离线差分线段树扫描线即可。
这个转化还是很漂亮的。(以前一直在找把区间点对(l,r)抽象成直角坐标系中点的问题。。)
法一法二的共同思路:
考虑每个点的贡献。
法一枚举点对的端点
法二枚举点对之间最大值的位置。
对于不同的枚举有不同的处理方法。
对于这种区间点对问题,经常是考虑每个点的贡献,
并且在枚举的时候,由于比较灵活,所以可以在加上一些钦定
(常用的是靠右的点,但是这个题的话,就不好考虑相对大小关系了,所以钦定是较小值较大值最大值)
如果像这个题还要询问,那么就考虑把一个点的贡献缩成一个值,然后用支持区间查询的数据结构维护一下。
当然询问离线也是常用的方法。