HNOI2017

单旋

Description

给你一棵单旋splay,让你维护执行某些操作之后节点的深度信息。

单旋splay就是说在祖父-父亲-我,三点一线的时候,仍坚持旋转我两次。

双旋就是先旋父亲一次,再旋我一次。

Solution

题目只要求旋转最小值和最大值到根(其实旋转别的似乎也差不多??)。手模一个三层以上的splay可以发现执行完操作后,相当于把该节点移到根,其儿子过继给父亲,树的其他部分形态都不变。

这说明每次操作深度发生变化的点都很少。比如说旋转最小值\(x\)。那么它深度变成1,其余部分除了它儿子深度不变以外,深度都加+1。把点权离散化,因为保证了权值两两不同,直接用其离散化以后的值作为编号,那么深度+1的点就是\([fa[x],maxn]\)这段区间。区间修改,上线段树直接搞定。

旋转最大值同理。

而删除操作,就是所有节点深度-1,也直接线段树就行。

最后还剩下插入操作。直接模拟显然是不行的。考虑找到\(x\)的前驱和后继(前驱是小于x的中最大的,后继是大于\(x\)的中最小的)。显然只有深度较深的那个没有儿子,把\(x\)接上去就可以啦。

怎么找到前驱后继?并行维护一个set就可以啦。

所以每次操作,维护节点的父亲和儿子,维护set,维护线段树。

Code

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef set<int>::iterator sit;
inline int read(){//be careful for long long!
    register int x=0,f=1;register char ch=getchar();
    while(!isdigit(ch)){if(ch=='-')f=0;ch=getchar();}
    while(isdigit(ch)){x=x*10+(ch^'0');ch=getchar();}
    return f?x:-x;
}

const int N=1e5+10;
int n,m,fa[N],ch[N][2],a[N],rt;
struct s_query{int op,x;}q[N];
set<int> s;

namespace Segment_Tree{
#define ls(p) p<<1
#define rs(p) p<<1|1
    int tr[N<<2],tag[N<<2];
    inline void pushdown(int p){
	if(!tag[p])return;
	tag[ls(p)]+=tag[p],tag[rs(p)]+=tag[p];
	tr[ls(p)]+=tag[p],tr[rs(p)]+=tag[p];
	tag[p]=0;
    }
    inline void Set(int p,int l,int r,int pos,int v){
	if(l==r){tr[p]=v;return;}
	pushdown(p);int mid=(l+r)>>1;
	if(pos<=mid)Set(ls(p),l,mid,pos,v);
	else Set(rs(p),mid+1,r,pos,v);
    }
    inline void Modify(int p,int l,int r,int L,int R,int v){
	if(L<=l&&r<=R){tr[p]+=v,tag[p]+=v;return;}
	pushdown(p);int mid=(l+r)>>1;
	if(L<=mid)Modify(ls(p),l,mid,L,R,v);
	if(mid<R)Modify(rs(p),mid+1,r,L,R,v);
    }
    inline int Query(int p,int l,int r,int pos){
	if(l==r)return tr[p];
	pushdown(p);int mid=(l+r)>>1;
	if(pos<=mid)return Query(ls(p),l,mid,pos);
	else return Query(rs(p),mid+1,r,pos);
    }
}using namespace Segment_Tree;

inline sit Pre(sit it){return --it;}
inline void Insert(int x){
    if(!s.size()){s.insert(x);Set(1,1,m,x,1);rt=x;puts("1");return;}
    int pre=(*Pre(s.lower_bound(x))),nxt=(*s.upper_bound(x));
    int d_pre=Query(1,1,m,pre),d_nxt=Query(1,1,m,nxt),d_x;
    if(s.lower_bound(x)!=s.begin()&&!ch[pre][1]){ch[pre][1]=x;fa[x]=pre;d_x=d_pre+1;}
    if(!fa[x]){ch[nxt][0]=x;fa[x]=nxt;d_x=d_nxt+1;}
    Set(1,1,m,x,d_x);s.insert(x);
    printf("%d\n",d_x);
}
inline void Splay(int type){
    if(!type){
	int p=(*s.begin());printf("%d\n",Query(1,1,m,p));
	if(p==rt)return;
	Set(1,1,m,p,1);Modify(1,1,m,fa[p],m,1);
	ch[fa[p]][0]=ch[p][1];fa[ch[p][1]]=fa[p];
	fa[rt]=p;fa[p]=0;ch[p][1]=rt;rt=p;
    }
    else{
	int p=(*Pre(s.end()));printf("%d\n",Query(1,1,m,p));
	if(p==rt)return;
	Set(1,1,m,p,1);Modify(1,1,m,1,fa[p],1);
	ch[fa[p]][1]=ch[p][0];fa[ch[p][0]]=fa[p];
	fa[rt]=p;fa[p]=0;ch[p][0]=rt;rt=p;
    }
}
inline void Pop(int type){
    Splay(type);s.erase(rt);
    Modify(1,1,m,1,m,-1);
    rt=ch[rt][type^1];fa[rt]=0;
}

int main(){
//    freopen("splay5.in","r",stdin);freopen("out.out","w",stdout);
    n=read();
    for(int i=1;i<=n;++i){q[i].op=read();if(q[i].op==1)q[i].x=a[++m]=read();}
    sort(a+1,a+m+1);
    for(int t=1;t<=n;++t){
	switch(q[t].op){
	case 1:{
	    int x=lower_bound(a+1,a+m+1,q[t].x)-a;
	    Insert(x);break;
	}
	case 2:Splay(0);break;
	case 3:Splay(1);break;
	case 4:Pop(0);break;
	case 5:Pop(1);break;
	}
    }
    return 0;
}

影魔

Solution

暴力是st表预处理出区间最大值,然后\(\text O(n^2)\)枚举\(i,j\)计算贡献,没有什么优化空间,考虑换一种枚举方式。

每一对\((i,j)\)做贡献,其实也可以看作中间的最大值\(k_i\)的贡献。

所以我们尝试从枚举\(k_i\)的角度入手。先求出从\(k_i\)向左走,第一个比它大的位置,记为\(L_i\);还有向右走第一个比它大的位置,记为\(R_i\)。如果区间再往两边延伸,\(k_i\)就不是区间的最大值了,所以\([L_i,R_i]\)就是它的“影响范围”。所有的\(L,R\)可以用一个单调栈从左往右扫一遍求出。

对于每一个\(k_i\),我们考虑一下它的贡献:

  • \(L_i,R_i\)作为左右端点。有\(p_1\)的贡献。
  • \(L_i\)作为左端点,\([i+1,R_i-1]\)作为右端点。每一对有\(p_2\)的贡献。
  • \([L_i+1,i-1]\)作为左端点,\(R_i\)作为右端点。每一对有\(p_2\)的贡献。

发现第二种和第三种点对的贡献,有某一个端点是一段区间。可以把贡献挂链,挂在另一个孤立的端点处。这样每次计算贡献,扫到那个孤立端点时,区间增加贡献即可。

例如第二种,可以把\([i+1,R_i-1]\)的区间放到\(L_i\),当扫到\(L_i\)时,把贡献加上。第三种同理。

而对于第一种,就是单点修改,我们看作把\([R_i,R_i]\)挂在\(L_i\)处,或者\([L_i,L_i]\)挂在\(R_i\)处都行。

现在做法显然了。

用一棵线段树维护贡献。从左往右枚举端点,每扫到一个点\(i\),把挂在其下的贡献加到线段树上。

询问考虑差分解决。对于一个询问\([l,r]\),拆成两个询问\(l-1\)\(r\)

扫到\(l-1\)时,记\([l,r]\)的贡献和为\(sum1\);扫到\(r\)时,记\([l,r]\)贡献和为\(sum2\)。那么答案就是\(sum2-sum1\)

Code

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
inline int read(){//be careful for long long!
    register int x=0,f=1;register char ch=getchar();
    while(!isdigit(ch)){if(ch=='-')f=0;ch=getchar();}
    while(isdigit(ch)){x=x*10+(ch^'0');ch=getchar();}
    return f?x:-x;
}

const int N=2e5+10;
int n,m,p1,p2,a[N],L[N],R[N],stk[N],tp,tot;
ll ans[N];
struct s_node{int l,r,p,v,id;inline bool operator <(const s_node &a)const{return p<a.p;}}op[N*3],q[N<<1];

namespace Segment_Tree{
    ll tr[N<<2],tag[N<<2];
#define ls(p) p<<1
#define rs(p) p<<1|1
    inline void pushdown(int p,int l,int r){
	if(!tag[p])return;
	tag[ls(p)]+=tag[p],tag[rs(p)]+=tag[p];
	int mid=(l+r)>>1;
	tr[ls(p)]+=tag[p]*(mid-l+1),tr[rs(p)]+=tag[p]*(r-mid);
	tag[p]=0;
    }
    inline void update(int p){tr[p]=tr[ls(p)]+tr[rs(p)];}
    inline void Modify(int p,int l,int r,int L,int R,int v){
	if(L<=l&&r<=R){tr[p]+=1ll*(r-l+1)*v;tag[p]+=v;return;}
	pushdown(p,l,r);int mid=(l+r)>>1;
	if(L<=mid)Modify(ls(p),l,mid,L,R,v);
	if(R>mid)Modify(rs(p),mid+1,r,L,R,v);
	update(p);
    }
    inline ll Query(int p,int l,int r,int L,int R){
	if(L<=l&&r<=R)return tr[p];
	pushdown(p,l,r);int mid=(l+r)>>1;ll ans=0;
	if(L<=mid)ans+=Query(ls(p),l,mid,L,R);
	if(R>mid)ans+=Query(rs(p),mid+1,r,L,R);
	return ans;
    }
}using namespace Segment_Tree;

int main(){
    n=read(),m=read();p1=read(),p2=read();
    for(int i=1;i<=n;++i)a[i]=read();
    a[n+1]=n+1;
    for(int i=1;i<=n+1;++i){
	while(tp&&a[stk[tp]]<=a[i])R[stk[tp]]=i,--tp;
	L[i]=stk[tp];stk[++tp]=i;
    }
    for(int i=1;i<=n;++i){
	if(1<=L[i]&&R[i]<=n)op[++tot]=(s_node){L[i],L[i],R[i],p1,0};
	if(L[i]<i-1&&R[i]<=n)op[++tot]=(s_node){L[i]+1,i-1,R[i],p2,0};
	if(R[i]>i+1&&L[i]>=1)op[++tot]=(s_node){i+1,R[i]-1,L[i],p2,0};
    }
    sort(op+1,op+tot+1);
    for(int i=1;i<=m;++i){
	int l=read(),r=read();
	ans[i]+=1ll*(r-l)*p1;//(l,r)==empty;
	q[i]=(s_node){l,r,l-1,-1,i};
	q[m+i]=(s_node){l,r,r,1,i};
    }
    sort(q+1,q+m+m+1);
    int o_t=1,q_t=1;while(!q[q_t].p&&q_t<=m+m)++q_t;
    for(int i=1;i<=n;++i){
	while(op[o_t].p<=i&&o_t<=tot)Modify(1,1,n,op[o_t].l,op[o_t].r,op[o_t].v),++o_t;
	while(q[q_t].p<=i&&q_t<=m+m)ans[q[q_t].id]+=Query(1,1,n,q[q_t].l,q[q_t].r)*q[q_t].v,++q_t;
    }
    for(int i=1;i<=m;++i)printf("%lld\n",ans[i]);
    return 0;
}

礼物

Solution

给其中一个手环增加亮度,相当于给第一个手环增加c的亮度,c可以是负数。

\[\begin{align} \sum\limits_{i=1}^n(x_i+c-y_i)^2&=\sum\limits_{i=1}^nx_i^2+y_i^2+c^2+2c(x_i-y_i)-2x_iy_i\\ &=n\cdot c^2+2c\cdot(\sum\limits_{i=1}^n x-\sum\limits_{i=1}^n y)+\sum\limits_{i=1}^n (x_i^2+y_i^2)-2\cdot \sum\limits_{i=1}^nx_iy_i \end{align} \]

前三项是关于c的一元二次函数,\(\text O(1)\)求最值。

最后一项是变量。我们要式子值最小,那么 \(\sum\limits_{i=1}^nx_iy_i\) 要最大。

\(y\) 翻转一下,那式子就是\(\sum\limits_{i=1}^nx_iy'_{n-i+1}\)。可以发现就是一个普通的卷积。答案在\(n+1\)出取到。

因为题目要求旋转手环过程中的最大值。所以把\(y'\)倍长,答案就是\(n+1,n+2\dots 2n-1\)中 的最大值。

现在来看这道题还是很套路的,像把\(y\)翻转化成卷积,在成七也才刚考过一次。我还是太cai了,想半天才想到。希望这次印象能深刻一点。

然后平移的话要想到倍长之后在后面取这个套路。

Code

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
inline int read(){//be careful for long long!
    register int x=0,f=1;register char ch=getchar();
    while(!isdigit(ch)){if(ch=='-')f=0;ch=getchar();}
    while(isdigit(ch)){x=x*10+(ch^'0');ch=getchar();}
    return f?x:-x;
}

const int N=2.7e5+10,mod=998244353;
int n,m,a[N],b[N],trans[N];
int f[N],g[N],sum;ll ans;
inline int power(int base,int n){int ans=1;for(;n;n>>=1,base=1ll*base*base%mod)if(n&1)ans=1ll*ans*base%mod;return ans;}
inline int F(int x){return m*x*x-2*x*sum;}

inline void NTT(int *a,int n,int type){
    static unsigned long long f[N];
    for(int i=0;i<n;++i)f[i]=a[i];
    for(int i=0;i<n;++i)if(i<trans[i])swap(f[i],f[trans[i]]);
    for(int len=2;len<=n;len<<=1){
	int e=power(3,(mod-1)/len),d=len>>1;if(type)e=power(e,mod-2);
	for(int p=0;p<n;p+=len)
	    for(int i=p,nw=1;i<p+d;++i,nw=1ll*nw*e%mod){
		unsigned long long t=f[i+d]*nw%mod;
		f[i+d]=f[i]+mod-t;f[i]+=t;
	    }
    }
    for(int i=0;i<n;++i)a[i]=f[i]%mod;
}
    
int main(){
//    freopen("in.in","r",stdin);
    m=read(),n=read();
    for(int i=0;i<m;++i)a[i]=read();
    for(int i=0;i<m;++i)b[i]=read();
    for(int i=0;i<m;++i)f[i]=a[i],g[m+m-i-1]=g[m-i-1]=b[i],sum+=a[i]-b[i],ans+=a[i]*a[i]+b[i]*b[i];
    ans+=min(F(floor((double)sum/m)),F(ceil((double)sum/m)));
    for(n=1;n<m+m+m;n<<=1);
    for(int i=0;i<n;++i)trans[i]=(trans[i>>1]>>1)|(i&1?(n>>1):0);
    NTT(f,n,0),NTT(g,n,0);
    for(int i=0;i<n;++i)f[i]=1ll*f[i]*g[i]%mod;
    NTT(f,n,1);int inv=power(n,mod-2);
    for(int i=0;i<n;++i)f[i]=1ll*f[i]*inv%mod;
    int res=0;
    for(int i=m-1;i<=2*m-2;++i)res=max(res,f[i]);
    printf("%lld\n",ans-res*2);
    return 0;
}

大佬

Solution

首先发现我能做的操作中只有一个是可以回复自己的自信值的,其他操作都和自信值无关。所以维持自己的自信值和攻击大佬可以分开考虑。

首先考虑维持自己的自信值非负。可以设\(f_{i,j}\)表示前\(i\)天,维持血量为\(j\)之余剩下的天数。转移很简单。然后对所有的\(f_{i,j}\)\(max\),就得到了最大的可操作天数。

ps:刚开始想的是对\(f_{n,j}\)\(max\),但是其实并不是这么回事。一个\(f_{i,j}\),它代表的是整局游戏只进行\(i\)天,除了维持生命值以外剩下的\(f_{i,j}\)天就可以用来攻击大佬。游戏并非一定要进行\(n\)天。可能不进行到\(n\)天,所能提供的操作天数还更多。

然后考虑攻击大佬。我们发现实际造成伤害的只有怼大佬和平a(还嘴)。所以只需考虑用几次怼(不超过两次)和什么时候怼。只和天数、嘲讽值有关。

可以先把所有的天数、嘲讽值的方案算出来。可以用\(bfs+hash表去重\)。复杂度与状态数有关。因为嘲讽值增长很快,题目又要求大佬生命值不能爆负,所以状态数不会太多。

然后把所有方案按照嘲讽值排序。

不怼和只怼一次很好算,接下来考虑怼两次的。设两次怼大佬(花费天数,嘲讽值)分别是\((d1,f1),(d2,f2)\)。总共可以怼\(X\)天。

那么有:

\[f1+f2\leq C\\ f1+f2+(X-d1-d2)\geq C \]

双指针扫一下就可以了。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
inline int read(){//be careful for long long!
    register int x=0,f=1;register char ch=getchar();
    while(!isdigit(ch)){if(ch=='-')f=0;ch=getchar();}
    while(isdigit(ch)){x=x*10+(ch^'0');ch=getchar();}
    return f?x:-x;
}

const int N=1e2+10,_=1e6+10;
int n,m,mc,a[N],w[N],f[N][N],c[N],mxc;

inline void Cal_day(){
    memset(f,-1,sizeof(f));f[0][mc]=0;
    for(int i=0;i<n;++i)
	for(int j=a[i+1];j<=mc;++j)
	    if(~f[i][j]){
		f[i+1][j-a[i+1]]=max(f[i+1][j-a[i+1]],f[i][j]+1);
		f[i+1][min(j-a[i+1]+w[i+1],mc)]=max(f[i+1][min(j-a[i+1]+w[i+1],mc)],f[i][j]);
	    }
    int mx=0;
    for(int i=1;i<=n;++i)for(int j=0;j<=mc;++j)mx=max(mx,f[i][j]);
    n=mx;
}

struct Hash{
    int mod=1000003,base=131;
    int head[1000003],tot;
    struct line{int x,y,next;}a[_];
    inline void Insert(int x,int y){
	int name=(1ll*x*base+y)%mod;
	a[++tot]=(line){x,y,head[name]};head[name]=tot;
    }
    inline bool Find(int x,int y){
	int name=(1ll*x*base+y)%mod;
	for(int i=head[name];i;i=a[i].next)if(a[i].x==x&&a[i].y==y)return 1;
	return 0;
    }
}H;

struct node{int d,f,l;};
PII stk[_];int tp;

inline void Bfs(){
    queue<node> q;
    q.push((node){1,1,0});
    while(q.size()){
	node nw=q.front();q.pop();
	if(nw.d==n)continue;
	q.push((node){nw.d+1,nw.f,nw.l+1});
	if(nw.l<=1)continue;//直接还嘴
	if(1ll*nw.f*nw.l>1ll*mxc)continue;//julao血量爆负
	if(H.Find(nw.f*nw.l,nw.d+1))continue;//去重剪枝
	H.Insert(nw.f*nw.l,nw.d+1);
	stk[++tp]=make_pair(nw.f*nw.l,nw.d+1);
	q.push((node){nw.d+1,nw.f*nw.l,nw.l});
    }
}

int main(){
    n=read(),m=read(),mc=read();
    for(int i=1;i<=n;++i)a[i]=read();
    for(int i=1;i<=n;++i)w[i]=read();
    for(int i=1;i<=m;++i)c[i]=read(),mxc=max(mxc,c[i]);
    Cal_day();
    Bfs();
    sort(stk+1,stk+tp+1);
    #define w first
    #define d second
    for(int t=1;t<=m;++t){
	if(c[t]<=n){puts("1");continue;}//不需要怼
	int flg=0;
	for(int i=tp,j=1,mn=0x3f3f3f3f;i;--i){//双指针
	    while(stk[i].w+stk[j].w<=c[t]&&j<=tp)mn=min(mn,stk[j].d-stk[j].w),++j;
	    if(stk[i].w<=c[t]&&stk[i].w+n-stk[i].d>=c[t]){flg=1;break;}//怼一次
	    if(stk[i].w-stk[i].d>=c[t]+mn-n){flg=1;break;}//Twice
	}
	printf("%d\n",flg);
    }
    #undef w
    #undef d
    return 0;
}
posted @ 2019-12-24 19:30  Fruitea  阅读(204)  评论(0编辑  收藏  举报