2021牛客暑期多校训练营7
比赛链接:https://ac.nowcoder.com/acm/contest/11258
F,H,I,12。讨论了半天K,还是不会做……
B
分析:
区间操作题,要想办法在\( logn \)级别的时间内做完每个操作。
如果把一个最长不减子序列分成两段,仍然可以算贡献,而且两段内部的贡献不受影响,只要考虑断点的改变;基于这种可拆分的性质,我们想到用线段树。
线段树维护的东西是要用来计算两段“缝合”起来时改变的贡献的。可以看到这和前一段的最大值、最大值所在位置的\( b \)值有关。所以线段树要维护区间最大值和区间最大值所在位置的\( b \)值。
接着,考虑查询。为了保证复杂度,需要在每个区间存下整个区间的答案;但是答案不仅和区间内部的值有关,还要考虑前一个接过来的元素的大小。换句话说,即使查询到了这个区间,取的也不一定是整个区间的答案,而是根据前面的数,从某个位置开始的子序列的答案。
为了化解这一点,同时又快速查询,我们可以只记录这种情况:左区间取了最大值后右区间的答案。由于已经记录了区间最大值和对应的\( b \),所以可以知道右区间的答案应该从哪个值开始。把右区间这个答案记成\( cal[u] \)。
这样,每次线段树上\( pushup \)的时候就需要对右区间重新进行查询(因为起始值改变了);进行修改操作的复杂度就成了\( O(log^2n) \);
而查询操作由于只有右区间能直接利用,所以左区间会一直查到单个位置,故查询复杂度(大约……)也是\( O(log^2n) \)。
查询时务必注意前一段对后一段的影响!为此还要用结构体返回查询值。
代码如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include<iostream> #define ls (u<<1) #define rs ((u<<1)|1) #define mid ((l+r)>>1) using namespace std; int const N=2e5+5; int n,a[N],b[N],q,mx[N<<2],pa[N<<2],cal[N<<2],lz[N<<2]; struct Nd{ int ans,x,p; }; int rd() { int ret=0,f=1; char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1; ch=getchar();} while(ch>='0'&&ch<='9')ret=(ret<<3)+(ret<<1)+ch-'0',ch=getchar(); return ret*f; } void pdn(int u) { if(!lz[u])return; lz[u]=0; pa[ls]^=1; lz[ls]^=1; pa[rs]^=1; lz[rs]^=1; } int findb(int u,int l,int r,int p) { if(l==r)return pa[u]; pdn(u); if(p<=mid)return findb(ls,l,mid,p); else return findb(rs,mid+1,r,p); } Nd qry(int u,int l,int r,int ql,int qr,int x,int p)//ql~qr内,前一个数为x,其位置的b为p,取出最长不降子序列 { if(ql>qr||x>mx[u])return (Nd){0,x,p}; Nd ans; //printf("qry(l=%d r=%d x=%d p=%d ql=%d qr=%d)\n",l,r,x,p,ql,qr); if(l==r){ans=(mx[u]>=x)?(Nd){(pa[u]!=p),mx[u],pa[u]}:(Nd){0,x,p};///! //printf("0ql=%d qr=%d l=%d r=%d x=%d p=%d = %d,%d,%d\n",ql,qr,l,r,x,p,ans.ans,ans.x,ans.p); return ans; } pdn(u); if(l>=ql&&r<=qr) { if(x<=mx[ls]){ans=qry(ls,l,mid,ql,qr,x,p); //ans.ans+=cal[u]; ans.x=mx[rs]; ans.p=pa[rs]; if(mx[rs]>=ans.x)ans.ans+=cal[u],ans.x=mx[rs],ans.p=pa[rs];///! //printf("1ql=%d qr=%d l=%d r=%d x=%d p=%d = %d,%d,%d\n",ql,qr,l,r,x,p,ans.ans,ans.x,ans.p); return ans;} else {ans=qry(rs,mid+1,r,ql,qr,x,p); //printf("2ql=%d qr=%d l=%d r=%d x=%d p=%d = %d,%d,%d\n",ql,qr,l,r,x,p,ans.ans,ans.x,ans.p); return ans;} } if(mid>=ql) { if(mid>=qr){ans=qry(ls,l,mid,ql,qr,x,p); //printf("3ql=%d qr=%d l=%d r=%d x=%d p=%d = %d,%d,%d\n",ql,qr,l,r,x,p,ans.ans,ans.x,ans.p); return ans;} else { ans=qry(ls,l,mid,ql,qr,x,p); Nd a2=qry(rs,mid+1,r,ql,qr,ans.x,ans.p); //printf("4ql=%d qr=%d l=%d r=%d x=%d p=%d = %d,ansx=%d,ansp=%d,ansa=%d,a2x=%d,a2p=%d,a2a=%d\n",ql,qr,l,r,x,p,ans.ans+a2.ans,ans.x,ans.p,ans.ans,a2.x,a2.p,a2.ans); return (Nd){ans.ans+a2.ans,a2.x,a2.p}; } } else {ans=qry(rs,mid+1,r,ql,qr,x,p); //printf("5ql=%d qr=%d l=%d r=%d x=%d p=%d = %d,%d,%d\n",ql,qr,l,r,x,p,ans.ans,ans.x,ans.p); return ans;} } void upt(int u,int l,int r) { if(mx[ls]>mx[rs])mx[u]=mx[ls],pa[u]=pa[ls],cal[u]=0; else mx[u]=mx[rs],pa[u]=pa[rs],cal[u]=qry(rs,mid+1,r,mid+1,r,mx[ls],pa[ls]).ans; } void build(int u,int l,int r) { if(l==r){mx[u]=a[l]; pa[u]=b[l]; return;} build(ls,l,mid); build(rs,mid+1,r); upt(u,l,r); //printf("l=%d r=%d mid=%d cal=%d\n",l,r,mid,cal[u]); } void change1(int u,int l,int r,int ps,int x)//更新mx { if(l==r){mx[u]=x; return;} pdn(u); if(ps<=mid)change1(ls,l,mid,ps,x); else change1(rs,mid+1,r,ps,x); upt(u,l,r); } void change2(int u,int l,int r,int ql,int qr)//更新b { if(l>=ql&&r<=qr){pa[u]^=1; lz[u]^=1; return;} pdn(u); if(mid>=ql)change2(ls,l,mid,ql,qr); if(mid<qr)change2(rs,mid+1,r,ql,qr); upt(u,l,r); } int main() { n=rd(); for(int i=1;i<=n;i++)a[i]=rd(); for(int i=1;i<=n;i++)b[i]=rd(); build(1,1,n); q=rd(); for(int i=1,tp,t1,t2;i<=q;i++) { tp=rd(); t1=rd(); t2=rd(); if(tp==1)a[t1]=t2,change1(1,1,n,t1,t2); if(tp==2)change2(1,1,n,t1,t2); if(tp==3)printf("%d\n",qry(1,1,n,t1+1,t2,a[t1],findb(1,1,n,t1)).ans); } return 0; }
F
分析:
第一棵树(下称树1)中选取的链一定是连续的,容易想到dfs;因为dfs到的当前点的祖先链都已经dfs过了。
所以我们想利用]\( fa[u] \)的答案来计算\( u \)的答案。我们可以考虑每个点向上最长能延伸多少个点。
在树2上的祖先关系可以转化成dfs序上的区间覆盖问题。那么,在对树1dfs的过程中,我们希望记录下当前点祖先在树2上的覆盖信息,以此计算答案。因为dfs有撤销的过程,所以我们可以用主席树来实现:
对于树1上dfs到的点\(u\),继承\(fa[u]\)的主席树,然后在\(u\)的dfs序子树区间上覆盖\(dep[u]\)这个值;
查询\(fa[u]\)的主席树上\(u\)子树区间内的最大值,这个值表示会与\(u\)产生冲突的最深的祖先点;所以\(ans[u]=dep[u]-query(fa[u], l[u], r[u]) \);
但是还要考虑到\(u\)的祖先点彼此之间的冲突;由于\(u\)向上延伸的链和\(fa[u]\)向上延伸的链是重合的,所以\(ans[u]\)只需要再对\(ans[fa[u]]\)取个\(min\)值即可。
注意主席树上\(lazy\)标记的用法……挺妙的。
时间复杂度\(O(nlogn)\)。
代码如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include<iostream> #include<cstring> #define mid ((l+r)>>1) using namespace std; int const N=3e5+5; int T,n,hd1[N],cnt1,nxt1[N<<1],to1[N<<1],hd2[N],cnt2,nxt2[N<<1],to2[N<<1]; int tim,dfn[N],siz[N],dep[N],ans[N]; int rt[N],tcnt,ls[N<<5],rs[N<<5],mx[N<<5],lz[N<<5]; int rd() { int ret=0,f=1; char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1; ch=getchar();} while(ch>='0'&&ch<='9')ret=(ret<<3)+(ret<<1)+ch-'0',ch=getchar(); return ret*f; } void add1(int x,int y){nxt1[++cnt1]=hd1[x]; hd1[x]=cnt1; to1[cnt1]=y;} void add2(int x,int y){nxt2[++cnt2]=hd2[x]; hd2[x]=cnt2; to2[cnt2]=y;} void init() { cnt1=0; cnt2=0; memset(hd1,0,sizeof hd1); memset(hd2,0,sizeof hd2); tim=0; memset(ans,0,sizeof ans); tcnt=0; memset(rt,0,sizeof rt); memset(mx,0,sizeof mx); } void dfs2(int u,int fa) { dfn[u]=++tim; siz[u]=1; for(int i=hd2[u],v;i;i=nxt2[i]) if((v=to2[i])!=fa)dfs2(v,u),siz[u]+=siz[v]; } int build(int l,int r) { int ret=++tcnt; mx[ret]=0; lz[ret]=0; if(l>=r)return ret; ls[ret]=build(l,mid); rs[ret]=build(mid+1,r); return ret; } int qry(int u,int l,int r,int ql,int qr) { if(l>=ql&&r<=qr)return mx[u]; int ret=lz[u];// if(mid>=ql)ret=max(ret,qry(ls[u],l,mid,ql,qr)); if(mid<qr)ret=max(ret,qry(rs[u],mid+1,r,ql,qr)); return ret; } int update(int u,int l,int r,int ql,int qr,int x) { int ret=++tcnt; mx[ret]=max(mx[u],x); ls[ret]=ls[u]; rs[ret]=rs[u]; lz[ret]=0;// if(l>=ql&&r<=qr){lz[ret]=x; return ret;}// if(mid>=ql)ls[ret]=update(ls[u],l,mid,ql,qr,x); if(mid<qr)rs[ret]=update(rs[u],mid+1,r,ql,qr,x); return ret; } void dfs1(int u,int fa) { dep[u]=dep[fa]+1; if(fa)ans[u]=min(ans[fa]+1,dep[u]-qry(rt[fa],1,n,dfn[u],dfn[u]+siz[u]-1)); else ans[u]=1; //printf("ans[%d]=%d\n",u,ans[u]); rt[u]=update(rt[fa],1,n,dfn[u],dfn[u]+siz[u]-1,dep[u]); for(int i=hd1[u],v;i;i=nxt1[i]) if((v=to1[i])!=fa)dfs1(v,u); } int main() { T=rd(); while(T--) { n=rd(); init(); for(int i=1,u,v;i<n;i++) u=rd(),v=rd(),add1(u,v),add1(v,u); for(int i=1,u,v;i<n;i++) u=rd(),v=rd(),add2(u,v),add2(v,u); dfs2(1,0); rt[0]=build(1,n); dfs1(1,0); int anss=0; for(int i=1;i<=n;i++)anss=max(anss,ans[i]); printf("%d\n",anss); } return 0; }
H
分析:
签到题。用桶记录出现的数,直接枚举因子计数。复杂度\( O(nlogn) \)。
I
分析:
签到题。按位考虑。
J
分析:
题解第一种做法,有点不好想到,但复杂度靠谱。
还看到一种比较直接的做法,就是对错误Floyd各种剪枝。核心思想是对于转移方程\( dis[i][j] = min(dis[i][j], dis[i][k]+dis[k][j]) \),只遍历有可能使\( dis[i][j] \)正确的那些\( k \)。
错误Floyd的转移有很整齐的顺序,对于\(k\)我们可以分成小于\(j\)的和大于\(j\)的。小于\(j\)的\(k\)在前面已经转移得到了\( dis[i][k] \),而大于\(j\)的\(k\)除了直接与\(i\)连边的,其他都是\(dis[i][k]=inf\)。
所以对于大于\(j\)的\(k\),\( dis[i][k] = inf \)的\(k\)就不遍历了,也就是只找和\(i\)直接连边的点\(k\);
对于小于\(j\)的\(k\),只考虑有可能使接下来的\(dis[i][j]\)答案正确的。也就是如果\(dis[i][j]\)答案正确,就把\(j\)加到\(i\)要遍历的点集里。
这个点集初始只有\(i\)直接相连的点。每次枚举到一个\(j\),遍历这个点集中的点来更新答案。
但是这样还是会TLE。再加点剪枝,比如正确答案就是\(inf\)的就不更新了,还有已经是正确答案的也不更新了。
时间复杂度??,呃反正是能过,还不慢呢。(毕竟各种做法都是加速错误Floyd这个过程)
代码如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include<iostream> #include<queue> #include<cstring> #include<vector> #define pb push_back using namespace std; int const N=2005,M=5005; int n,m,d[N][N],dis[N][N],hd[N],cnt,nxt[M],to[M],w[M],inf; bool vis[N]; struct Nd{ int d,id; bool operator < (const Nd &a) const {return d>a.d;} }; priority_queue<Nd>q; vector<int>ed[N]; void add(int x,int y,int t){nxt[++cnt]=hd[x]; hd[x]=cnt; to[cnt]=y; w[cnt]=t;} void dijk(int st) { memset(vis,0,sizeof vis); q.push((Nd){0,st}); while(q.size()) { int nd=q.top().d,u=q.top().id; q.pop(); if(vis[u])continue; vis[u]=1; for(int i=hd[u],v;i;i=nxt[i]) { if(vis[v=to[i]])continue; if(d[st][v]>nd+w[i]) d[st][v]=nd+w[i],q.push((Nd){d[st][v],v}); } } } int main() { scanf("%d%d",&n,&m); memset(d,0x3f3f3f3f,sizeof d); memset(dis,0x3f3f3f3f,sizeof dis); inf=d[0][0]; for(int i=1;i<=n;i++)d[i][i]=0,dis[i][i]=0; for(int i=1,u,v,t;i<=m;i++) { scanf("%d%d%d",&u,&v,&t); add(u,v,t); dis[u][v]=t; ed[u].pb(v); } for(int i=1;i<=n;i++)dijk(i); for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) { if(d[i][j]==dis[i][j]||d[i][j]==inf)continue; for(int k:ed[i]) dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]); if(d[i][j]==dis[i][j])ed[i].pb(j); } int ans=0; for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) ans+=(d[i][j]==dis[i][j]);//,printf("d[%d][%d]=%d dis[%d][%d]=%d\n",i,j,d[i][j],i,j,dis[i][j]); printf("%d\n",ans); return 0; }
K
分析:
比赛时想了半天,也想过贪心之类的,较小的数减成0、较大的数加成\( k \)这种。但是区间内的元素彼此牵连,总是很乱不能做。
看了题解才发现应该差分,这样区间加减就变成单点加减了;区间归零也可以转换成差分归零。一下子清晰好多。
先考虑只有一次询问而且没有\( k \)的情况要怎么做:在差分数组\( b_i \)上,一次操作会把某个位置\( +1 \),另一个位置\( -1 \);而差分数组的总和等于\( 0 \)(在原数组前后插两个\( 0 \),差分数组一共\( n+1 \)个元素)。所以最小次数就是\( \sum_{i=1}^{i=n+1} |b_i| / 2 \)。
现在考虑引入\( k \):每个差分既可以变成\( 0 \),也可以变成\( \pm k \)。为了方便考虑,这时我们可以把负数\( x \)变为正数\( k+x \),这样所有数都在\( [0,k) \)这个区间里。可以想到贪心的做法就是让较小的数变成\( 0 \),较大的数变成\( k \);我们可以把整个数组排序,然后二分一下在哪里分界。当把较小数变成\( 0 \)的操作次数\( pref \)与把较大数变成\( k \)的操作次数\( suf \)最接近时,答案最优,二分结束。
现在,回到我们的问题:有多次询问,每次询问有一个区间和一个\( k \)。这要求我们在\( logn \)级别的时间内把一个区间的差分取出来,负数变成正数(这个过程和\( k \)有关,不能预处理),然后排序,再二分。为了快速取区间并排序,可以想到对差分序列建立权值主席树(主席树额外的好处是动态建点,不需要离散化)。
但是,负数变正数与\( k \)有关,还是不能做。我们可以干脆分开正负考虑;对于负数\( x \),我们原本把它变成\( k+x \),然后与二分到的值\( mid \)作比较;现在我们保留\( x \),让它与\( mid-k \)作比较。这样就在二分的过程中解决负数的问题了。
还要注意值恰好是二分的\( mid \)的那些数;它们可以一部分取到\( 0 \),一部分取到\( \pm k \),所以这里还需要再二分,求最优的答案。
时间复杂度\( O(nlogk+qlog^2k) \)。
写完不知为何TLE了,感觉不是时间复杂度的问题,应该是某些递归边界没写好。后来把主席树查询函数改了一下,当前区间没有值了就直接返回,就过了。仔细想想,不判断这个的话会把范围内的所有数不停地找下去,确实会TLE,和我一开始要在\( 0 \)号点build一个主席树犯了一样的错误。笑。
代码如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include<iostream> #define ll long long #define mid ((l+r)>>1) using namespace std; int const N=2e5+5; ll const inf=1e17; int n,q,a[N],b[N],up,dn; int cnt,rt[2][N],ls[2][N<<5],rs[2][N<<5],num[2][N<<5]; ll sum[2][N<<5]; struct Nd{ ll s; int num; Nd operator + (const Nd &a) const {return (Nd){s+a.s,num+a.num};} }; int rd() { int ret=0,f=1; char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1; ch=getchar();} while(ch>='0'&&ch<='9')ret=(ret<<3)+(ret<<1)+ch-'0',ch=getchar(); return ret*f; } /* int build(int tp,int l,int r) { int ret=++cnt; if(l>=r)return ret; ls[tp][ret]=build(tp,l,mid); rs[tp][ret]=build(tp,mid+1,r); return ret; } */ int update(int tp,int pre,int l,int r,int x) { int ret=++cnt; ls[tp][ret]=ls[tp][pre]; rs[tp][ret]=rs[tp][pre]; sum[tp][ret]=sum[tp][pre]+x; num[tp][ret]=num[tp][pre]+1; if(l==r)return ret; if(x<=mid)ls[tp][ret]=update(tp,ls[tp][pre],l,mid,x); else rs[tp][ret]=update(tp,rs[tp][pre],mid+1,r,x); return ret; } void Build() { //rt[0][0]=build(0,0,up); rt[1][0]=build(1,1,dn); for(int i=1;i<=n;i++)// { if(b[i]>=0) { rt[0][i]=update(0,rt[0][i-1],0,up,b[i]); rt[1][i]=rt[1][i-1]; } else { rt[0][i]=rt[0][i-1]; rt[1][i]=update(1,rt[1][i-1],1,dn,-b[i]); } } } Nd qry(int tp,int pre,int nw,int l,int r,int ql,int qr) { if(ql>qr||l>r||num[tp][nw]-num[tp][pre]==0)return (Nd){0,0};//! //if(ql>qr||l>r||l>qr||r<ql)return (Nd){0,0};//TLE if(l>=ql&&r<=qr)return (Nd){sum[tp][nw]-sum[tp][pre],num[tp][nw]-num[tp][pre]}; Nd ret=(Nd){0,0}; if(mid>=ql)ret=ret+qry(tp,ls[tp][pre],ls[tp][nw],l,mid,ql,qr); if(mid<qr)ret=ret+qry(tp,rs[tp][pre],rs[tp][nw],mid+1,r,ql,qr); return ret; } int main() { n=rd(); q=rd(); for(int i=1;i<=n;i++) { a[i]=rd(); b[i]=a[i]-a[i-1]; if(b[i]>up)up=b[i]; if(b[i]<dn)dn=b[i]; } //b[n+1]=0-a[n]; //if(b[n+1]>up)up=b[n+1]; if(b[n+1]<dn)dn=b[n+1]; dn=-dn; Build(); for(int i=1,L,R,k;i<=q;i++) { L=rd(); R=rd(); k=rd(); Nd qup1,qup2,qdn1,qdn2; int l=0,r=k; ll ans=inf; while(l<=r) { //printf("l=%d r=%d mid=%d\n",l,r,mid); qup1=qry(0,rt[0][L],rt[0][R],0,up,0,mid-1); qup2=qry(0,rt[0][L],rt[0][R],0,up,mid+1,up); qdn1=qry(1,rt[1][L],rt[1][R],1,dn,1,k-mid-1); qdn2=qry(1,rt[1][L],rt[1][R],1,dn,k-mid+1,dn); int midc=(R-L)-qup1.num-qup2.num-qdn1.num-qdn2.num; if(a[L]<mid)qup1.s+=a[L],qup1.num++; else if(a[L]>mid)qup2.s+=a[L],qup2.num++; else midc++; /* printf("midc=%d\n",midc); printf("up1=%lld cnt1=%d up2=%lld cnt2=%d\n",qup1.s,qup1.num,qup2.s,qup2.num); printf("dn1=%lld cnt1=%d dn2=%lld cnt2=%d\n",qdn1.s,qdn1.num,qdn2.s,qdn2.num);*/ ll pref=qup1.s+((ll)k*qdn2.num-qdn2.s); ll suf=((ll)k*qup2.num-qup2.s)+qdn1.s; if(!midc)ans=min(ans,max(pref,suf)); else { int cl=0,cr=midc; while(cl<=cr) { int Mid=((cl+cr)>>1); ll cpref=pref+(ll)Mid*mid, csuf=suf+(ll)(midc-Mid)*(k-mid); ans=min(ans,max(cpref,csuf)); if(cpref<csuf)cl=Mid+1; else cr=Mid-1; } } if(pref<suf)l=mid+1; else r=mid-1; } printf("%lld\n",ans); } return 0; }