Competition Set - 模拟赛 I
HNOI2017 Day2
2023-06-10
注:Day2T2换为BJOI2017Day2T1,以匹配学习进度
A 大佬
你需要用 天挑战一名大佬。大佬的自信值为 。你的目标是在 天内使大佬的自信值恰好为 。
用 来表示你的自信值上限。在第 天,大佬会对你发动一次嘲讽,使你的自信值减小 ,如果这个时刻你的自信值小于 了,那么你就失败了。否则,你能且仅能选择如下的行为之一:
- 还一句嘴,大佬的自信值 减小 。
- 做一天的水题,使得自己的当前自信值增加 ,如果超过 就变为
- 让自己的等级值 加 。
- 让自己的讽刺能力 乘以自己当前等级 ,使讽刺能力 更新为 。
- 怼大佬,让大佬的自信值 减小 。并在怼完大佬之后,你自己的等级 自动降为 ,讽刺能力 降为 。这个操作只能做不超过两次。
注意,在任何时候,不能大佬的自信值为负。在第 天被攻击之前,你的自信值等于 ,讽刺能力 是 ,等级 是 。
一共有 个大佬,他们的嘲讽时间都是 天,而且第 天的嘲讽值都是 。不管和哪个大佬较量,你在第 天做水题的自信回涨都是 。对每个大佬,问能否战胜。
tag:DP,枚举
首先,注意到提升自己的自信值与降低大佬的自信值相互独立,所以可以考虑先求出最多能够花多少天来攻击大佬。这只需要做一个简单的DP,状态为天数和自信值。
然后,考虑操作“怼大佬”。预处理出怼大佬所有可能的“伤害”,这样的数不超过 ,所有素因子不超过 ,数目并不太多,约为 。可以直接dfs。
接下来,求出每个“伤害”所需要的步数,这也可以DP。将一个数拆成若干个数的积,枚举最大值,不断更新拼出某个数的最少因数个数即可。
对于每个询问,先用不怼大佬和怼一次大佬更新答案。怼两次大佬的情况,先枚举第一次怼的伤害,然后在大佬的剩余自信值附近(距离不超过100)枚举第二次伤害,更新最小回合数。比较最小回合数与可用回合数即得答案。
难度Easy+,成功场切。
点击查看代码
#include<bits/stdc++.h> using namespace std; int n,m,mc,a[105],w[105],ans,dp[105][105],cnt; int p[30]={0,2,3,5,7,11,13,17,19,23,29,31,37,41,43,47 ,53,59,61,67,71,73,79,83,89,97,101}; vector<int> val; void dfs(int x,int f,int op){ if(op)val.push_back(f); if(p[x]>n)return; dfs(x+1,f,0); if(f<1e8/p[x])dfs(x,f*p[x],1); } int len,f[1000005],g[1000005]; void init(){ dfs(1,1,1); sort(val.begin(),val.end()); memset(f,0x3f,sizeof(f)); memset(g,0x3f,sizeof(g)); f[0]=g[0]=0;len=val.size(); for(int i=2;i<=n;++i) for(int j=0;j<len;++j)if(val[j]%i==0){ int p=lower_bound(val.begin(),val.end(),val[j]/i)-val.begin(); f[j]=min(f[j],f[p]+1);g[j]=min(g[j],f[j]+i); } for(int i=0;i<len;++i)g[i]=g[i]-val[i]+1; } void solve(){ memset(dp,-0x3f,sizeof(dp)); dp[0][mc]=0; for(int i=1;i<=n;i++) for(int j=a[i];j<=mc;j++){ dp[i][j-a[i]]=max(dp[i][j-a[i]],dp[i-1][j]+1); int c=min(j-a[i]+w[i],mc); dp[i][c]=max(dp[i][c],dp[i-1][j]); } for(int i=1;i<=n;i++) for(int j=0;j<=mc;j++) cnt=max(cnt,dp[i][j]); } int main(){ ios::sync_with_stdio(false); cin>>n>>m>>mc; for(int i=1;i<=n;i++)cin>>a[i]; for(int i=1;i<=n;i++)cin>>w[i]; init();solve(); for(int i=1,c;i<=m;i++){ cin>>c;ans=c; for(int j=0;j<len;j++)if(val[j]<=c){ ans=min(ans,g[j]+c); int x=lower_bound(val.begin(),val.end(),c-val[j]-100)-val.begin(), y=lower_bound(val.begin(),val.end(),c-val[j]+1)-val.begin(); for(int k=x;k<y;k++)ans=min(ans,g[j]+g[k]+c); } printf(ans<=cnt?"1\n":"0\n"); } return 0; }
B 抛硬币
一枚均匀的硬币,小A抛次,小B抛次,求小A抛出正面的次数大于小B的情况数。
。
tag:组合恒等式,exLucas
数学题,直接推式子,第三个等号用到了范德蒙恒等式。
用扩展lucas定理求后面的一堆组合数即可。
难度:Medium-,考场exLucas没有实现好,丢了20分。
点击查看代码
#include<bits/stdc++.h> using namespace std; typedef long long ll; ll a,b,ans,k,c2[1025],c5[1953126],r1,r2,x2,x5,d2,d5,pwr[15][15];string res; const ll m1=1024,m2=1953125,l1=787109376,l2=212890625,val=976563; inline void exgcd(ll a,ll b,ll&x,ll&y){ if(b==0){x=1;y=0;return;} exgcd(b,a%b,x,y);ll x0=x,y0=y; x=y0;y=x0-a/b*y0; } inline ll power(ll a,ll b,ll mod){ register ll c=1; for(;b;b>>=1){ if(b&1)c=c*a%mod; a=a*a%mod; } return c; } inline void f(ll n,ll p,ll k,ll&x,ll&y){ register ll m,c,cnt=0; if(p==2)m=m1,c=c2[m]; else m=m2,c=c5[m]; while(n){ register ll q=n/m,r=n-m*q;cnt+=q; x=x*(p==2?c2[r]:c5[r])%m; n/=p;y+=n; }x=x*power(c,cnt,m)%m; } inline ll C(ll n,ll m,ll p,ll k){ register ll x,y=1,z=1,t,d,e=0,h=0; if(p==2)x=x2,d=d2;else x=x5,d=d5; f(m,p,k,y,e);f(n-m,p,k,z,h);d-=e+h; if(d>=k)return 0;t=pwr[p][k-d]; register ll r,s,u,v;exgcd(y,t,r,s);exgcd(z,t,u,v); r=(r%t+t)%t;u=(u%t+t)%t; t=x*r%t*u%t;t=t*pwr[p][d]; return t; } int main(){ c2[0]=c5[0]=pwr[2][0]=pwr[5][0]=1; for(int i=1;i<=10;i++)pwr[2][i]=2*pwr[2][i-1],pwr[5][i]=5*pwr[5][i-1]; for(register int i=1;i<=1024;++i) if(i%2!=0)c2[i]=1ll*c2[i-1]*i%m1;else c2[i]=c2[i-1]; for(register int i=1;i<=1953125;++i) if(i%5!=0)c5[i]=1ll*c5[i-1]*i%m2;else c5[i]=c5[i-1]; ios::sync_with_stdio(false); while(cin>>a>>b>>k){ // double t1=clock()*1000/CLOCKS_PER_SEC; r1=power(2,a+b,m1);r2=power(2,a+b,m2); ans=0;res="";x2=x5=1;d2=d5=0; f(a+b,2,10,x2,d2);f(a+b,5,9,x5,d5); if(a==b){ r1=(r1-C(a*2,a,2,10)+m1)%m1; r2=(r2-C(a*2,a,5,9)+m2)%m2; } else if(a>b+1){ ll d=(a+b+1)/2; for(register ll m=b+1;m<=d;++m){ register ll x=C(a+b,m,2,10),y=C(a+b,m,5,9); r1=(r1+x)%m1;r2=(r2+y)%m2; if(a+b-m>d&&a+b-m<a)r1=(r1+x)%m1,r2=(r2+y)%m2; } } ans=r1/2*l2+r2*val%m2*l1; while(k--){res=char(ans%10+'0')+res;ans/=10;} cout<<res<<endl; // double t2=clock()*1000/CLOCKS_PER_SEC; // cout<<(t2-t1)<<"ms\n"; } return 0; }
C 喷式水战改
一堆玩意儿,每个玩意儿有三个状态0,1,2,每个状态有一个权值。维护这些玩意儿构成的序列,支持在某个位置插入若干个,以及询问:将序列分为4段,每段状态相同,依次为0,1,2,0,总权值和的最大值。
操作数 ,权值 ,每次插入数目 。
tag:Splay
如果每次只插入一个,那么可以很容易用Splay维护。每个结点维护8种分割的最大值,即目标分割的8个子串。
现在每次插入多个,只要在插入时(如果需要)拆点即可。
难度Medium,考场接近做出,因为Splay刚学印象深。但其实也挺板的?
点击查看代码
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int N=4e5+5; int n;ll ans,lstans; int rt,tot,fa[N],ch[N][2]; ll siz[N],cnt[N],val[N][3],sum[N][9]; void init(){rt=1;tot=2;cnt[1]=cnt[2]=1;siz[1]=2;siz[2]=1;fa[2]=1;ch[1][1]=2;} ll max(ll a,ll b,ll c){return max(max(a,b),c);} ll max(ll a,ll b,ll c,ll d){return max(max(a,b,c),d);} void push_up(int p){ int lc=ch[p][0],rc=ch[p][1]; siz[p]=siz[lc]+siz[rc]+cnt[p]; for(int op=0;op<3;op++) sum[p][op]=sum[lc][op]+val[p][op]*cnt[p]+sum[rc][op]; for(int op=3;op<6;op++) sum[p][op]=max(sum[lc][op]+val[p][(op+1)%3]*cnt[p]+sum[rc][(op+1)%3], sum[lc][op%3]+val[p][op%3]*cnt[p]+sum[rc][op]); sum[p][6]=max(sum[lc][6]+val[p][2]*cnt[p]+sum[rc][2], sum[lc][3]+val[p][1]*cnt[p]+sum[rc][4], sum[lc][0]+val[p][0]*cnt[p]+sum[rc][6]); sum[p][7]=max(sum[lc][7]+val[p][0]*cnt[p]+sum[rc][0], sum[lc][4]+val[p][2]*cnt[p]+sum[rc][5], sum[lc][1]+val[p][1]*cnt[p]+sum[rc][7]); sum[p][8]=max(sum[lc][8]+val[p][0]*cnt[p]+sum[rc][0], sum[lc][6]+val[p][2]*cnt[p]+sum[rc][5], sum[lc][3]+val[p][1]*cnt[p]+sum[rc][7], sum[lc][0]+val[p][0]*cnt[p]+sum[rc][8]); } bool get(int p){return p==ch[fa[p]][1];} void rotate(int x){ int y=fa[x],z=fa[y],op=get(x)^1; ch[y][op^1]=ch[x][op];if(ch[x][op])fa[ch[x][op]]=y; ch[x][op]=y;fa[y]=x;fa[x]=z;if(z)ch[z][y==ch[z][1]]=x; push_up(y);push_up(x); } void splay(int x,int goal=0){ for(int p=fa[x];p!=goal;p=fa[x]){ if(fa[p]!=goal)rotate(get(p)==get(x)?p:x); rotate(x); }if(!goal)rt=x; } int kth(ll&k){ int p=rt; while(1){ if(siz[ch[p][0]]>=k)p=ch[p][0]; else{k-=siz[ch[p][0]]+cnt[p];if(k<=0)return p;p=ch[p][1];} } } void ins(ll x,ll a,ll b,ll c,ll m){ ll k=x+1;int p=kth(k),q; if(k==0){ k=x+2;q=kth(k);splay(p);splay(q,p); ch[q][0]=++tot;fa[tot]=q;cnt[tot]=m; val[tot][0]=a;val[tot][1]=b;val[tot][2]=c; push_up(tot);push_up(q);push_up(p); } else{ ll l=x+1-k-cnt[p];q=kth(l);splay(q);splay(p,q); int y=ch[p][0]=++tot;fa[y]=p;cnt[y]=m; val[y][0]=a;val[y][1]=b;val[y][2]=c; ch[y][0]=++tot;fa[tot]=y;cnt[tot]=k+cnt[p];cnt[p]=-k; val[tot][0]=val[p][0];val[tot][1]=val[p][1];val[tot][2]=val[p][2]; push_up(tot);push_up(y);push_up(p);push_up(q); } } int main(){ scanf("%d",&n); init(); for(int i=1;i<=n;i++){ ll p,a,b,c,x; scanf("%lld%lld%lld%lld%lld",&p,&a,&b,&c,&x); ins(p,a,b,c,x); splay(1);splay(2,1);ans=sum[ch[2][0]][8]; printf("%lld\n",ans-lstans); lstans=ans; } return 0; }
HNOI2017 Day1
2023-06-09
A 单旋
考虑一棵单旋实现的splay,支持以下操作:插入;单旋最小值;单旋最大值;单旋并删除最小值;单旋并删除最大值。其中单旋表示将点转到根。每次操作的代价是操作的点的深度(插入后或单旋前)。次操作,求每次操作的代价。
,所有结点的关键码互不相同。
tag:Splay
事实上这个题肯定不能用Splay做,但分析这样一棵Splay的性质还是很关键的。
模拟几个操作可以发现,单旋最值其实只是把最值换到根,然后最值的儿子(唯一)代替它原来的位置。那么它对深度的影响就非常明确:对于最小值,设它的父亲权值是,那么单旋使的深度变为,的深度+1;删除使的深度-1。
我们可以把所有点权离散化,然后在线段树上修改。对于不在树上的点,可以一并修改,只要插入的时候用赋值方式即可。
至于插入操作,容易发现插入一个点,要么成为它前驱的右儿子,要么成为它后继的左儿子,且应选取二者中深度较大的一个。用一个set维护在树上的点权即可。
难度Medium,难点在于插入。
点击查看代码
#include<bits/stdc++.h> using namespace std; const int N=1e5+5; int n,m,q[N][2],x[N],rt,fa[N],ch[N][2]; struct SegmentTree{ int tag[N<<2]; #define mid ((l+r)>>1) void modify(int p,int l,int r,int L,int R,int v){ if(l>=L&&r<=R){tag[p]+=v;return;} if(L<=mid)modify(p<<1,l,mid,L,R,v); if(R>mid)modify(p<<1|1,mid+1,r,L,R,v); } int query(int p,int l,int r,int x){ if(l==r)return tag[p]; if(x<=mid)return query(p<<1,l,mid,x)+tag[p]; else return query(p<<1|1,mid+1,r,x)+tag[p]; } #undef mid }seg; set<int> S; int main(){ scanf("%d",&m); for(int i=1;i<=m;i++){ scanf("%d",&q[i][0]); if(q[i][0]==1){ scanf("%d",&q[i][1]); x[++n]=q[i][1]; } } sort(x+1,x+n+1); for(int i=1;i<=m;i++){ int op=q[i][0]; if(op==1){ int v=lower_bound(x+1,x+n+1,q[i][1])-x,res; if(S.empty()){rt=v;res=1;} else{ auto it=S.lower_bound(v); if(it==S.end()){ int u=*--it;ch[u][1]=v;fa[v]=u; res=seg.query(1,1,n,u)+1; } else if(it==S.begin()){ int u=*it;ch[u][0]=v;fa[v]=u; res=seg.query(1,1,n,u)+1; } else{ int u1=*it,u2=*--it; int du1=seg.query(1,1,n,u1), du2=seg.query(1,1,n,u2); if(du1>du2)ch[u1][0]=v,fa[v]=u1,res=du1+1; else ch[u2][1]=v,fa[v]=u2,res=du2+1; } } S.insert(v); int dv=seg.query(1,1,n,v); seg.modify(1,1,n,v,v,res-dv); printf("%d\n",res); } if(op==2){ int v=*S.begin(),res; printf("%d\n",res=seg.query(1,1,n,v)); seg.modify(1,1,n,v,v,1-res); if(fa[v]){ seg.modify(1,1,n,fa[v],n,1); ch[fa[v]][0]=ch[v][1];if(ch[v][1])fa[ch[v][1]]=fa[v]; ch[v][1]=rt;fa[rt]=v;fa[v]=0;rt=v; } } if(op==3){ int v=*--S.end(),res; printf("%d\n",res=seg.query(1,1,n,v)); seg.modify(1,1,n,v,v,1-res); if(fa[v]){ seg.modify(1,1,n,1,fa[v],1); ch[fa[v]][1]=ch[v][0];if(ch[v][0])fa[ch[v][0]]=fa[v]; ch[v][0]=rt;fa[rt]=v;fa[v]=0;rt=v; } } if(op==4){ int v=*S.begin(),res; printf("%d\n",res=seg.query(1,1,n,v)); seg.modify(1,1,n,v,!fa[v]?n:fa[v]-1,-1); if(fa[v]){ch[fa[v]][0]=ch[v][1];if(ch[v][1])fa[ch[v][1]]=fa[v];} else{rt=ch[v][1];ch[v][1]=fa[rt]=0;} S.erase(S.begin()); } if(op==5){ int v=*--S.end(),res; printf("%d\n",res=seg.query(1,1,n,v)); seg.modify(1,1,n,fa[v]+1,v,-1); if(fa[v]){ch[fa[v]][1]=ch[v][0];if(ch[v][0])fa[ch[v][0]]=fa[v];} else{rt=ch[v][0];ch[v][0]=fa[rt]=0;} S.erase(--S.end()); } } return 0; }
B 影魔
给定的排列,对一个区间,定义,其贡献为:
- 若不存在或且,则贡献为;
- 若或,则贡献为。
- 其余情况贡献为。
给定个询问,每次询问要求一个区间的所有子区间的贡献之和。
。
tag:线段树,树状数组
本题有一个部分分,对于该部分分,容易发现可以把最大值和的关系分开考虑。具体地,对每个,若,则提供的贡献。对同理,两部分贡献可以直接累加。
那么,用单调栈求出前一个,后一个比它大的位置。对询问按右端点排序,从左往右扫描,给区间的值加,并处理右端点为的所有询问即可。
一般情况,我们只要再求出贡献为的区间即可。考虑最大值,它能作为最大值的区间满足。又要满足的条件,所以题中的区间只有。
为统计某个询问区间中有多少个上述区间,可以使用树状数组套线段树,比较套路。
难度Medium,考虑最大值的位置是基本的,但是考场上脑子一抽想统计贡献为 甚至没有贡献的区间数,然后做不出来。
点击查看代码
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int N=2e5+10; int n,m,a[N],st[N],top,x[N],y[N];ll ans[N],p1,p2; struct query{int l,r,id;}q[N]; bool cmp1(query a,query b){return a.r<b.r;} bool cmp2(query a,query b){return a.l>b.l;} int tot,lc[N*60],rc[N*60];ll sum[N*60],tag[N*60]; #define mid ((l+r)>>1) int new_node(){ ++tot; lc[tot]=rc[tot]=sum[tot]=tag[tot]=0; return tot; } void push_up(int p){sum[p]=sum[lc[p]]+sum[rc[p]];} void push_down(int p,int l,int r){ int v=tag[p];tag[p]=0; if(!v||l==r)return; sum[lc[p]]+=(mid-l+1)*v;tag[lc[p]]+=v; sum[rc[p]]+=(r-mid)*v;tag[rc[p]]+=v; } void modify(int p,int l,int r,int L,int R,int v){ if(l>=L&&r<=R){sum[p]+=(r-l+1)*v;tag[p]+=v;return;} if(!lc[p])lc[p]=new_node();if(!rc[p])rc[p]=new_node(); push_down(p,l,r); if(L<=mid)modify(lc[p],l,mid,L,R,v); if(R>mid)modify(rc[p],mid+1,r,L,R,v); push_up(p); } ll query(int p,int l,int r,int L,int R){ if(!p)return 0; if(l>=L&&r<=R)return sum[p]; push_down(p,l,r); if(R<=mid)return query(lc[p],l,mid,L,R); if(L>mid)return query(rc[p],mid+1,r,L,R); return query(lc[p],l,mid,L,R)+query(rc[p],mid+1,r,L,R); } #undef mid void add(int x,int v){for(;x<=n;x+=x&-x)modify(x,1,n,v,v,1);} int ask(int x,int y){int res=0;for(;x;x-=x&-x)res+=query(x,1,n,y,n);return res;} int main(){ scanf("%d%d%lld%lld",&n,&m,&p1,&p2); for(int i=1;i<=n;i++)scanf("%d",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,cmp1);new_node(); for(int i=1,j=1;i<=n;i++){ while(top&&a[st[top]]<a[i])--top; modify(1,1,n+2,st[top]+1,i,1); x[i]=st[top];st[++top]=i; for(;j<=m&&q[j].r<=i;++j) ans[q[j].id]+=query(1,1,n+2,q[j].l+1,q[j].r+1); }tot=0;new_node();top=0;st[0]=n+1; sort(q+1,q+m+1,cmp2); for(int i=n,j=1;i>=1;i--){ while(top&&a[st[top]]<a[i])--top; modify(1,1,n+2,i+2,st[top]+1,1); y[i]=st[top];st[++top]=i; for(;j<=m&&q[j].l>=i;++j) ans[q[j].id]+=query(1,1,n+2,q[j].l+1,q[j].r+1); } tot=0; for(int i=1;i<=n;i++)new_node(); for(int i=1;i<=n;i++)if(x[i]<y[i]&&x[i]!=0&&y[i]!=n+1)add(y[i],x[i]); for(int i=1;i<=m;i++){ int id=q[i].id,l=q[i].l,r=q[i].r; int cnt1=ask(r,l)+r-l,cnt2=ans[id]-2*cnt1; ans[id]=p1*cnt1+p2*cnt2; } for(int i=1;i<=m;i++)printf("%lld\n",ans[i]); return 0; }
C 礼物
给定两个长为的数列,所有数为中的整数。可以将一个数列中所有数加上一个常数,也可以对一个数列做轮换。最小化。
tag:FFT
假设在数列上加上,则目标函数化为
关于的部分是二次函数,且与是否轮换无关,所以可以直接求出。
剩下的部分是最小化,显然想到卷积形式,用FFT做即可。
但是!我还不会FFT,所以我使用的暴力。毕竟也不大,卡卡常就过了。最慢的点跑了939ms(时限1s),喜提洛谷最劣解。
难度???,应该是板的,但是我不会板,但是我会暴力。暴力场切。
点击查看代码
#include<iostream> #include<cstdio> #include<algorithm> #include<cmath> using namespace std; inline int read(){ int x=0;char ch=getchar(); while(ch<'0'||ch>'9')ch=getchar(); while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();} return x; } typedef long long ll; const int N=5e4+5; int n,m,x[N<<1],y[N],sum,c,ans,res; int main(){ n=read();m=read(); for(register int i=0;i<n;++i)x[n+i]=x[i]=read(),sum+=x[i],ans+=x[i]*x[i]; for(register int i=0;i<n;++i)y[i]=read(),sum-=y[i],ans+=y[i]*y[i]; c=floor(-1.0*sum/n+0.5);ans+=n*c*c+2*c*sum; for(register int i=0;i<n;++i){ sum=0; for(register int j=0;j<n;++j)sum+=x[i+j]*y[j]; res=max(res,sum); } printf("%d\n",ans-2*res); return 0; }
HNOI2018 Day1
2023-05-30
A 寻宝游戏
给定个长为的二进制串,可以在每两个二进制数之间和第一个数之前添加一个运算符(按位与/按位或),然后在这个算式最前面填上。问使得这个算式的值刚好为给定的询问值的添加方法数。组询问。答案对取模。
。
tag:思维题
将按位与看做,按位或看做,会发现:将操作序列和每个数第位构成的序列从后往前比较,若操作序列字典序小,则该位结果为,否则结果为。证明是不难的。
那么只要将每一位构成的序列排序,如果询问串要求比一段前缀大,比一段后缀小就合法,输出分界点的两个数的差;否则无解。
用基数排序可以将时间复杂度做到。
难度Hard-。
点击查看代码
#include<bits/stdc++.h> using namespace std; const int N=1005,M=5005,mod=1e9+7; int n,m,q,d=1,cnt[2],val[M],A[M],B[M];char s[M]; int main(){ scanf("%d%d%d",&n,&m,&q); for(int i=1;i<=m;i++)A[i]=i; for(int i=1;i<=n;i++,d=d*2%mod){ scanf("%s",s+1); cnt[0]=0;cnt[1]=m; for(int j=1;j<=m;j++){ if(s[j]=='0')cnt[0]++; else val[j]=(val[j]+d)%mod; } for(int j=m;j>=1;j--) B[cnt[s[A[j]]-'0']--]=A[j]; for(int j=1;j<=m;j++)A[j]=B[j]; } for(int i=1;i<=m;i++)B[A[i]]=i; A[m+1]=m+1;val[m+1]=d; for(int i=1;i<=q;i++){ int L=0,R=m+1; scanf("%s",s+1); for(int j=1;j<=m;j++){ if(s[j]=='0')L=max(L,B[j]); else R=min(R,B[j]); } if(L>R)printf("0\n"); else printf("%d\n",(val[A[R]]-val[A[L]]+mod)%mod); } return 0; }
B 转盘
一个转盘上有摆成一圈的个物品,依次编号为,编号为的物品会在时刻出现(之后不消失)。在 时刻时,小G可以任选 个物品中的一个,每经过一个单位时间可以继续选择当前物品或选择下一个物品。在每一时刻,如果小 G 选择的物品已经出现了,那么小G将会标记它。问小G至少需要多少时间来标记所有物品。
然而还有次修改,每次修改改变一个物品的出现时间。强制在线。
。
建议BC两题换一下名字
tag:线段树,单调栈
首先转化问题,记,注意到小G最多转一圈,随便推推容易发现:以为起点的答案是。然后可以把上界换成。
这个时候转化视角,考虑对于某个的最小值。显然只用考虑是后缀最大值的情形。设是大于的数中最靠右的一个,则的答案是。
所以可以考虑用线段树维护一个单调栈,从后向前考虑即可。
难度Hard。
点击查看代码
#include<bits/stdc++.h> using namespace std; const int N=1e5+5,INF=1<<30; int n,m,op,a[N<<1],ans; struct node{int mx,res;}tr[N<<2]; int query(int p,int l,int r,int x){ if(l==r)return tr[p].mx>x?x+l:INF; int mid=l+r>>1; if(tr[p<<1|1].mx<=x)return query(p<<1,l,mid,x); return min(tr[p].res,query(p<<1|1,mid+1,r,x)); } void push_up(int p,int l,int r){ tr[p].mx=max(tr[p<<1].mx,tr[p<<1|1].mx); tr[p].res=query(p<<1,l,l+r>>1,tr[p<<1|1].mx); } void build(int p,int l,int r){ if(l==r){tr[p].mx=a[l]-l;return;} int mid=l+r>>1; build(p<<1,l,mid); build(p<<1|1,mid+1,r); push_up(p,l,r); } void modify(int p,int l,int r,int x,int v){ if(l==r){tr[p].mx=v-l;return;} int mid=l+r>>1; if(x<=mid)modify(p<<1,l,mid,x,v); else modify(p<<1|1,mid+1,r,x,v); push_up(p,l,r); } int main(){ scanf("%d%d%d",&n,&m,&op); for(int i=1;i<=n;i++)scanf("%d",a+i); build(1,1,n); printf("%d\n",ans=query(1,1,n,tr[1].mx-n)+n); for(int i=1,x,y;i<=m;i++){ scanf("%d%d",&x,&y); if(op)x^=ans,y^=ans; modify(1,1,n,x,y); printf("%d\n",ans=query(1,1,n,tr[1].mx-n)+n); } return 0; }
C 毒瘤
个点,条边的连通简单无向图,求独立集个数。
。
tag:DP,虚树/动态DP
首先随便取一棵生成树,然后标记所有非树边的端点。
对于树的情况,很容易用树形DP解决。同时有一个显然的暴力:枚举上述所有端点的颜色,做树形DP。
考虑对所有端点建出虚树,在虚树上DP。这样时间复杂度就是,其中,能过。
发现在虚树上的一条边转移时,不论该边两个端点状态如何,因为这两个点在原树上之间的形态相同,所以转移系数是相同的。那么暴力求出每个点到它虚树上的父亲的转移系数。这里可以直接暴力,最多把每个点遍历一次。
剩下就是二进制枚举。
这道题也可以用动态DP,可以算是一个模板题。
难度Hard-。
点击查看代码
#include<bits/stdc++.h> using namespace std; const int N=1e5+50,mod=998244353; int n,m,ans,a[N]; int head[N],ver[N<<1],nxt[N<<1],tot=1; int hc[N],vc[N<<1],nc[N<<1],tc; void add(int u,int v){ver[++tot]=v;nxt[tot]=head[u];head[u]=tot;} void addc(int u,int v){vc[++tc]=v;nc[tc]=hc[u];hc[u]=tc;} int fa[N][20],dep[N],dfn[N],Dfn,tr[N<<1],vis[N],x[N],d[N],k,st[N],top; void dfs(int u){ dfn[u]=++Dfn; for(int i=head[u],v;i;i=nxt[i]) if(!dfn[v=ver[i]]){ tr[i]=tr[i^1]=1; fa[v][0]=u;dep[v]=dep[u]+1; dfs(v); } } void LCA_pre(){ for(int j=1;(1<<j)<=n;j++) for(int i=1;i<=n;i++) fa[i][j]=fa[fa[i][j-1]][j-1]; } int LCA(int u,int v){ if(dep[u]<dep[v])swap(u,v); int d=dep[u]-dep[v]; for(int i=17;i>=0;i--) if(d&(1<<i))u=fa[u][i]; if(u==v)return u; for(int i=17;i>=0;i--) if(fa[u][i]!=fa[v][i]) u=fa[u][i],v=fa[v][i]; return fa[u][0]; } bool cmp(int i,int j){return dfn[i]<dfn[j];} void vtree(){ sort(x+1,x+k+1,cmp);st[top=1]=1; for(int i=1;i<=k;i++)if(x[i]!=1){ int g=LCA(x[i],st[top]); if(g!=st[top]){ while(dfn[g]<dfn[st[top-1]]) addc(st[top-1],st[top]),--top; if(dfn[g]!=dfn[st[top-1]]) addc(g,st[top]),st[top]=g; else addc(g,st[top]),--top; } st[++top]=x[i]; } for(int i=1;i<top;i++)addc(st[i],st[i+1]); } int dp[N][2],trans[N][2][2],f[N][2]; void DP_pre(int u){ dp[u][0]=dp[u][1]=1; for(int i=head[u],v;i;i=nxt[i]) if(tr[i]&&(v=ver[i])!=fa[u][0]){ DP_pre(v); if(!vis[v]){ dp[u][0]=1ll*dp[u][0]*(dp[v][0]+dp[v][1])%mod; dp[u][1]=1ll*dp[u][1]*dp[v][0]%mod; } else vis[u]=1; } } void DP(int u){ f[u][0]=dp[u][0];f[u][1]=dp[u][1]; if(a[u]!=-1)f[u][1-a[u]]=0; for(int i=hc[u],v;i;i=nc[i]){ DP(v=vc[i]); int f0=(1ll*trans[v][0][0]*f[v][0]+1ll*trans[v][0][1]*f[v][1])%mod, f1=(1ll*trans[v][1][0]*f[v][0]+1ll*trans[v][1][1]*f[v][1])%mod; f[u][0]=1ll*f[u][0]*(f0+f1)%mod; f[u][1]=1ll*f[u][1]*f0%mod; } } int main(){ memset(a,-1,sizeof(a)); scanf("%d%d",&n,&m); for(int i=1,u,v;i<=m;i++){ scanf("%d%d",&u,&v); add(u,v);add(v,u); } dfs(1);LCA_pre(); for(int i=2;i<=tot;i+=2)if(!tr[i]){ ++k;vis[d[k]=x[k]=ver[i]]=1; ++k;vis[d[k]=x[k]=ver[i^1]]=1; } DP_pre(1); sort(x+1,x+k+1); k=unique(x+1,x+k+1)-x-1; vtree(); for(int u=1;u<=n;u++) for(int i=hc[u];i;i=nc[i]){ int v=vc[i]; trans[v][0][0]=trans[v][1][1]=1; for(int x=v;fa[x][0]!=u;x=fa[x][0]){ int t00=trans[v][0][0],t01=trans[v][0][1], t10=trans[v][1][0],t11=trans[v][1][1],y=fa[x][0]; trans[v][0][0]=1ll*dp[y][0]*(t00+t10)%mod; trans[v][0][1]=1ll*dp[y][0]*(t01+t11)%mod; trans[v][1][0]=1ll*dp[y][1]*t00%mod; trans[v][1][1]=1ll*dp[y][1]*t01%mod; } } for(int i=0;i<(1<<k);i++){ for(int j=1;j<=k;j++)a[x[j]]=(i>>j-1)&1; bool flag=1; for(int j=1;j<=m-n+1;j++) if(a[d[2*j-1]]&&a[d[2*j]])flag=0; if(!flag)continue; DP(1);ans=(ans+(f[1][0]+f[1][1])%mod)%mod; } printf("%d\n",ans); return 0; }
动态DP:
点击查看代码
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int N=1e5+50;const ll mod=998244353; int n,m;ll f[N][2],ans; int head[N],nxt[N<<1],ver[N<<1],tot=1,tr[N<<1]; void add(int u,int v){ver[++tot]=v;nxt[tot]=head[u];head[u]=tot;} ll power(ll a,ll b){ ll c=1; for(;b;b>>=1){ if(b&1)c=c*a%mod; a=a*a%mod; } return c; } struct Matrix{ ll a[2][2]; Matrix (){a[0][0]=a[0][1]=a[1][0]=a[1][1]=0;} Matrix operator *(const Matrix&b)const{ Matrix c; c.a[0][0]=(a[0][0]*b.a[0][0]+a[0][1]*b.a[1][0])%mod; c.a[1][0]=(a[1][0]*b.a[0][0]+a[1][1]*b.a[1][0])%mod; c.a[0][1]=(a[0][0]*b.a[0][1]+a[0][1]*b.a[1][1])%mod; c.a[1][1]=(a[1][0]*b.a[0][1]+a[1][1]*b.a[1][1])%mod; return c; } }g[N]; int fa[N],top[N],siz[N],L[N],dfn,R[N],son[N],id[N]; void dfs(int u){ f[u][0]=f[u][1]=1;siz[u]=1; for(int i=head[u],v;i;i=nxt[i]) if(!siz[v=ver[i]]){ tr[i]=tr[i^1]=1; fa[v]=u;dfs(v);siz[u]+=siz[v]; if(siz[v]>siz[son[u]])son[u]=v; f[u][0]=f[u][0]*(f[v][0]+f[v][1])%mod; f[u][1]=f[u][1]*f[v][0]%mod; } } void rdfs(int u,int tp){ g[u].a[0][0]=g[u].a[1][0]=1; L[u]=++dfn;id[dfn]=u;top[u]=tp;R[tp]=dfn; if(son[u])rdfs(son[u],tp); for(int i=head[u],v;i;i=nxt[i]) if(tr[i]&&(v=ver[i])!=fa[u]&&v!=son[u]){ rdfs(v,v); g[u].a[0][0]=g[u].a[0][0]*(f[v][0]+f[v][1])%mod; g[u].a[1][0]=g[u].a[1][0]*f[v][0]%mod; } g[u].a[0][1]=g[u].a[0][0]; } struct SegmentTree{ Matrix a[N<<2]; #define mid (l+r>>1) void build(int p,int l,int r){ if(l==r){a[p]=g[id[l]];return;} build(p<<1,l,mid); build(p<<1|1,mid+1,r); a[p]=a[p<<1]*a[p<<1|1]; } void modify(int p,int l,int r,int x){ if(l==r){a[p]=g[id[l]];return;} if(x<=mid)modify(p<<1,l,mid,x); else modify(p<<1|1,mid+1,r,x); a[p]=a[p<<1]*a[p<<1|1]; } Matrix query(int p,int l,int r,int L,int R){ if(l>=L&&r<=R)return a[p]; if(R<=mid)return query(p<<1,l,mid,L,R); if(L>mid)return query(p<<1|1,mid+1,r,L,R); return query(p<<1,l,mid,L,R)*query(p<<1|1,mid+1,r,L,R); } #undef mid }seg; int a[N],x[50],k,st[N],Top;Matrix g0[N]; vector<int> rej[N]; void update(int u,int op){ st[++Top]=u,g0[Top]=g[u]; if(op==0)g[u].a[1][0]=0; else g[u].a[0][0]=g[u].a[0][1]=0; while(u){ Matrix lst=seg.query(1,1,n,L[top[u]],R[top[u]]); seg.modify(1,1,n,L[u]); Matrix now=seg.query(1,1,n,L[top[u]],R[top[u]]); u=fa[top[u]];st[++Top]=u,g0[Top]=g[u]; g[u].a[0][0]=g[u].a[0][0]*(now.a[0][0]+now.a[1][0])%mod *power(lst.a[0][0]+lst.a[1][0],mod-2)%mod; g[u].a[1][0]=g[u].a[1][0]*now.a[0][0]%mod *power(lst.a[0][0],mod-2)%mod; g[u].a[0][1]=g[u].a[0][0]; } } void clear(int tim){ while(Top>tim){ g[st[Top]]=g0[Top]; seg.modify(1,1,n,L[st[Top]]); --Top; } } void Enum(int p){ if(p==k+1){ Matrix res=seg.query(1,1,n,1,R[1]); ans=(ans+res.a[0][0]+res.a[1][0])%mod; return; } int tim=Top; update(x[p],a[x[p]]=0);Enum(p+1);clear(tim);a[x[p]]=-1; update(x[p],a[x[p]]=1);bool flag=1; for(auto t:rej[x[p]])if(a[t]==1){flag=0;break;} if(flag)Enum(p+1);clear(tim);a[x[p]]=-1; } int main(){ memset(a,-1,sizeof(a)); scanf("%d%d",&n,&m); for(int i=1,u,v;i<=m;i++){ scanf("%d%d",&u,&v); add(u,v);add(v,u); } dfs(1);rdfs(1,1);seg.build(1,1,n); for(int i=2;i<=tot;i+=2)if(!tr[i]){ x[++k]=ver[i];x[++k]=ver[i^1]; rej[ver[i]].push_back(ver[i^1]); rej[ver[i^1]].push_back(ver[i]); } sort(x+1,x+k+1);k=unique(x+1,x+k+1)-x-1; Enum(1); printf("%lld\n",ans); return 0; }
USACO23open选做
2023-05-22
A Custodial Cleanup G
个点 条边的无向图,点 有一个颜色 ,一把颜色为 的钥匙,目标是在该点放置颜色为 的钥匙。FJ初始在点 ,他可以捡起当前点的钥匙,放下一把或多把钥匙,以及移动向相邻的点。这里能够移动的条件是FJ手中至少有一把和目标点颜色相同的钥匙。问FJ能否完成目标。 组数据。
, 。
, , 。
tag:BFS
(其实这个tag也无所谓,关键是分析)
首先考虑FJ至多能捡起多少钥匙,此时当然钦定FJ不放下任何钥匙。做一个BFS,不断扩展可以到达的连通块。扩展过程中,维护已经拿到的钥匙(用一个bool数组),并记录各个颜色相邻的点(用个vector)。每次取出新点,检查是否获得该颜色的钥匙,如果没有则更新,并将该颜色的vector中所有点加入队列。然后扩展该点的邻点,如果颜色可以走到就加入队列,否则加入相应的vector。
在过程中,我们求出了FJ能到达的所有点。在这些点中,再来考虑FJ能放下哪些钥匙。假设FJ可以完成目标,让FJ倒着运动,即初始时点 有一把颜色为 的钥匙,此时FJ可以进入一个点,当且仅当他有该点颜色的钥匙,或者该点颜色的钥匙就放在该房间里。同样的做一遍BFS即可。
对于第二遍BFS中不能到达的点,检查是否有 成立。若有不成立者,则答案为NO,否则为YES。
难度Easy+,场切。
点击查看代码
#include<bits/stdc++.h> using namespace std; const int N=1e5+5; int T,n,m,c[N],s[N],t[N]; int head[N],nxt[N<<1],ver[N<<1],tot; void add(int u,int v){ver[++tot]=v;nxt[tot]=head[u];head[u]=tot;} int vis1[N],vis2[N],tag[N]; queue<int> Q;vector<int> col[N]; int main(){ scanf("%d",&T); while(T--){ scanf("%d%d",&n,&m);tot=0; for(int i=1;i<=n;i++){col[i].clear();head[i]=vis1[i]=tag[i]=0;} for(int i=1;i<=n;i++)scanf("%d",c+i); for(int i=1;i<=n;i++)scanf("%d",s+i); for(int i=1;i<=n;i++)scanf("%d",t+i); for(int i=1,u,v;i<=m;i++){scanf("%d%d",&u,&v);add(u,v);add(v,u);} Q.push(1); while(!Q.empty()){ int u=Q.front();Q.pop();vis1[u]=1; if(!tag[s[u]]){ for(auto x:col[s[u]])Q.push(x); col[s[u]].clear();tag[s[u]]=1; } for(int i=head[u],v;i;i=nxt[i]) if(!vis1[v=ver[i]]){ if(tag[c[v]])Q.push(v); else col[c[v]].push_back(v); } } for(int i=1;i<=n;i++){col[i].clear();vis2[i]=tag[i]=0;} Q.push(1); while(!Q.empty()){ int u=Q.front();Q.pop();vis2[u]=1; if(!tag[t[u]]){ for(auto x:col[t[u]])Q.push(x); col[t[u]].clear();tag[t[u]]=1; } for(int i=head[u],v;i;i=nxt[i]) if(vis1[v=ver[i]]&&!vis2[v]){ if(tag[c[v]]||c[v]==t[v])Q.push(v); else col[c[v]].push_back(v); } } bool flag=1; for(int i=1;i<=n;i++)if(!vis2[i]&&s[i]!=t[i])flag=0; printf(flag?"YES\n":"NO\n"); } return 0; }
B Tree Merging G
定义一棵有根树的一次合并操作为:选择两个具有相同父亲的结点,将其合并成一个节点,新节点的编号为原来的两个节点编号的较大值,新节点的子节点集合为原来的两个节点子节点集合的并集。
给定初始状态和最终状态,构造操作序列。保证有解。初始状态和最终状态分别是 , 个点的树。 组数据。
, 。
tag:思维题
按照深度处理。
假设深度较小的所有合并已经正确维护,考虑某一个节点的所有儿子。那些不在最终树中的点将被合并。
Case 1:如果该点的子树中有点被保留,那么找到被保留的这个点的某个祖先与之深度相同,合并。
Case 2:否则,找到一个点,最终树中其子树往下每层的最大值都大于该点的最大值,合并。
所有步骤都可以暴力,我甚至还用了一个set。看起来复杂度不太对,但是应该不好卡。
难度Medium-,场切。
点击查看代码
#include<bits/stdc++.h> using namespace std; const int N=1005; int T,n,m,rt,f[N],g[N],r[N],mx[N][N],mx0[N][N]; set<int> a[N],b[N],s[N]; vector<pair<int,int> > ans; void merge(int x,int y){ ans.push_back(make_pair(x,y)); for(auto p:a[x])a[y].insert(p);a[x].clear(); for(auto p:s[x])s[y].insert(p);s[x].clear(); for(int i=1;mx[y][i];i++)mx[y][i]=max(mx[y][i],mx[x][i]); } void dfsa(int u){ s[u].clear();s[u].insert(u);mx[u][0]=u; for(auto v:a[u]){ dfsa(v); for(auto x:s[v])s[u].insert(x); for(int i=0;mx[v][i];i++) mx[u][i+1]=max(mx[u][i+1],mx[v][i]); } } void dfsb(int u){ mx0[u][0]=u; for(auto v:b[u]){ dfsb(v); for(int i=0;mx0[v][i];i++) mx0[u][i+1]=max(mx0[u][i+1],mx0[v][i]); } } void dfs(int u){ for(auto x:a[u])if(b[u].find(x)==b[u].end()){ int y=0; for(auto z:s[x])if(r[z]){y=z;break;} if(y){ for(;b[u].find(y)==b[u].end();y=g[y]); merge(x,y);continue; } for(auto y:b[u]){ bool flag=1; for(int i=0;mx[x][i];i++)if(mx0[y][i]<mx[x][i])flag=0; if(flag){merge(x,y);break;} } } for(auto v:b[u])dfs(v); } int main(){ scanf("%d",&T); while(T--){ scanf("%d",&n); ans.clear(); for(int i=1;i<=n;i++){ a[i].clear(),b[i].clear(),r[i]=f[i]=0; for(int j=0;mx[i][j];j++)mx[i][j]=0; for(int j=0;mx0[i][j];j++)mx0[i][j]=0; } for(int i=1,u,v;i<n;i++){ scanf("%d%d",&u,&v); a[v].insert(u);f[u]=v; } scanf("%d",&m); for(int i=1,u,v;i<m;i++){ scanf("%d%d",&u,&v); b[v].insert(u);g[u]=v;r[u]=r[v]=1; } for(int i=1;i<=n;i++)if(!f[i])rt=i; dfsa(rt);dfsb(rt);dfs(rt); printf("%d\n",ans.size()); for(auto x:ans)printf("%d %d\n",x.first,x.second); } return 0; }
C Pareidolia P
对于一个字符串,其权值为满足下面条件的 的最大值:字符串bessie
重复 次后是该字符串的子串。
给定一个长为 的字符串 和 次修改字符,求所有修改前和每次修改后,字符串所有子串的权值之和。
。
tag:DP,线段树
首先用DP处理静态问题。记 bessie
,下标从 开始。用 表示以 结尾,处理到 的子串数目。转移如下:
用线段树维护这个转移,对每个区间,需要记录:
- 从 进入,离开的字符 。
- 离开的字符是 的后缀数 。
- 进入的字符是 的贡献位置数 。
合并时,用左边的 与右边的 相乘更新答案。
难度Medium+。
点击查看代码
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int N=2e5+5; int n,m;char s[N]; string t="bessie"; struct node{ll nxt[6],cnt[6],res[6],sum;}tr[N<<2]; node gen(char ch,int pos){ node x;x.sum=0; for(int i=0;i<6;i++)x.nxt[i]=x.cnt[i]=x.res[i]=0; if(pos){ for(int i=0;i<6;i++)x.nxt[i]=(ch==t[i]?(i+1)%6:i); x.cnt[x.nxt[0]]=1;x.res[5]=(ch=='e'?n-pos+1:0); } return x; } void push_up(int p){ node a=tr[p<<1],b=tr[p<<1|1],c=gen('#',0); c.sum=a.sum+b.sum; for(int i=0;i<6;i++){ c.nxt[i]=b.nxt[a.nxt[i]]; c.cnt[i]+=b.cnt[i];c.cnt[b.nxt[i]]+=a.cnt[i]; c.res[i]=b.res[a.nxt[i]]+a.res[i]; c.sum+=a.cnt[i]*b.res[i]; } tr[p]=c; } void build(int p,int l,int r){ if(l==r){tr[p]=gen(s[l],l);return;} build(p<<1,l,l+r>>1); build(p<<1|1,(l+r>>1)+1,r); push_up(p); } void modify(int p,int l,int r,int x,char c){ if(l==r){tr[p]=gen(c,x);return;} int mid=l+r>>1; if(x<=mid)modify(p<<1,l,mid,x,c); else modify(p<<1|1,mid+1,r,x,c); push_up(p); } int main(){ scanf("%s",s+1);n=strlen(s+1); build(1,1,n); printf("%lld\n",tr[1].sum); scanf("%d",&m); for(int i=1;i<=m;i++){ int x;char c[2]; scanf("%d%s",&x,c); modify(1,1,n,x,c[0]); printf("%lld\n",tr[1].sum); } return 0; }
D Triples of Cows P
给定一棵初始有 个点的树。在第 天,这棵树的第 个点会被删除,所有与点 直接相连的点之间都会两两连上一条边。在每次删点前,求出满足 之间有边, 之间有边且 的有序三元组 数。
。
tag:拆边,并查集
由于每次删点之后新增的边数太多,所以考虑拆边。用一个白点表示一条边,并将原来树中的点称为黑点,编号不变。对于第 条边 ,在 间连边,得到一棵树 。
删除点 时,我们将点 相邻的所有白点合并为一个点,然后删除 。容易归纳证明:每次删除后 仍然是树;真实的 间有边等价于树 中存在一个白点与 均相邻。
因为依次删除 ,所以在树 中,可以把点 作为根。这样每次删除时,就可以把 的所有相邻白点合并到 的父亲白点上去。然后,用并查集维护每个白点被合并到了哪个点。
用 表示在初始的 中 的父亲结点, 表示某个时刻白点 被合并到的点。那么,在这一时刻,黑点 的父亲结点是 ,白点 的父亲节点是 。
下面来考虑如何统计答案。对白点 ,记 为 的儿子个数。
对于一个符合要求的 ,设 通过白点 相连, 通过白点 相连。
- 如果 :固定 ,在 的邻点中任选 个,则对答案的贡献为求和的条件是 是白点。
- 如果 ,且 都是 的子节点:固定 ,先任取 的两个子结点 (有序),此时贡献 。则总的贡献为第一个求和的条件是 是黑点,后两个求和的条件是 是 的儿子。
注意到后一项拆出来就是对所有白点 ,求 的和,那就拆出来吧。 - 如果 ,且 一个是 的子结点,另一个是 的父亲结点:不妨 是 的父亲结点,固定 , 是 的一个子结点, 又是 的一个子结点,则对答案的贡献为三个求和的条件分别为 是白点, 是 的子结点, 是 的结点。
列出式子后,我们发现需要维护以下数据:
- 白点 的儿子数目
- 黑点 的儿子的 值之和,也可以存到数组 里
- 白点 的儿子的 值之和
答案是 ,其中 ,两个求和的条件分别是 是黑点,是白点。
删除点 时,枚举它初始的的儿子(一定没有被合并过),在并查集中将其合并到 的父亲中。然后清零 和 的儿子的 值,更新 的三层祖先的值,并更新答案。
难度Hard。
点击查看代码
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int N=4e5+5; int n,fa[N],p[N];ll s[N],t[N],ans; int head[N],ver[N<<1],nxt[N<<1],tot; void add(int u,int v){ver[++tot]=v;nxt[tot]=head[u];head[u]=tot;} void dfs(int u){ for(int i=head[u],v;i;i=nxt[i]) if((v=ver[i])!=fa[u]){ fa[v]=u;dfs(v); if(u<=n)s[u]+=s[v]; else ++s[u],t[u]+=s[v]; } } int find(int x){return (x==p[x]?x:(p[x]=find(p[x])));} ll f(ll x){return x*x*x-x*x-x;} int main(){ scanf("%d",&n); for(int i=1,u,v;i<n;i++){ scanf("%d%d",&u,&v); add(u,n+i);add(v,n+i);add(n+i,u);add(n+i,v); } dfs(n); for(int i=1;i<2*n;i++)p[i]=i; for(int i=1;i<=n;i++)ans+=s[i]*s[i]; for(int i=n+1;i<2*n;i++)ans+=f(s[i])+2*s[i]*t[i]; for(int u=1;u<=n;u++){ printf("%lld\n",ans); int g=find(fa[u]),w=fa[g];ll del=-1; ans-=f(s[g])+2*s[g]*t[g]+s[w]*s[w];s[w]-=s[g];--s[g]; t[g]-=s[u];ans-=s[u]*s[u];s[u]=0; for(int i=head[u],v;i;i=nxt[i]) if((v=ver[i])!=fa[u]){ p[v]=g;s[g]+=s[v];t[g]+=t[v];del+=s[v]; ans-=f(s[v])+2*s[v]*t[v];s[v]=t[v]=0; } s[w]+=s[g];ans+=f(s[g])+2*s[g]*t[g]+s[w]*s[w]; t[w=find(fa[fa[g]])]+=del;ans+=2*s[w]*del; } return 0; }
APIO2015
2023-04-20
A 巴厘岛的雕塑
个数分为若干组,组数不少于 且不多于 。最小化各组和的 值。
, 或 ,。
tag:贪心,DP
按位处理,从高到低依次尝试每一位是否能够是 。 求出在满足高位的条件下的最少分组数和最多分组数即可。
但似乎这个做法是伪的,uoj上没有过。
重新考虑 状态,考虑到某一位能否分若干组即可。不想写了。
难度Easy,场切。
点击查看代码
#include<bits/stdc++.h> using namespace std; const int N=2005; typedef long long ll; int n,a,b,dp[N][2];ll s[N],res; int main(){ scanf("%d%d%d",&n,&a,&b); for(int i=1,x;i<=n;i++){ scanf("%d",&x); s[i]=s[i-1]+x; } res=(1ll<<45)-1; for(int d=44;d>=0;d--){ ll chk=res-(1ll<<d); dp[0][0]=dp[0][1]=0; for(int i=1;i<=n;i++){ dp[i][0]=1e9;dp[i][1]=-1e9; for(int j=0;j<i;j++) if((s[i]-s[j]|chk)==chk){ dp[i][0]=min(dp[i][0],dp[j][0]+1); dp[i][1]=max(dp[i][1],dp[j][1]+1); } } if(dp[n][1]>=a&&dp[n][0]<=b)res=chk; } printf("%lld\n",res); return 0; }
B 雅加达的摩天楼
有 座楼排列成一条直线,依次编号为 到 。有 只叫做 “doge” 的神秘生物,编号依次是 到 。编号为 的 doge 最初在 的楼。doge 能够跳跃,编号为 的 doge 的跳跃能力为 。在一次跳跃中,位于摩天楼 而跳跃能力为 的 doge 可以跳跃到编号为 (如果 )或 (如果 )的摩天楼。
编号为 的 doge 有一条紧急的消息要尽快传送给编号为 的 doge。任何一个收到消息的 doge 有以下两个选择:
- 跳跃到其他摩天楼上;
- 将消息传递给它当前所在的摩天楼上的其他 doge。
计算将消息从 号 doge 传递到 号 doge 所需要的最少总跳跃步数,或者告诉它们消息永远不可能传递到 号 doge。
,,
tag:最短路,建图技巧
以位置为顶点建图,每个点上如果有 doge 就向它能够跳到的点连边。跑 dijkstra 可以求解,但是会 T。
考虑重复的边。一个剩余类中可能有很多 doge,没有必要让它们都连边。事实上,同一个剩余类中只需要在相邻的 doge 间连边即可。此时容易证明边数级别为 。时间复杂度为 。
难度Medium-,场切。
点击查看代码
#include<bits/stdc++.h> using namespace std; typedef pair<int,int> pii; typedef vector<int> vi; const int N=3e4+5,M=1e7+5; int n,m,r,s,t,dis[N],vis[N]; int head[N],nxt[M],ver[M],val[M],tot; void add(int u,int v,int w){ ver[++tot]=v;val[tot]=w; nxt[tot]=head[u];head[u]=tot; } map<pii,vi> MP;vi d[N]; priority_queue<pii> Q; int main(){ scanf("%d%d",&n,&m); for(int i=1,b,p;i<=m;i++){ scanf("%d%d",&b,&p); if(i==1)s=b;if(i==2)t=b; d[b].push_back(p); MP[pii(b%p,p)].push_back(b); } for(auto f:MP){ vi x=f.second; int b=f.first.first,p=f.first.second; sort(x.begin(),x.end());int len=x.size(); for(int i=0;i<len;i++){ int lst=b,nxt=(n-b)/p*p+b; if(i!=0)lst=x[i-1]; if(i!=len-1)nxt=x[i+1]; for(int j=lst;j<=nxt;j+=p) add(x[i],j,abs(x[i]-j)/p); } } memset(dis,0x3f,sizeof(dis)); Q.push(make_pair(0,s));dis[s]=0; while(!Q.empty()){ int u=Q.top().second,d=-Q.top().first;Q.pop(); if(vis[u])continue;vis[u]=1; for(int i=head[u],v;i;i=nxt[i]) if(dis[v=ver[i]]>d+val[i]){ dis[v]=d+val[i]; Q.push(make_pair(-dis[v],v)); } } if(dis[t]>1e9)printf("-1\n"); else printf("%d\n",dis[t]); return 0; }
C 巴邻旁之桥
河岸两旁有 个人居住和工作,两岸分别为 A 和 B。每个人有一个居住地和工作地。现在要建 座桥使得所有人上班的总路程最小。桥长为 。求最小值。
,
tag:中位数
首先不考虑那些居住和工作在同侧的人。对于剩下的人,先加上桥上路程。
的情况中,设桥建在位置 ,则目标函数形如 ,这里 表示所有位置。要最小化目标函数,只用将 取为中位数即可。
的情况,对于一个人来说,他应当选择距离自己居住地和工作地中点更近的一座桥。那么按照这个中点排序,两座桥一定分别负责一段前缀和一段后缀。枚举这样的划分。对前缀求解时,进行一遍扫描,只需要动态维护中位数,用对顶堆即可。时间复杂度 。
难度Medium,场切。
点击查看代码
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int N=1e5+5; int n,k;ll ans;char c,d; namespace task1{ int a[N*2],m=0; void solve(){ for(int i=1,x,y;i<=n;i++){ cin>>c>>x>>d>>y; if(c==d)ans+=abs(x-y); else a[++m]=x,a[++m]=y; } sort(a+1,a+m+1); for(int i=1;i<=m;i++)ans+=abs(a[i]-a[m/2]); printf("%lld\n",ans+m/2); } } namespace task2{ struct P{int l,r;}a[N]; int m=0;ll res=1e18,pre[N],suf[N]; bool cmp(P a,P b){return a.l+a.r<b.l+b.r;} void solve(){ memset(pre,0x3f,sizeof(pre)); memset(suf,0x3f,sizeof(suf)); for(int i=1,x,y;i<=n;i++){ cin>>c>>x>>d>>y; if(x>y)swap(x,y); if(c==d)ans+=y-x; else a[++m].l=x,a[m].r=y; } if(!m){printf("%lld\n",ans);return;} sort(a+1,a+m+1,cmp); pre[0]=suf[m+1]=0; priority_queue<int> Q1,Q2; while(!Q1.empty())Q1.pop();while(!Q2.empty())Q2.pop(); pre[1]=a[1].r-a[1].l; Q1.push(a[1].l);Q2.push(-a[1].r); ll s1=a[1].l,s2=a[1].r; for(int i=2,x,tmp;i<=m;i++){ tmp=Q1.top(); x=a[i].l;if(x<tmp)Q1.push(x),s1+=x;else Q2.push(-x),s2+=x; x=a[i].r;if(x<tmp)Q1.push(x),s1+=x;else Q2.push(-x),s2+=x; if(Q1.size()>i){x=Q1.top();Q1.pop();s1-=x;s2+=x;Q2.push(-x);} if(Q2.size()>i){x=-Q2.top();Q2.pop();s2-=x;s1+=x;Q1.push(x);} pre[i]=s2-s1; } while(!Q1.empty())Q1.pop();while(!Q2.empty())Q2.pop(); suf[m]=a[m].r-a[m].l; Q1.push(a[m].l);Q2.push(-a[m].r); s1=a[m].l;s2=a[m].r; for(int i=m-1,x,tmp;i>=1;i--){ tmp=Q1.top(); x=a[i].l;if(x<tmp)Q1.push(x),s1+=x;else Q2.push(-x),s2+=x; x=a[i].r;if(x<tmp)Q1.push(x),s1+=x;else Q2.push(-x),s2+=x; if(Q1.size()>m-i+1){x=Q1.top();Q1.pop();s1-=x;s2+=x;Q2.push(-x);} if(Q2.size()>m-i+1){x=-Q2.top();Q2.pop();s2-=x;s1+=x;Q1.push(x);} suf[i]=s2-s1; } for(int i=0;i<=m;i++)res=min(res,pre[i]+suf[i+1]); printf("%lld\n",ans+m+res); } } int main(){ ios::sync_with_stdio(false); cin>>k>>n; if(k==1)task1::solve(); else task2::solve(); return 0; }
APIO2014
2023-04-04
A 回文串
给定字符串 。对 的所有回文子串,求其长度与出现次数之积的最大值。
。
tag:Manacher、后缀数组 / 回文树
解法1:首先用Manacher算法求出所有的回文串。这并不难,只要在右端点每次扩展的时候记录回文串就可以了,剩下的回文串已经统计过了。
对每个回文串,我们来求它出现的次数。使用后缀数组,找到这个回文串对应的后缀在SA中对应的位置,然后向左向右两次二分,求出包含这一前缀的后缀数目。后缀数组的性质决定了这可以实现。
时间复杂度 。
点击查看代码
#include<bits/stdc++.h> using namespace std; typedef long long ll; typedef unsigned long long ull; const int N=3e5+5; int n,m,a[N<<1],lg2[N],cnt,str[N<<2][2]; int S,x[N],y[N],c[N],sa[N],rk[N],h[N],mn[N][25]; ll ans;char s[N],t[N<<1]; void SA(){ S=300; for(int i=1;i<=n;i++){x[i]=s[i];++c[x[i]];} for(int i=2;i<=S;i++)c[i]+=c[i-1]; for(int i=n;i>=1;i--)sa[c[x[i]]--]=i; for(int k=1;k<=n;k<<=1){ int num=0; for(int i=n-k+1;i<=n;i++)y[++num]=i; for(int i=1;i<=n;i++)if(sa[i]>k)y[++num]=sa[i]-k; for(int i=1;i<=S;i++)c[i]=0; for(int i=1;i<=n;i++)c[x[i]]++; for(int i=2;i<=S;i++)c[i]+=c[i-1]; for(int i=n;i>=1;i--){sa[c[x[y[i]]]--]=y[i];y[i]=0;} for(int i=1;i<=n;i++)swap(x[i],y[i]); num=1;x[sa[1]]=1; for(int i=2;i<=n;i++){ if(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k]) x[sa[i]]=num; else x[sa[i]]=++num; } if(num==n)break;S=num; } } void LCP(){ int k=0; for(int i=1;i<=n;i++)rk[sa[i]]=i; for(int i=1;i<=n;i++){ if(rk[i]==1)continue; if(k)k--; int j=sa[rk[i]-1]; while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k])k++; h[rk[i]]=k; } } void build_ST(){ for(int i=1;i<=n;i++)mn[i][0]=h[i]; for(int j=1;(1<<j)<=n;j++) for(int i=1;i<=n-(1<<j)+1;i++) mn[i][j]=min(mn[i][j-1],mn[i+(1<<j-1)][j-1]); } int query(int l,int r){ if(l>r)return 0; int i=lg2[r-l+1];int j=(1<<i); return min(mn[l][i],mn[r-j+1][i]); } void Manacher(){ int l=1,r=0; for(int i=1;i<=m;++i){ if(r>=i&&i+a[l+r-i]<=r)a[i]=a[l+r-i]; else{ int k=max(0,r-i); while(i+k<=m&&i-k>=1&&t[i+k]==t[i-k]){ ++k;++cnt; str[cnt][0]=(i-k+2)/2;str[cnt][1]=(i+k-1)/2; if(str[cnt][0]>str[cnt][1])--cnt; } a[i]=k;l=i-k+1;r=i+k-1; } } } ll solve(int l,int r){ int L=0,R=0,p1=0,p2=0; L=2;R=rk[l];p1=rk[l]+1; while(L<=R){ int mid=L+R>>1; if(query(mid,rk[l])>=r-l+1)p1=mid,R=mid-1; else L=mid+1; } L=rk[l]+1;R=n;p2=rk[l]; while(L<=R){ int mid=L+R>>1; if(query(rk[l]+1,mid)>=r-l+1)p2=mid,L=mid+1; else R=mid-1; } return 1ll*(p2-p1+2)*(r-l+1); } int main(){ scanf("%s",s+1); n=strlen(s+1);m=2*n+1;t[1]='#'; for(int i=0;(1<<i)<=n;i++)lg2[1<<i]=i; for(int i=1,lst=0;i<=n;i++){ if(lg2[i])lst=lg2[i]; else lg2[i]=lst; } for(int i=n;i>=1;--i)t[2*i]=s[i],t[2*i+1]='#'; Manacher();SA();LCP();build_ST(); for(int i=1;i<=cnt;i++)ans=max(ans,solve(str[i][0],str[i][1])); printf("%lld\n",ans); return 0; }
解法2:回文树模板题。似乎不必多讲。
难度Medium,场切,但是因为数据太水。做法不是以上两种。
B 序列分割
将长为 的数组分 次,每次在某一段内部将其分成两段,得分是分出两段的元素和的乘积。 次操作的总得分是每次得分之和。求最大得分并输出方案。
,,。
tag:斜率优化DP
首先发现分段的顺序无关紧要,最后的得分是分出各段元素和两两积的和,也就是所有元素总和的平方,减去各段元素和的平方,再折半。所以只用求各段元素和的平方的最小值。
用 表示前 个分为 段的最小值。 作为初始值,其中 是前缀和。
转移方程是
然后按照斜率优化的套路写就可以了。
难度Easy+。
点击查看代码
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int N=1e5+5,K=205; int n,k,lst[K][N]; ll dp[K][N],s[N],b[N],sum; int q[N],he,ta; int main(){ scanf("%d%d",&n,&k); for(int i=1,x;i<=n;i++){ scanf("%d",&x); s[i]=s[i-1]+x;sum+=x; } memset(dp,0x3f,sizeof(dp)); for(int j=1;j<=n;j++)dp[1][j]=s[j]*s[j]; for(int i=2;i<=k+1;i++){ he=ta=1;q[1]=i-1; b[i-1]=s[i-1]*s[i-1]+dp[i-1][i-1]; for(int j=i;j<=n;j++){ while(he<ta&&(b[q[he+1]]-b[q[he]])<=2*s[j]*(s[q[he+1]]-s[q[he]]))++he; dp[i][j]=s[j]*s[j]-2*s[q[he]]*s[j]+b[q[he]];lst[i][j]=q[he]; b[j]=s[j]*s[j]+dp[i-1][j]; while(ta>he&&(b[j]-b[q[ta-1]])*(s[j]-s[q[ta]])>= (b[j]-b[q[ta]])*(s[j]-s[q[ta-1]]))--ta; q[++ta]=j; } } printf("%lld\n",(sum*sum-dp[k+1][n])/2); for(int i=k,j=lst[k+1][n];i>=1;i--){ printf("%d ",j); j=lst[i][j]; } return 0; }
C 连珠线
初始有一个点,用如下两种操作生成一棵树:
Append(w, v)
:一个新的点 和一个已经添加的点 用红边连接起来。
Insert(w, u, v)
:一个新的点 插入到用红边连起来的两个点 之间。具体过程是删去 之间红边,分别用蓝边连接 和 。
给定 个点的最终状态。求蓝边权值之和的最大可能值。
。
tag:换根DP
首先分析这样生成的树的蓝边分布有什么限制。显然蓝边可以按照加入时间两两配对,且配对的两条边相邻。称它们的公共点为这两条蓝边的中心。
可以把操作看成删边。考虑最后剩下某个点 的情形,将原树看做以 为根的有根树。用 表示将以 为根的子树删到只剩 ,且 不作为某组蓝边的中心时的最大值。 类似,但表示 作为某个中心的最大值。注意此时 所在的两条蓝边必定包含它向上连的边,否则删去这两条边后图出现多个连通分支,不合题意。这里 的值不包括向上的这条边。
在此基础上,不难写出转移方程:
其中 表示连接 的边的边权。
然后换根即可。只用预处理 的最大值和次大值。
难度Medium,换根不熟悉。
点击查看代码
#include<bits/stdc++.h> using namespace std; const int N=2e5+5,INF=1<<29; int n,dp[N][2],g[N][2],ans; int head[N],nxt[N<<1],ver[N<<1],val[N<<1],tot; int max(int a,int b){return a>b?a:b;} void add(int u,int v,int w){ ver[++tot]=v; val[tot]=w; nxt[tot]=head[u]; head[u]=tot; } void dfs(int u,int fa){ dp[u][0]=0;g[u][0]=g[u][1]=-INF; for(int i=head[u];i;i=nxt[i]){ int v=ver[i]; if(v==fa)continue; dfs(v,u); int tmp=max(dp[v][0],dp[v][1]+val[i]); dp[u][0]+=tmp; tmp=dp[v][0]+val[i]-tmp; if(tmp>g[u][0])g[u][1]=g[u][0],g[u][0]=tmp; else if(tmp>g[u][1])g[u][1]=tmp; } dp[u][1]=g[u][0]+dp[u][0]; } void redfs(int u,int fa,int in){ for(int i=head[u];i;i=nxt[i]){ int v=ver[i]; if(v==fa)continue; int t0=dp[u][0],t1=dp[u][1],t2=dp[v][0],t3=dp[v][1]; int tmp=max(dp[v][0],dp[v][1]+val[i]); dp[u][0]=dp[u][0]-tmp; if(g[u][0]==val[i]+dp[v][0]-tmp)dp[u][1]=dp[u][0]+max(in,g[u][1]); else dp[u][1]=dp[u][0]+max(in,g[u][0]); tmp=max(dp[u][0],dp[u][1]+val[i]); dp[v][0]=dp[v][0]+tmp; dp[v][1]=dp[v][0]+max(g[v][0],val[i]+dp[u][0]-tmp); ans=max(ans,dp[v][0]); redfs(v,u,val[i]+dp[u][0]-tmp); dp[u][0]=t0;dp[u][1]=t1;dp[v][0]=t2;dp[v][1]=t3; } } int main(){ scanf("%d",&n); for(int i=1,u,v,w;i<n;i++){ scanf("%d%d%d",&u,&v,&w); add(u,v,w);add(v,u,w); } dfs(1,-1);ans=dp[1][0]; redfs(1,-1,-INF); printf("%d\n",ans); return 0; }
APIO2013
2023-04-10
A 机器人
给定一个 的带障碍网格图以及 个顺次编号机器人,每次将一个机器人推向某个方向知道碰到墙或障碍才会停止,如果碰到了转向器(不算障碍),会转向继续前进。在停下后若有编号相邻的,则将两个机器人合并,新编号为最小的编号与最大的编号。同一时刻只能有一个机器人移动,求最小推动次数使所有机器人合并。
,
tag:分层图DP
用 表示将编号 到 的机器人合并到点 的最小次数。
两种转移:一种是在某处将两个机器人合并,另一种是同一个机器人的移动。
前者直接枚举合并即可,后者跑最短路。
难度Hard-。
点击查看代码
#include<bits/stdc++.h> using namespace std; typedef pair<int,int> pii; const int N=502,INF=0x3f3f3f3f; int T,m,n,L,R,f[N][N][10][10]; char a[N][N];bool used[N][N];pii to[N][N][4]; int dx[]={0,1,0,-1},dy[]={1,0,-1,0}; pii push(int x,int y,int d){ if(to[x][y][d]!=pii(0,0))return to[x][y][d]; to[x][y][d]=pii(-1,-1); int dd=d; if(a[x][y]=='C')dd=(d+1)%4; if(a[x][y]=='A')dd=(d+3)%4; int xx=x+dx[dd],yy=y+dy[dd]; if(xx>n||xx<1||yy>m||yy<1||a[xx][yy]=='x') return (to[x][y][d]=pii(x,y)); return (to[x][y][d]=push(xx,yy,dd)); } struct Pair_Queue{ int he,ta,t[N*N]; pii q[N*N],c[N*N]; inline void init(){memset(t,0,sizeof(t));} inline void reset(){he=1;ta=0;} inline bool empty(){return ta<he;} inline void push(int x,int y){q[++ta]=pii(x,y);} inline pii front(){return q[he];} inline void pop(){++he;} #define dis(k) f[(k).first][(k).second][L][R] inline void sort(){ int mn=INF,mx=0; for(int i=he,val;val=dis(q[i]),i<=ta;i++) c[i]=q[i],mn=min(mn,val),mx=max(mx,val),t[val]++; for(int i=mn+1;i<=mx;i++)t[i]+=t[i-1]; for(int i=he;i<=ta;i++)q[t[dis(c[i])]--]=c[i]; for(int i=mn;i<=mx;i++)t[i]=0; } #undef dis }Q1,Q2; void bfs(int l,int r){ Q1.reset();Q2.reset();L=l;R=r; for(int i=1;i<=n;++i) for(int j=1;j<=m;++j) if(f[i][j][l][r]<INF)Q1.push(i,j); if(Q1.empty())return; memset(used,0,sizeof(used));Q1.sort();pii u; #define dis(k) f[(k).first][(k).second][L][R] #define use(k) used[(k).first][(k).second] for(int x,y;!Q1.empty()||!Q2.empty();){ if(Q1.empty())u=Q2.front(),Q2.pop(); else if(Q2.empty())u=Q1.front(),Q1.pop(); else if(dis(Q1.front())<dis(Q2.front()))u=Q1.front(),Q1.pop(); else u=Q2.front(),Q2.pop(); x=u.first;y=u.second;used[x][y]=1; for(int d=0;d<4;d++){ int xx=to[x][y][d].first,yy=to[x][y][d].second; if(xx<1||xx>n||yy<1||yy>m||a[xx][yy]=='x')continue; if(f[x][y][l][r]+1<f[xx][yy][l][r]){ f[xx][yy][l][r]=f[x][y][l][r]+1; used[xx][yy]=1;Q2.push(xx,yy); } } while(!Q1.empty()&&use(Q1.front()))Q1.pop(); } } int main(){ memset(f,0x3f,sizeof(f));Q1.init();Q2.init(); scanf("%d%d%d",&T,&m,&n); for(int i=1;i<=n;++i)scanf("%s",a[i]+1); for(int i=1;i<=n;++i) for(int j=1;j<=m;++j) if(isdigit(a[i][j])){ int v=a[i][j]-'0'; f[i][j][v][v]=0; } for(int i=1;i<=n;++i) for(int j=1;j<=m;++j) for(int d=0;d<4;++d) if(a[i][j]!='x')to[i][j][d]=push(i,j,d); for(int len=1;len<=T;++len) for(int l=1,r;r=l+len-1,r<=T;++l){ for(int mid=l;mid<r;++mid) for(int i=1;i<=n;++i)for(int j=1;j<=m;++j) f[i][j][l][r]=min(f[i][j][l][r], f[i][j][l][mid]+f[i][j][mid+1][r]); bfs(l,r); } int ans=INF; for(int i=1;i<=n;++i) for(int j=1;j<=m;++j)ans=min(ans,f[i][j][1][T]); printf("%d\n",ans<INF?ans:-1); return 0; }
B 道路费用
给定 个点, 条边的图,点,边都有权值,边权互不相同。现新加入 条边,权值自定,要求最大化图中某一棵最小生成树的代价。这里代价定义为:每个点到 号点的路径上新加入边的权值和乘以点权,再对所有点求和。求这个最大代价。
,,。
tag:最小生成树,缩点
注意 很小。
先求原图的最小生成树。然后钦定 条边的权值为0,再求最小生成树。这样最小生成树其余至少 条边是必选的, 条边是可选的。对这些必选边建图缩点,图中至多剩下 个点。问题就可以转化为这样的小情况。
二进制枚举 条边中选用的边,对每种选择跑最小生成树。然后用上述剩下 条边中没有选入的边,找到树上包含它的环,更新环上边的最大权值即可。
难度Medium+。
点击查看代码
#include<bits/stdc++.h> using namespace std; const int N=1e5+5; int n,m,k,num,p[N],q[25][2],P[N],no[N]; int head[N],nxt[N<<1],ver[N<<1],tot=1; int b[25],c[25],d[25],f[25],g[25];long long ans,a[25],sz[25]; int find(int u){return (u==P[u]?u:(P[u]=find(P[u])));} struct edge{int u,v,w;}e[N*3]; void add(int u,int v){ ver[++tot]=v; nxt[tot]=head[u]; head[u]=tot; } bool operator <(const edge &a,const edge &b){return a.w<b.w;} void dfs(int u,int fa,int t){ no[u]=t;a[t]+=p[u]; for(int i=head[u];i;i=nxt[i]) if(ver[i]!=fa)dfs(ver[i],u,t); } void rdfs(int u){ sz[u]=a[u]; for(int i=head[u],v;i;i=nxt[i]){ v=ver[i]; if(v==f[u])continue; f[v]=u;d[v]=d[u]+1; rdfs(v);sz[u]+=sz[v]; } } int main(){ scanf("%d%d%d",&n,&m,&k); for(int i=1,u,v,w;i<=m;i++)scanf("%d%d%d",&e[i].u,&e[i].v,&e[i].w); for(int i=1;i<=k;i++)scanf("%d%d",&q[i][0],&q[i][1]); for(int i=1;i<=n;i++)scanf("%d",p+i); for(int i=1;i<=n;i++)P[i]=i; sort(e+1,e+m+1); for(int i=1,cnt=0;i<=m;i++){ int u=e[i].u,v=e[i].v; int fu=find(u),fv=find(v); if(fu==fv)continue; P[fu]=fv;e[++cnt]=e[i]; } for(int i=1;i<=n;i++)P[i]=i; for(int i=1;i<=k;i++){ int fu=find(q[i][0]),fv=find(q[i][1]); if(fu==fv)continue;P[fu]=fv; } for(int i=1;i<n;i++){ int fu=find(e[i].u),fv=find(e[i].v); if(fu==fv){e[++num]=e[i];continue;} add(e[i].u,e[i].v);add(e[i].v,e[i].u);P[fu]=fv; } for(int i=1,cnt=0;i<=n;i++)if(!no[i])dfs(i,-1,++cnt); for(int i=1;i<=num;i++)e[i].u=no[e[i].u],e[i].v=no[e[i].v]; for(int i=1;i<=k;i++)q[i][0]=no[q[i][0]],q[i][1]=no[q[i][1]]; for(int st=1;st<(1<<k);st++){ long long res=0; tot=1;for(int i=1;i<=num+1;i++)head[i]=0; for(int i=1;i<=k;i++)b[i]=(st>>i-1)&1; for(int i=1;i<=num+1;i++){P[i]=i;g[i]=1e9;} for(int i=1;i<=k;i++)if(b[i]){ int fu=find(q[i][0]),fv=find(q[i][1]); if(fu==fv){res=-1;break;} add(q[i][0],q[i][1]);add(q[i][1],q[i][0]);P[fu]=fv; } if(res==-1)continue; for(int i=1;i<=num;i++){ int fu=find(e[i].u),fv=find(e[i].v); if(fu==fv){c[i]=0;continue;} P[fu]=fv;c[i]=1; add(e[i].u,e[i].v);add(e[i].v,e[i].u); } rdfs(1); for(int i=1;i<=num;i++)if(!c[i]){ int u=e[i].u,v=e[i].v,w=e[i].w; if(d[u]<d[v])swap(u,v); while(d[u]>d[v]){g[u]=min(g[u],w);u=f[u];} while(u!=v){g[u]=min(g[u],w);g[v]=min(g[v],w);u=f[u];v=f[v];} } for(int i=1;i<=k;i++)if(b[i]){ int u=q[i][0],v=q[i][1]; if(d[u]>d[v])swap(u,v); res+=1ll*sz[v]*g[v]; } ans=max(ans,res); } printf("%lld\n",ans); return 0; }
C 出题人
提交答案题。
目标是造数据,输入有数字个数限制。循环超过 次算作超时。
Q1:Floyd,Heap-Dijkstra,BellmanFord 三种算法,任取两个(有序),卡掉一个而放另一个过。
Q2:求图的色数,目标算法是从小到大枚举并判断,要求放它过和卡掉它。点数 ,边数 。
tag:思维题
卡 Floyd 非常简单。
卡 BellmanFord 只要一堆自环,重边即可。
卡 Heap-Dijkstra 需要构造连续的三角形,利用负权边把它卡到指数级别。
Q2要卡掉随便构造都可以,要放过只要构造二分图。
难度Medium。
APIO2012
2023-03-29
A 派遣
个人的从属关系构成一棵树,第 个人的直接上司是 (, 表示i为根),每个人有两个参数:薪水 和领导力 。现在要确定一个管理者,选择其若干下属(直接或间接,可以不含管理者),使得选出的人得薪水总和不超过给定的预算 。在此条件下,最大化管理者的领导力与选出的人数之积。
,,,,。
tag:线段树合并 / 堆、启发式合并
先考虑固定管理者的情形。此时要最大化选出的人数,也就是要在这棵子树中从小到大依次取 ,使得总和不超过 。
这有两种方法维护:一是用线段树合并,二分查找,时间复杂度 。二是用优先队列启发式合并,只维护和不超过 的部分,多的删掉。此时每个元素至多删除一次,所以时间复杂度 。
实现了第一种。
难度Easy,场切。
点击查看代码
#include<bits/stdc++.h> using namespace std; typedef long long ll; typedef pair<ll,int> pli; pli operator +(const pli &a,const pli &b){ return make_pair(a.first+b.first,a.second+b.second); }; const int N=1e5+5; int n,a[N],b[N],num[N];ll m,c[N],ans; int head[N],nxt[N],ver[N],tot; void add(int u,int v){ ver[++tot]=v; nxt[tot]=head[u]; head[u]=tot; } int rt[N],segtot; ll sum[N*20];int cnt[N*20],lc[N*20],rc[N*20]; void modify(int p,int l,int r,int x){ if(l==r){sum[p]+=b[x];++cnt[p];return;} int mid=l+r>>1; if(x<=mid){ if(!lc[p])lc[p]=++segtot; modify(lc[p],l,mid,x); } else{ if(!rc[p])rc[p]=++segtot; modify(rc[p],mid+1,r,x); } sum[p]=sum[lc[p]]+sum[rc[p]]; cnt[p]=cnt[lc[p]]+cnt[rc[p]]; } pli query(int p,int l,int r,int L,int R){ if(!p)return make_pair(0,0); if(l>=L&&r<=R)return make_pair(sum[p],cnt[p]); int mid=l+r>>1;pli res=make_pair(0,0); if(L<=mid)res=res+query(lc[p],l,mid,L,R); if(R>mid)res=res+query(rc[p],mid+1,r,L,R); return res; } int merge(int p,int q){ if(!p)return q; if(!q)return p; sum[p]+=sum[q];cnt[p]+=cnt[q]; lc[p]=merge(lc[p],lc[q]); rc[p]=merge(rc[p],rc[q]); return p; } void dfs(int u){ rt[u]=++segtot; modify(rt[u],1,n,c[u]); for(int i=head[u],v;i;i=nxt[i]){ v=ver[i];dfs(v); rt[u]=merge(rt[u],rt[v]); } int l=1,r=n,res=0; while(l<=r){ int mid=l+r>>1; pli p=query(rt[u],1,n,1,mid); if(p.first<=m)l=mid+1,res=p.second; else r=mid-1; } ans=max(ans,1ll*res*a[u]); } int main(){ scanf("%d%lld",&n,&m); for(int i=1,fa;i<=n;i++){ scanf("%d%lld%d",&fa,c+i,a+i);b[i]=c[i]; if(fa)add(fa,i); } sort(b+1,b+n+1); for(int i=1;i<=n;i++){ int l=1,r=n,pos=0; while(l<=r){ int mid=l+r>>1; if(b[mid]<c[i])l=mid+1; else r=mid-1; if(b[mid]==c[i])pos=mid; } c[i]=pos+num[pos]; num[pos]++; } dfs(1); printf("%lld\n",ans); return 0; }
B 守卫
长为 的 数列中有 个 ,满足 个条件。每个条件形如 ,,,含义如下:
- 时,区间 中没有 。
- 时,区间 中有 。
保证初始有解。求所有必定为 的位置。
,,,。
tag:贪心
首先解决简单情况:
- 丢掉所有条件给出为 的位置。
- 如果剩下的可能位置数与总数相同,剩下的所有位置都满足条件。
3.如果有某个 的区间长为 ,这个位置满足条件。
4.如果有某两个 的区间有包含关系,只用考虑小的那个。
下面来考虑剩下的情形。对所有区间按左端点递增排序,则右端点也递增。
首先可以贪心求出一组解(不考虑 ),这只要从左到右扫描所有区间,如果某个区间还没有 ,在其右端点放 。同时可以求出满足前 个区间所需要的 的数目的最小值 。类似的,可以求出满足后 个区间所需要的 的数目的最小值 。
“某个位置必须是 ”等价于“某个位置是 时无解”。只用考虑那些满足 的区间 的右端点,记为 。则满足前 个区间的条件需要 个 。
再考虑所有完全在 右侧的区间,设为后 个。假设 不是 ,所以满足前 个区间时不会满足后 个区间中的任何一个。那么无解的充分必要条件就是 。
可以二分求。时间复杂度 。
难度Medium+。
点击查看代码
#include<bits/stdc++.h> using namespace std; const int N=1e5+5; int n,k,m,d[N],t,h[N],lst[N],nxt[N],cnt,st[N],top,f[N],g[N],flag; struct range{int l,r;}p[N]; bool operator <(const range &a,const range &b){ return a.l!=b.l?a.l<b.l:a.r<b.r; } int main(){ scanf("%d%d%d",&n,&k,&m); for(int i=1,a,b,c;i<=m;i++){ scanf("%d%d%d",&a,&b,&c); if(c==0)++d[a],--d[b+1]; else{++t;p[t].l=a;p[t].r=b;} } for(int i=1;i<=n;i++){ d[i+1]+=d[i]; if(!d[i]){lst[i]=nxt[i]=++cnt;h[cnt]=i;} } if(k==cnt){ for(int i=1;i<=cnt;i++)printf("%d\n",h[i]); printf("\n"); return 0; } for(int i=1;i<=n;i++)if(!lst[i])lst[i]=lst[i-1]; for(int i=n;i>=1;i--)if(!nxt[i])nxt[i]=nxt[i+1]; for(int i=1;i<=t;i++)p[i].l=nxt[p[i].l],p[i].r=lst[p[i].r]; sort(p+1,p+t+1);top=0; for(int i=1;i<=t;i++){ if(p[i].l>p[i].r)continue; while(top&&p[st[top]].r>=p[i].r)--top; st[++top]=i; }t=top; for(int i=1;i<=t;i++)p[i]=p[st[i]]; int mx=0,mn=1e9; for(int i=1;i<=t;i++){ if(p[i].l>mx)f[i]=f[i-1]+1,mx=p[i].r; else f[i]=f[i-1]; } for(int i=t;i>=1;i--){ if(p[i].r<mn)g[i]=g[i+1]+1,mn=p[i].l; else g[i]=g[i+1]; } for(int i=1;i<=t;i++){ if(p[i].l==p[i].r){flag=1;printf("%d\n",h[p[i].l]);continue;} if(f[i]!=f[i-1]+1)continue; int pos=t+1,l=i+1,r=t; while(l<=r){ int mid=l+r>>1; if(p[mid].l>p[i].r-1)pos=mid,r=mid-1; else l=mid+1; } if(f[i]+g[pos]>k){flag=1;printf("%d\n",h[p[i].r]);} } if(!flag)printf("-1\n"); return 0; }
C 苦无
的表格中,有 个质点从某方格中心同时开始运动。没有两个质点初始时在同一方格中。所有质点以同样的速度做直线运动,方向为上、下、左、右之一。如果两个或多个质点在同一时刻位于同一位置,则这些质点相撞并消失。求被质点经过的格子数。
这里每个质点用三个参数 ,, 来描述,表示其开始运动的位置是从左往右的 列、从上往下的第 行的方格的中心。运动的方向由 表示,分别为:
- ,表示向右;
- ,表示向上;
- ,表示向左;
- ,表示向下。
,,;,,。
tag:模拟、堆、扫描线
只用求出每个质点的运动路程,然后看作宽为 的矩形做扫描线即可。
求运动路程,就只需要求相撞的情况。相撞质点的方向有 种可能,如同一行,左边的向右,右边的向左;再如同一条左上—右下对角线,左边的向下,右边的向左;等等。
在每一种可能中,最先相撞的一定是两个相邻的点。一旦遇到两个相撞,就要删去这两个点,考虑新产生的相邻对,于是使用链表。
按照 种情形, 个方向排序,排序后构建链表。然后将所有相撞按时间放入一个堆中,每次取出某个时刻所有的相撞,判断是否存在(可能已经撞过)。如果是存在的相撞,就更新链表和相撞。
细节:
- 排序的时候要先按照方向分开。
- 可以假设质点每秒运动半个方格边长,这样所有的时间都是整数。
难度Medium+。
点击查看代码
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int N=2e5+5,INF=1<<30; int n,W,H,X[N<<1];ll ans; struct P{int no,x,y,d,nxt[6],lst[6],t,ex;}a[N]; int fir[]={0,3,0,3,0,1},sec[]={2,1,1,2,3,2},dx[]={1,0,-1,0},dy[]={0,-1,0,1}; bool cmp(P a,P b){return a.no<b.no;} bool cmp1(P a,P b){ if((a.d==0||a.d==2)&&(b.d==1||b.d==3))return true; if((b.d==0||b.d==2)&&(a.d==1||a.d==3))return false; return a.y!=b.y?a.y<b.y:a.x<b.x; } bool cmp2(P a,P b){ if((a.d==1||a.d==3)&&(b.d==0||b.d==2))return true; if((b.d==1||b.d==3)&&(a.d==0||a.d==2))return false; return a.x!=b.x?a.x<b.x:a.y<b.y; } bool cmp3(P a,P b){ if((a.d==0||a.d==1)&&(b.d==2||b.d==3))return true; if((b.d==0||b.d==1)&&(a.d==2||a.d==3))return false; return a.x-a.y!=b.x-b.y?a.x-a.y<b.x-b.y:a.x<b.x; } bool cmp4(P a,P b){ if((a.d==0||a.d==3)&&(b.d==1||b.d==2))return true; if((b.d==0||b.d==3)&&(a.d==1||a.d==2))return false; return a.x+a.y!=b.x+b.y?a.x+a.y<b.x+b.y:a.x<b.x; } struct hit{int t,p,q;}; bool operator <(const hit &a,const hit &b){return a.t>b.t;} priority_queue<hit> Q; struct ScanLine{ ll l,r,h;int op; bool operator <(const ScanLine &b){ return h<b.h; } }line[N<<1]; struct SegmentTree{ int ls[N<<3],rs[N<<3],sum[N<<3];ll len[N<<3]; #define lc p<<1 #define rc p<<1|1 void push_up(int p){ if(sum[p])len[p]=X[rs[p]+1]-X[ls[p]]; else len[p]=len[lc]+len[rc]; } void build(int p,int l,int r){ ls[p]=l;rs[p]=r;len[p]=sum[p]=0; if(l==r)return; build(lc,l,l+r>>1); build(rc,(l+r>>1)+1,r); } void modify(int p,int L,int R,int c){ if(X[rs[p]+1]<=L||R<=X[ls[p]])return; if(L<=X[ls[p]]&&X[rs[p]+1]<=R){sum[p]+=c;push_up(p);return;} modify(lc,L,R,c);modify(rc,L,R,c); push_up(p); } }seg; int main(){ //freopen("kunai.in","r",stdin); //freopen("kunai.out","w",stdout); scanf("%d%d%d",&W,&H,&n); for(int i=1;i<=n;i++){ scanf("%d%d%d",&a[i].x,&a[i].y,&a[i].d); a[i].ex=1;a[i].no=i; for(int k=0;k<6;k++)a[i].nxt[k]=a[i].lst[k]=-1; switch(a[i].d){ case 0:a[i].t=2*(W-a[i].x);break; case 1:a[i].t=2*(a[i].y-1);break; case 2:a[i].t=2*(a[i].x-1);break; case 3:a[i].t=2*(H-a[i].y);break; } } a[n+1].d=2;a[0].d=0; sort(a+1,a+n+1,cmp1); for(int i=1;i<n;i++){ if(a[i].d==1||a[i].d==3)break; if(a[i].y==a[i+1].y)a[i].nxt[0]=a[i+1].no,a[i+1].lst[0]=a[i].no; } sort(a+1,a+n+1,cmp2); for(int i=1;i<n;i++){ if(a[i].d==0||a[i].d==2)break; if(a[i].x==a[i+1].x)a[i].nxt[1]=a[i+1].no,a[i+1].lst[1]=a[i].no; } sort(a+1,a+n+1,cmp3); for(int i=0,op=2;i<n;i++){ if((a[i].d==0||a[i].d==1)&&(a[i+1].d==2||a[i+1].d==3)){op=3;continue;} if(i!=0&&a[i].x-a[i].y==a[i+1].x-a[i+1].y) a[i].nxt[op]=a[i+1].no,a[i+1].lst[op]=a[i].no; } sort(a+1,a+n+1,cmp4); for(int i=0,op=4;i<n;i++){ if((a[i].d==0||a[i].d==3)&&(a[i+1].d==1||a[i+1].d==2)){op=5;continue;} if(i!=0&&a[i].x+a[i].y==a[i+1].x+a[i+1].y) a[i].nxt[op]=a[i+1].no,a[i+1].lst[op]=a[i].no; } sort(a+1,a+n+1,cmp); for(int k=0;k<6;k++) for(int i=1;i<=n;i++) if(a[i].nxt[k]!=-1&&a[i].d==fir[k]&&a[a[i].nxt[k]].d==sec[k]) Q.push({abs(a[i].x-a[a[i].nxt[k]].x)+abs(a[i].y-a[a[i].nxt[k]].y),i,a[i].nxt[k]}); while(!Q.empty()){ int t0=Q.top().t; while(!Q.empty()&&Q.top().t==t0){ hit tmp=Q.top();int i=tmp.p,j=tmp.q;Q.pop(); if(!a[i].ex&&a[i].t!=t0||!a[j].ex&&a[j].t!=t0)continue; if(a[i].ex){ for(int k=0;k<6;k++){ int LS=a[i].lst[k],NX=a[i].nxt[k]; if(LS!=-1)a[LS].nxt[k]=NX; if(NX!=-1)a[NX].lst[k]=LS; if(LS!=-1&&NX!=-1&&a[LS].d==fir[k]&&a[NX].d==sec[k]) Q.push({abs(a[LS].x-a[NX].x)+abs(a[LS].y-a[NX].y),LS,NX}); } } if(a[j].ex){ for(int k=0;k<6;k++){ int LS=a[j].lst[k],NX=a[j].nxt[k]; if(LS!=-1)a[LS].nxt[k]=NX; if(NX!=-1)a[NX].lst[k]=LS; if(LS!=-1&&NX!=-1&&a[LS].d==fir[k]&&a[NX].d==sec[k]) Q.push({abs(a[LS].x-a[NX].x)+abs(a[LS].y-a[NX].y),LS,NX}); } } a[i].t=a[j].t=t0;a[i].ex=a[j].ex=0; } } for(int i=1;i<=n;i++){ int dist=a[i].t,X1,Y1,X2,Y2; if(dist%2==0)dist/=2; switch(a[i].d){ case 0:{X1=a[i].x;X2=a[i].x+dist+1;Y1=a[i].y;Y2=a[i].y+1;break;} case 1:{X1=a[i].x;X2=a[i].x+1;Y1=a[i].y-dist;Y2=a[i].y+1;break;} case 2:{X1=a[i].x-dist;X2=a[i].x+1;Y1=a[i].y;Y2=a[i].y+1;break;} case 3:{X1=a[i].x;X2=a[i].x+1;Y1=a[i].y;Y2=a[i].y+dist+1;break;} } X[2*i-1]=X1;X[2*i]=X2; line[2*i-1]={X1,X2,Y1,1};line[2*i]={X1,X2,Y2,-1}; } sort(line+1,line+2*n+1); sort(X+1,X+2*n+1); int tot=unique(X+1,X+2*n+1)-X-1; seg.build(1,1,tot-1); for(int i=1;i<2*n;i++){ seg.modify(1,line[i].l,line[i].r,line[i].op); ans+=seg.len[1]*(line[i+1].h-line[i].h); } printf("%lld\n",ans); return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具