2022“杭电杯”中国大学生算法设计超级联赛(3)
A. Equipment Upgrade
设$E_i$表示从等级$i$强化至等级$n$的期望花费,则有
\begin{eqnarray*}
E_n&=&0\\
E_i&=&c_i+p_i\cdot E_{i+1}+\frac{\left(1-p_i\right)}{\sum_{j=1}^i w_j}\cdot\left(\sum_{j=1}^i w_j\cdot E_{i-j}\right)\\
p_i\cdot E_{i+1}&=&E_i-c_i-\frac{\left(1-p_i\right)}{\sum_{j=1}^i w_j}\cdot\left(\sum_{j=1}^i w_j\cdot E_{i-j}\right)\\
E_{i+1}&=&\frac{E_i-c_i-\frac{\left(1-p_i\right)}{\sum_{j=1}^i w_j}\cdot\left(\sum_{j=1}^i w_j\cdot E_{i-j}\right)}{p_i}
\end{eqnarray*}
根据上式,如果已知$E_0,E_1,\dots,E_i$,即可计算出$E_{i+1}$的值。但是由于$E_0$未知,我们无法从前往后进行递推。在这里,设$E_i=a_i\cdot E_0+b_i$,则
\begin{eqnarray*}
a_0&=&1\\
b_0&=&0\\
a_{i+1}&=&\frac{a_i-\frac{\left(1-p_i\right)}{\sum_{j=1}^i w_j}\cdot\left(\sum_{j=1}^i w_j\cdot a_{i-j}\right)}{p_i}\\
b_{i+1}&=&\frac{b_i-c_i-\frac{\left(1-p_i\right)}{\sum_{j=1}^i w_j}\cdot\left(\sum_{j=1}^i w_j\cdot b_{i-j}\right)}{p_i}
\end{eqnarray*}
此时$a$和$b$都是可以从前往后递推计算的,且计算过程是卷积的形式,可以用分治+NTT求出,最后根据$a_n\cdot E_0+b_n=E_n=0$解出$E_0$的值即为答案。
时间复杂度$O(n\log^2n)$。
#include<cstdio> #include<algorithm> using namespace std; typedef long long ll; const int N=262144,K=17,P=998244353,G=3; int Case,n,i,x; int pos[N+5],A[N+5],B[N+5],W[N+5],g[K+5],ng[K+5],inv[N+5],inv2; int p[N+5],invp[N+5],cost[N+5],w[N+5],pre[N+5]; int ea[N+5],eb[N+5],fa[N+5],fb[N+5]; inline int po(int a,int b){int t=1;for(;b;b>>=1,a=1LL*a*a%P)if(b&1)t=1LL*t*a%P;return t;} inline void NTT(int*a,int n,int t){ for(int i=1;i<n;i++)if(i<pos[i])swap(a[i],a[pos[i]]); for(int d=0;(1<<d)<n;d++){ int m=1<<d,m2=m<<1,_w=t==1?g[d]:ng[d]; for(int i=0;i<n;i+=m2)for(int w=1,j=0;j<m;j++){ int&A=a[i+j+m],&B=a[i+j],t=1LL*w*A%P; A=B-t;if(A<0)A+=P; B=B+t;if(B>=P)B-=P; w=1LL*w*_w%P; } } if(t==-1)for(int i=0,j=inv[n];i<n;i++)a[i]=1LL*a[i]*j%P; } void solve(int l,int r){ if(l==r){ if(l){ ea[l+1]=((ea[l]-(1LL-p[l])*fa[l]%P*pre[l])%P+P)*invp[l]%P; eb[l+1]=((eb[l]-cost[l]-(1LL-p[l])*fb[l]%P*pre[l])%P+P)*invp[l]%P; } return; } int mid=(l+r)>>1; solve(l,mid); int k=1; while(k<r-l+1)k<<=1; for(int i=0;i<k;i++)A[i]=B[i]=W[i]=0; for(int i=l;i<=mid;i++)A[i-l]=ea[i],B[i-l]=eb[i]; for(int i=1;i<=r-l;i++)W[i]=w[i]; int j=__builtin_ctz(k)-1; for(int i=0;i<k;i++)pos[i]=pos[i>>1]>>1|((i&1)<<j); NTT(A,k,1); NTT(B,k,1); NTT(W,k,1); for(int i=0;i<k;i++)A[i]=1LL*A[i]*W[i]%P,B[i]=1LL*B[i]*W[i]%P; NTT(A,k,-1); NTT(B,k,-1); for(int i=mid+1;i<=r;i++){ fa[i]=(fa[i]+A[i-l])%P; fb[i]=(fb[i]+B[i-l])%P; } solve(mid+1,r); } int main(){ for(g[K]=po(G,(P-1)/N),ng[K]=po(g[K],P-2),i=K-1;~i;i--)g[i]=1LL*g[i+1]*g[i+1]%P,ng[i]=1LL*ng[i+1]*ng[i+1]%P; for(inv[1]=1,i=2;i<=N;i++)inv[i]=1LL*(P-inv[P%i])*(P/i)%P;inv2=inv[2]; scanf("%d",&Case); while(Case--){ scanf("%d",&n); for(i=0;i<n;i++){ scanf("%d%d",&x,&cost[i]); p[i]=1LL*x*inv[100]%P; invp[i]=100LL*inv[x]%P; } for(i=1;i<n;i++){ scanf("%d",&w[i]); pre[i]=(pre[i-1]+w[i])%P; } for(i=1;i<n;i++)pre[i]=po(pre[i],P-2); for(i=0;i<=n;i++)fa[i]=fb[i]=0; ea[0]=1,eb[0]=0; ea[1]=1,eb[1]=(P-cost[0])%P; solve(0,n-1); int ans=1LL*(P-eb[n])*po(ea[n],P-2)%P; printf("%d\n",(ans+P)%P); } }
B. Boss Rush
二分答案,转化为判断$T$帧内能否打败BOSS,即求出$T$帧内能打出的最高伤害,判断是否大于等于$H$。
从前往后依次发动若干个技能,则下一个技能可以发动的时刻等于之前发动过的技能的演出时间之和,因此只和之前发动过哪些技能有关。设$f_{S}$表示发动了$S$ 集合的技能,在$T$帧内最多能结算多少伤害,枚举不在$S$中的某个技能$x$作为下一个技能进行转移,由于技能发动时刻已知,因此可以$O(1)$计算出在$T$帧内下一个技能可以结算多少伤害。
时间复杂度$O(n2^n\log ans)$。
#include<cstdio> typedef long long ll; const int N=18,M=100005; int Case,n,i,j,S,t[N],d[N],l,r,ans,mid,sum[(1<<N)+1]; ll hp,f[(1<<N)+1],dmg[N][M]; inline void up(ll&a,ll b){a<b?(a=b):0;} bool check(int T){ int S,i; for(S=0;S<1<<n;S++)f[S]=-1; f[0]=0; for(S=0;S<1<<n;S++){ ll w=f[S]; if(w<0)continue; if(w>=hp)return 1; int cur=sum[S]; if(cur>T)continue; for(i=0;i<n;i++)if(!(S>>i&1)){ if(cur+d[i]-1<=T)up(f[S|(1<<i)],w+dmg[i][d[i]-1]); else up(f[S|(1<<i)],w+dmg[i][T-cur]); } } return 0; } int main(){ scanf("%d",&Case); while(Case--){ scanf("%d%lld",&n,&hp); ans=-1,l=r=0; for(i=0;i<n;i++){ scanf("%d%d",&t[i],&d[i]); r+=t[i]+d[i]-1; for(j=0;j<d[i];j++)scanf("%lld",&dmg[i][j]); for(j=1;j<d[i];j++)dmg[i][j]+=dmg[i][j-1]; } for(S=1;S<1<<n;S++)sum[S]=sum[S-(S&-S)]+t[__builtin_ctz(S&-S)]; while(l<=r){ mid=(l+r)>>1; if(check(mid))r=(ans=mid)-1;else l=mid+1; } printf("%d\n",ans); } }
C. Cyber Language
签到模拟,遍历每个字符,如果一个字符是小写字母且前一个字符是空格或者它是第一个字符,那么把它转大写输出。
#include<cstdio> #include<cstring> const int N=1000005; int Case,n,i;char s[N]; int main(){ fgets(s,N,stdin); sscanf(s,"%d",&Case); while(Case--){ fgets(s,N,stdin); n=strlen(s); for(i=0;i<n;i++) if(s[i]>='a'&&s[i]<='z') if(!i||(i>0&&s[i-1]==' ')) putchar(s[i]-'a'+'A'); puts(""); } }
D. Divide the Sweets
令$sum_S$表示$S$集合的箱子的糖果数之和,$f_{i,S}$表示把$S$集合的箱子分给$i$个孩子的最小平方和,则$f_{i,S}=\min\left(f_{i-1,T}+(sum_S-sum_T)^2\right)$,其中$T$是$S$的子集。直接枚举子集转移的时间复杂度为$O(m3^n)$,不能接受。
方便起见,令$g_S=f_{i,S},h_S=f_{i-1,S}$,则
\begin{eqnarray*}
g_S&=&\min\left(h_T+\left(sum_S-sum_T\right)^2\right)\\
&=&\min\left(h_T+sum_S^2+sum_T^2-2\cdot sum_S\cdot sum_T\right)\\
&=&\min\left((h_T+sum_T^2)-2\cdot sum_S\cdot sum_T\right)+sum_S^2
\end{eqnarray*}
将表示集合的$n$位二进制数拆成前$\frac{n}{2}$位和后$\frac{n}{2}$位来考虑。枚举转移中$T$的前$\frac{n}{2}$位$A$,再枚举$A$的所有超集$A'$作为$S$的前半部分,然后枚举$S$的后$\frac{n}{2}$位$B'$,此时$S=A'B'$,我们需要找到一个$B'$的子集$B$作为$T$的后半部分(即$T=AB$),满足$(h_T+sum_T^2)-2\cdot sum_S\cdot sum_T$最小。这是经典斜率优化的形式,对于每个$T$,将其看作直线$y=sum_T\cdot x+(h_T+sum_T^2)$,若能得到$B'$子集的所有直线形成的凸壳,则在凸壳上询问$x=-2sum_S$时$y$的最小值即可完成状态转移。假设已经有了凸壳,为了保证询问的均摊复杂度为$O(1)$,需要按照$sum_S$递增(或递减)的顺序去询问,由于$sum_S=sum_{A'}+sum_{B'}$,所有的$S$对应的$B'$都相等,因此按照$sum_{A'}$递增(或递减)的顺序去枚举$A'$即可保证询问$x$坐标的单调性。
剩下的问题是如何得到$B'$子集的凸壳。类似于高维前缀和,对于每个状态$B$保存其凸壳。一开始对于每个状态$B$,它的凸壳大小为$1$,对应$T=AB$。接下来依次枚举后半部分的每一位,假设当前枚举到了第$x$位,对于一个集合$B$,如果它的第$x$位为$1$,则它缺少 B xor (1<<x) 的信息,暴力将 B xor (1<<x) 和$B$的凸壳二路归并成一个,作为$B$的新凸壳。最坏情况下,每次合并后凸壳大小翻倍,总代价为最终凸壳大小的两倍,所有状态的凸壳大小不超过子集数,因此预处理子集凸壳的时间复杂度为$O(3^{\frac{n}{2}})$。
总时间复杂度分析:一共要进行$O(m)$轮,每轮需要枚举$O(2^{\frac{n}{2}})$个$A$,然后枚举它的超集$A'$,总计$O(3^{\frac{n}{2}})$,每个$A'$又需要接着枚举$O(2^{\frac{n}{2}})$个后半部分$B'$,然后花费均摊$O(1)$的代价在子集的凸壳上进行询问,这部分每轮的总时间复杂度为$O(3^{\frac{n}{2}}\cdot 2^{\frac{n}{2}})=O(6^{\frac{n}{2}})$。而枚举完$A$后,又需要支付$O(3^{\frac{n}{2}})$的代价预处理出所有子集的凸壳,这部分每轮的总时间复杂度也为$O(6^{\frac{n}{2}})$。因此,最终得到总时间复杂度为$O(m6^{\frac{n}{2}})$。
#include<cstdio> #include<algorithm> #include<vector> using namespace std; typedef long long ll; const int N=20,M=1<<N,K=1<<(N/2),P=998244353; const ll inf=1LL<<60; int Case,n,m,fir,sec,cq,i,j,S,T,A,B,a[N],cnt[M+1],sum[M+1],ans[N+1],q[K+1]; ll pre[M+1],cur[M+1],base[M+1]; int st[K+1],en[K+1],head,tail; struct E{int k;ll b;E(){}E(int _k,ll _b){k=_k,b=_b;}}h[120005]; inline bool cmp(int x,int y){return sum[x]<sum[y];} inline void up(ll&a,ll b){a>b?(a=b):0;} inline void insert(const E&e){ while(head<tail&&(h[tail-1].b-h[tail].b)*(e.k-h[tail].k)<=(h[tail].b-e.b)*(h[tail].k-h[tail-1].k))tail--; h[++tail]=e; } inline void merge(int A,int B,int C,int D){ head=tail+1; while(A<=B&&C<=D){ if(h[A].k==h[C].k){ insert(h[A].b<h[C].b?h[A]:h[C]); A++,C++; continue; } insert(h[A].k<h[C].k?h[A++]:h[C++]); } while(A<=B)insert(h[A++]); while(C<=D)insert(h[C++]); } int main(){ scanf("%d",&Case); while(Case--){ scanf("%d%d",&n,&m); for(i=0;i<n;i++)scanf("%d",&a[i]); for(S=1;S<1<<n;S++){ T=S&-S; cnt[S]=cnt[S-T]+1; i=__builtin_ctz(T); sum[S]=sum[S-T]+a[i]; base[S]=base[S-T]+1LL*a[i]*a[i]; } for(i=1;i<=n;i++)ans[i]=0; cur[0]=inf; for(S=1;S<1<<n;S++){ cur[S]=1LL*sum[S]*sum[S]; ans[1]=(ans[1]+cur[S])%P; } fir=n/2; sec=n-fir; for(i=2;i<=m;i++){ for(S=1;S<1<<n;S++){ pre[S]=cur[S]; cur[S]=cnt[S]<=i?base[S]:inf; } for(A=0;A<1<<fir;A++){ cq=0; for(T=A;;T=(T+1)|A){ q[cq++]=T<<sec; if(T==(1<<fir)-1)break; } sort(q,q+cq,cmp); tail=0; for(B=0;B<1<<sec;B++){ st[B]=tail+1; S=A<<sec|B; if(cnt[S]>=i-1)h[++tail]=E(sum[S],pre[S]+1LL*sum[S]*sum[S]); en[B]=tail; } for(j=0;j<sec;j++)for(B=0;B<1<<sec;B++)if(B>>j&1){ int l=tail+1; merge(st[B],en[B],st[B^(1<<j)],en[B^(1<<j)]); st[B]=l; en[B]=tail; } for(B=0;B<1<<sec;B++){ int l=st[B],r=en[B]; if(l>r)continue; for(j=0;j<cq;j++){ S=q[j]|B; if(cnt[S]<=i)continue; ll x=-2*sum[S]; while(l<r&&h[l].k*x+h[l].b>h[l+1].k*x+h[l+1].b)l++; up(cur[S],h[l].k*x+h[l].b); } } } for(S=1;S<1<<n;S++){ if(cnt[S]>i)cur[S]+=1LL*sum[S]*sum[S]; ans[i]=(ans[i]+cur[S])%P; } } for(i=1;i<=m;i++)printf("%d\n",ans[i]); } }
E. Spanning Tree Game
题意即对于每个$k$ ($0\leq k\leq m$),从数组$a$中选取$k$个边权,从数组$b$中选取$m-k$个边权,并最大化最小生成树的边权和。
对于一条边$(u,v,a,b)$,有以下两种情况:
- $a<b$:拆成两条边$(u,v,a,1)$和$(u,v,b,0)$。
- $a\geq b$:拆成两条边$(u,v,b,-1)$和$(u,v,a,0)$。
根据最小生成树的Kruskal算法,将拆后的$2m$条边按边权从小到大排序,边权相同时,将类型($-1$或$0$或$1$)为$0$的边排在后面,从前往后依次考虑每条边:
- 若是$(u,v,w,1)$,则这条边可选可不选,若选了则$a$数组中选取的边数要增加$1$,此时若$u$和$v$不连通,则MST的边权和要增加$w$。
- 若是$(u,v,w,-1)$,则这条边可选可不选,若选了则$a$数组中选取的边数要减少$1$,此时若$u$和$v$不连通,则MST的边权和要增加$w$。
- 若是$(u,v,w,0)$,则这条边对应的另一条边在此之前已经考虑过。若另一条边选了,则这条边选上不会对MST产生多余的错误贡献;若另一条边没选,则这条边必须要选。因此,此时如果判断出$u$和$v$不连通,则MST的边权和要增加$w$。
上述过程中每条边可能会有两种选择,使得整个图中$n$个点的连通情况不同,由此设计出状态$f_{i,S,j}$表示考虑了排序后前$i$条边,$n$个点连通性的最小表示为$S$,有$j$条边边权来自数组$a$时,最小生成树边权和的最大值是多少。预处理转移后可以做到$O(1)$转移。
状态数分析:$i$和$j$都是$O(m)$个,$S$有$Bell(n)$个,其中$Bell(9)=21147$。
时间复杂度$O(m^2Bell(n))$。
#include<cstdio> #include<map> #include<algorithm> using namespace std; typedef unsigned long long ull; const int N=15,M=35,K=21155; int Case,n,m,cnt,ce,base,i,j,k,f[N],v[N],pre[K][M],cur[K][M]; ull pool[K]; map<ull,int>T; struct E{int x,y,w,t;E(){}E(int _x,int _y,int _w,int _t){x=_x,y=_y,w=_w,t=_t;}}e[M*2]; inline bool cmp(const E&a,const E&b){ if(a.w!=b.w)return a.w<b.w; return (!a.t)<(!b.t); } void dfs(int x,int y){ if(x>n){ ull S=0; for(int i=1;i<=n;i++)S=S<<4|f[i]; T[S]=++cnt; pool[cnt]=S; return; } for(int i=1;i<=y+1;i++){ f[x]=i; dfs(x+1,i>y?i:y); } } inline void up(int&a,int b){a<b?(a=b):0;} inline void clr(){for(int i=1;i<=cnt;i++)for(int j=0;j<=m;j++)cur[i][j]=-1;} inline void nxt(){for(int i=1;i<=cnt;i++)for(int j=0;j<=m;j++)pre[i][j]=cur[i][j];} int main(){ scanf("%d",&Case); while(Case--){ scanf("%d%d",&n,&m); ce=cnt=base=0; T.clear(); for(i=1;i<=m;i++){ int x,y,a,b; scanf("%d%d%d%d",&x,&y,&a,&b); if(a<b){ e[++ce]=E(x,y,a,1); e[++ce]=E(x,y,b,0); }else{ base++; e[++ce]=E(x,y,b,-1); e[++ce]=E(x,y,a,0); } } sort(e+1,e+ce+1,cmp); dfs(1,0); clr(); cur[cnt][base]=0; for(i=1;i<=ce;i++){ int x,y,w,t; x=e[i].x,y=e[i].y,w=e[i].w,t=e[i].t; nxt(); clr(); for(j=1;j<=cnt;j++){ ull S=pool[j]; for(k=n;k;k--){ f[k]=S&15; S>>=4; } int o=j,A=f[x],B=f[y],tmp=A==B?0:w; if(A!=B){ int A=f[x],B=f[y]; for(k=1;k<=n;k++)if(f[k]==A)f[k]=B; int now=0; for(k=1;k<=n;k++)v[k]=0; for(k=1;k<=n;k++)if(!v[f[k]])v[f[k]]=++now; S=0; for(k=1;k<=n;k++)S=S<<4|v[f[k]]; o=T[S]; } if(t==0){ for(k=0;k<=m;k++)if(~pre[j][k])up(cur[o][k],pre[j][k]+tmp); }else{ for(k=0;k<=m;k++)if(~pre[j][k])up(cur[j][k],pre[j][k]); if(t>0){ for(k=0;k<m;k++)if(~pre[j][k])up(cur[o][k+1],pre[j][k]+tmp); }else{ for(k=1;k<=m;k++)if(~pre[j][k])up(cur[o][k-1],pre[j][k]+tmp); } } } } for(i=0;i<=m;i++)printf("%d\n",cur[1][i]); } }
F. Dusk Moon
对于一个点集,它的凸包覆盖住了所有点,且最小覆盖圆覆盖住了凸包,因此仅保留凸包的顶点不会影响答案。
由于点的坐标在给定的正方形范围内随机,因此一个点集的凸包的期望顶点数为$O(\log n)$,使用线段树直接记录区间凸包点集,然后对于$O(\log n)$个点运行最小圆覆盖算法即可。
时间复杂度$O(q\log^2n)$。
#include<cstdio> #include<cmath> #include<cstdlib> #include<algorithm> using namespace std; typedef long double ld; const int N=100010,M=262150,K=35; const ld eps=1e-10; int Case,n,m,i,op,x,y,q[N],h[N],vf[M][K],vg[M][K],F[K],G[K]; struct P{int x,y;}a[N]; struct E{ld x,y;E(){}E(ld _x,ld _y){x=_x,y=_y;}}; inline ld dis(const E&a,const E&b){return sqrtl((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y));} inline E center(const E&x,const E&y,const E&z){ ld a1=y.x-x.x,b1=y.y-x.y, c1=(a1*a1+b1*b1)/2,a2=z.x-x.x, b2=z.y-x.y,c2=(a2*a2+b2*b2)/2, d=a1*b2-a2*b1; return E(x.x+(c1*b2-c2*b1)/d,x.y+(a1*c2-a2*c1)/d); } inline bool cmp0(int x,int y){ if(a[x].x!=a[y].x)return a[x].x<a[y].x; return a[x].y>a[y].y; } inline bool cmp1(int x,int y){ if(a[x].x!=a[y].x)return a[x].x<a[y].x; return a[x].y<a[y].y; } inline void merge0(int*A,int*B,int*C){ int cnt=0,i=0,j=0,t=0; while(A[i]&&B[j])q[cnt++]=cmp0(A[i],B[j])?A[i++]:B[j++]; while(A[i])q[cnt++]=A[i++]; while(B[j])q[cnt++]=B[j++]; for(i=0;i<cnt;i++){ j=q[i]; if(i&&a[j].x==a[q[i-1]].x)continue; while(t>1&&1LL*(a[h[t]].y-a[h[t-1]].y)*(a[j].x-a[h[t]].x)<=1LL*(a[j].y-a[h[t]].y)*(a[h[t]].x-a[h[t-1]].x))t--; h[++t]=j; } for(i=1;i<=t;i++)C[i-1]=h[i]; C[t]=0; } inline void merge1(int*A,int*B,int*C){ int cnt=0,i=0,j=0,t=0; while(A[i]&&B[j])q[cnt++]=cmp1(A[i],B[j])?A[i++]:B[j++]; while(A[i])q[cnt++]=A[i++]; while(B[j])q[cnt++]=B[j++]; for(i=0;i<cnt;i++){ j=q[i]; if(i&&a[j].x==a[q[i-1]].x)continue; while(t>1&&1LL*(a[h[t]].y-a[h[t-1]].y)*(a[j].x-a[h[t]].x)>=1LL*(a[j].y-a[h[t]].y)*(a[h[t]].x-a[h[t-1]].x))t--; h[++t]=j; } for(i=1;i<=t;i++)C[i-1]=h[i]; C[t]=0; } inline void up(int x){ merge0(vf[x<<1],vf[x<<1|1],vf[x]); merge1(vg[x<<1],vg[x<<1|1],vg[x]); } void build(int x,int a,int b){ if(a==b){ vf[x][0]=vg[x][0]=a; vf[x][1]=vg[x][1]=0; return; } int mid=(a+b)>>1; build(x<<1,a,mid),build(x<<1|1,mid+1,b); up(x); } void change(int x,int a,int b,int c){ if(a==b)return; int mid=(a+b)>>1; if(c<=mid)change(x<<1,a,mid,c);else change(x<<1|1,mid+1,b,c); up(x); } void ask(int x,int a,int b,int c,int d){ if(c<=a&&b<=d){ merge0(F,vf[x],F); merge1(G,vg[x],G); return; } int mid=(a+b)>>1; if(c<=mid)ask(x<<1,a,mid,c,d); if(d>mid)ask(x<<1|1,mid+1,b,c,d); } inline int cal(){ int n=0,i,j,k; static E b[K*2]; for(i=0;F[i];i++)b[n++]=E(a[F[i]].x,a[F[i]].y); for(i=0;G[i];i++)b[n++]=E(a[G[i]].x,a[G[i]].y); random_shuffle(b,b+n); E O=b[0]; ld R=0; for(i=1;i<n;i++)if(dis(b[i],O)>R+eps) for(O=b[i],R=0,j=0;j<i;j++)if(dis(b[j],O)>R+eps){ O=E((b[i].x+b[j].x)/2,(b[i].y+b[j].y)/2); R=dis(O,b[i]); for(k=0;k<j;k++)if(dis(b[k],O)>R+eps)O=center(b[k],b[j],b[i]),R=dis(O,b[i]); } return ceil(R); } int main(){ scanf("%d",&Case); while(Case--){ scanf("%d%d",&n,&m); for(i=1;i<=n;i++)scanf("%d%d",&a[i].x,&a[i].y); build(1,1,n); while(m--){ scanf("%d",&op); if(op==1){ scanf("%d",&x); scanf("%d%d",&a[x].x,&a[x].y); change(1,1,n,x); }else{ scanf("%d%d",&x,&y); F[0]=G[0]=0; ask(1,1,n,x,y); printf("%d\n",cal()); } } } }
G. Shallow Moon
按$w$行为一块,将$m\times m$的矩阵从上到下分为$\lceil\frac{m}{w}\rceil$个$w\times m$的块。其中最后一块可能不足$w$行,方便起见下面都忽略这种情况。
由于每块的行数和每个矩形障碍的行数相等,因此每个矩形障碍要么完全属于某一块,要么经过相邻的两块。一个矩形障碍对于某一块的影响只能是下列两种情况之一:
- 块内第$b$列至第$b+h-1$列的前若干行是障碍。
- 块内第$b$列至第$b+h-1$列的后若干行是障碍。
对于某块中固定的某一列,只需要考虑上述第一种情况中下边界最靠下的限制,以及第二种情况中上边界最靠上的限制,那么这一列要么都是障碍,要么能走的行数是一个连续的区间。假设某一块中有$k$个矩形障碍,那么按照每个障碍的左右边界将这一块$w\times m$的区域从左往右离散成$O(k)$个行数为$w$的区域,每个区域中最多只有一个连续的行区间可以行走。由于所有障碍的列数都为$h$,可以通过单调队列在$O(k)$时间内求出每个区域的可走区间。至此,我们成功地将一个包含$k$个矩形障碍的块的可走区域从左往右划分成了$O(k)$个矩形。
由于每块至少贡献一个区域,而总障碍数为$O(n)$,因此上述方法会得到$O(n+\frac{m}{w})$个矩形区域,当$\frac{m}{w}$较大时会退化,这是因为很多块一个障碍都没有,此时需要把连续的不含障碍的块合并成一个,则最终我们得到了$O(n)$个矩形区域。
对这$O(n)$个矩形区域建图,每个点代表一个区域,点权为区域的面积,若两个区域相邻则连边,答案即为每个连通块的点权和的平方之和。在这里,连边有以下两种情况:
- 同一块内左右相邻的两个区域需要连边,边数显然为$O(n)$。
- 相邻两块内上下相邻的两个区域需要连边,可以通过双指针实现,边数为相邻两块区域数之和,总边数仍为$O(n)$。
时间复杂度$O(n\log n)$,使用基数排序可以做到$O(n)$。
#include<cstdio> #include<algorithm> using namespace std; typedef unsigned long long ull; const int N=100000+105; int Case,n,M,W,H,i,j,k,x,y; int cp,pool[N*2]; int ce,cb,cnt; struct Block{ int l,r,bl,br,st,en; Block(){} Block(int _l,int _r){l=_l;r=_r;bl=l/W;br=r/W;} }block[N*4]; struct E{ int b,x,y,t; E(){} E(int _b,int _x,int _y,int _t){b=_b;x=_x;y=_y;t=_t;} }e[N*2]; struct Area{ int yl,yr,xl,xr; Area(){} Area(int _yl,int _yr,int _xl,int _xr){yl=_yl,yr=_yr,xl=_xl,xr=_xr;} ull cal(){return 1ULL*(yr-yl+1)*(xr-xl+1);} }area[N*8]; int f[N*8]; ull sz[N*8],ans; inline bool cmpe(const E&a,const E&b){return a.b==b.b?a.y<b.y:a.b<b.b;} inline void ext(int x,int y){ pool[++cp]=x/W; e[++ce]=E(x/W,x,y,0); if(x%W==0)return; x+=W-1; pool[++cp]=x/W; e[++ce]=E(x/W,x,y,1); } int F(int x){return f[x]==x?x:f[x]=F(f[x]);} inline void merge(int x,int y){ x=F(x),y=F(y); if(x==y)return; sz[y]+=sz[x]; f[x]=y; } inline bool check(int al,int ar,int bl,int br){return al<=br&&bl<=ar;} inline void analyze(int o,int l,int r){ int i,j,k,m,_; static int py[N*4],q0[N*2],q1[N*2]; int L=block[o].l,R=block[o].r; py[_=1]=M; for(i=l;i<=r;i++){ py[++_]=e[i].y-1; py[++_]=e[i].y+H-1; } sort(py+1,py+_+1); for(m=0,i=1;i<=_;i++)if(py[i]>py[m])py[++m]=py[i]; int h0=1,t0=0,h1=1,t1=0; block[o].st=cnt+1; for(i=1,j=l;i<=m;i++){ while(j<=r&&e[j].y<=py[i-1]+1){ if(e[j].t==0){ while(h0<=t0&&e[q0[t0]].x>=e[j].x)t0--; q0[++t0]=j; }else{ while(h1<=t1&&e[q1[t1]].x<=e[j].x)t1--; q1[++t1]=j; } j++; } int up=L,down=R; while(h0<=t0&&e[q0[h0]].y+H-1<py[i])h0++; if(h0<=t0)down=min(down,e[q0[h0]].x-1); while(h1<=t1&&e[q1[h1]].y+H-1<py[i])h1++; if(h1<=t1)up=max(up,e[q1[h1]].x+1); if(up<=down){ area[++cnt]=Area(py[i-1]+1,py[i],up,down); sz[cnt]=area[cnt].cal(); f[cnt]=cnt; } } int nowl=block[o].st,nowr=block[o].en=cnt; for(i=nowl;i<nowr;i++) if(area[i].yr+1==area[i+1].yl&&check(area[i].xl,area[i].xr,area[i+1].xl,area[i+1].xr)) merge(i,i+1); if(o==1)return; int prel=block[o-1].st,prer=block[o-1].en; for(i=nowl,j=prel,k=prel-1;i<=nowr;i++){ if(area[i].xl!=L)continue; while(j<=prer&&area[j].yr<area[i].yl)j++; while(k<prer&&area[k+1].yl<=area[i].yr)k++; for(_=j;_<=k;_++)if(area[_].xr+1==L)merge(i,_); } } int main(){ scanf("%d",&Case); while(Case--){ scanf("%d%d%d%d",&n,&M,&W,&H); pool[1]=0; pool[cp=2]=(M-1)/W; ce=cb=cnt=0; while(n--){ scanf("%d%d",&x,&y); x--; ext(x,y); } sort(pool+1,pool+cp+1); for(i=1;i<=cp;i++){ if(i>1&&pool[i]==pool[i-1])continue; int l=pool[i]*W; if(cb&&l>block[cb].r+1){ cb++; block[cb]=Block(block[cb-1].r+1,l-1); } block[++cb]=Block(l,min(l+W-1,M-1)); } sort(e+1,e+ce+1,cmpe); for(i=j=1;i<=cb;i++){ for(k=j;k<=ce&&e[k].b<=block[i].br;k++); analyze(i,j,k-1); j=k; } ans=0; for(i=1;i<=cnt;i++)if(F(i)==i)ans+=sz[i]*sz[i]; printf("%llu\n",ans); } }
H. Laser Alarm
三个不共线的点可以确定一个平面。对于任意一个平面,将其调整至经过三个顶点,结果不会变差。因此枚举三个顶点得到平面,然后$O(n)$计算触碰了该平面的线段数,更新答案即可。所有点都共线的情况需要特判。
时间复杂度$O(n^4)$。
#include<cstdio> const int N=55; int Case,n,i,j,k,o,now,ans; struct P{ int x,y,z; P(){} P(int _x,int _y,int _z){x=_x,y=_y,z=_z;} P operator-(const P&p)const{return P(x-p.x,y-p.y,z-p.z);} P operator*(const P&p)const{return P(y*p.z-z*p.y,z*p.x-x*p.z,x*p.y-y*p.x);} int operator^(const P&p)const{return x*p.x+y*p.y+z*p.z;} bool operator==(const P&p)const{return x==p.x&&y==p.y&&z==p.z;} }p[N*2]; inline int ptoplane(const P&a,const P&b,const P&c,const P&p){return((b-a)*(c-a))^(p-a);} inline bool colinear(const P&a,const P&b,const P&p){ P t=(a-b)*(b-p); return !t.x&&!t.y&&!t.z; } int main(){ scanf("%d",&Case); while(Case--){ scanf("%d",&n); for(i=0;i<n+n;i++)scanf("%d%d%d",&p[i].x,&p[i].y,&p[i].z); ans=1; for(i=0;i<n+n;i++)for(j=0;j<i;j++){ if(p[i]==p[j])continue; for(k=0;k<j;k++){ if(p[i]==p[k])continue; if(p[j]==p[k])continue; if(colinear(p[i],p[j],p[k]))continue; now=0; for(o=0;o<n;o++){ int x=ptoplane(p[i],p[j],p[k],p[o<<1]); int y=ptoplane(p[i],p[j],p[k],p[o<<1|1]); if(!x||!y||(x<0&&y>0)||(x>0&&y<0))now++; } if(now>ans)ans=now; } now=0; for(o=0;o<n;o++) if(colinear(p[i],p[j],p[o<<1])||colinear(p[i],p[j],p[o<<1|1])) now++; if(now>ans)ans=now; } printf("%d\n",ans); } }
I. Package Delivery
考虑$r$最小的那个区间$k$,第一次取快递放在第$r_k$天一定不会使结果变差。此时可能有很多区间覆盖了$r_k$,那么为了尽量延后下一次取快递的日期,此时的最优策略应该是选择覆盖$r_k$且$r$值最小的$k$个区间,使用堆找到并去掉这些区间后,问题就递归了。重复上述过程直至处理完所有$n$个区间。
时间复杂度$O(n\log n)$。
#include<cstdio> #include<algorithm> #include<vector> #include<queue> using namespace std; typedef pair<int,int>P; const int N=100005; int Case,n,k,i,j,t,ans,ql[N],qr[N],del[N]; P e[N]; priority_queue<P,vector<P>,greater<P> >q; inline bool cmpl(int x,int y){return e[x].first<e[y].first;} inline bool cmpr(int x,int y){return e[x].second<e[y].second;} int main(){ scanf("%d",&Case); while(Case--){ scanf("%d%d",&n,&k); for(i=1;i<=n;i++){ scanf("%d%d",&e[i].first,&e[i].second); ql[i]=i; qr[i]=i; del[i]=0; } sort(ql+1,ql+n+1,cmpl); sort(qr+1,qr+n+1,cmpr); for(ans=0,i=j=1;i<=n;i++){ if(del[qr[i]])continue; while(j<=n&&e[ql[j]].first<=e[qr[i]].second){ q.push(P(e[ql[j]].second,ql[j])); j++; } ans++; for(t=1;t<=k;t++){ if(q.empty())break; del[q.top().second]=1; q.pop(); } } printf("%d\n",ans); } }
J. Range Reachability Query
离线询问,设$f_{i,j}$表示$i$点能否仅通过编号在$[l_j,r_j]$之间的边走到第$j$个询问的目的地$v_j$,共$O(nq)$个01状态,可以使用bitset存储。第$j$个询问的答案即为$f_{u_j,j}$。
考虑一条边$u\rightarrow v$的转移,假设它的编号为$k$,令所有满足$l\leq k\leq r$的询问的集合为$S$,则有f[u] |= f[v] & S。接下来考虑如何得到集合$S$,一个直接的想法是从$1$到$m$依次考虑每条边,维护覆盖当前边的询问集合,每个询问拆成两个事件:
- 在$l$处加入集合。
- 在$r+1$处离开集合。
上述方法需要保存$m$个bitset,空间复杂度过高。节省空间的方法是每$O(\sqrt{q})$个事件保存一次bitset,这样只需保存$O(\sqrt{q})$个bitset,然后每次要得到集合$S$时,再往对应bitset的副本中暴力模拟$O(\sqrt{q})$个事件。
总时间复杂度$O(\frac{mq}{w}+m\sqrt{q})$。
#include<cstdio> #include<algorithm> using namespace std; typedef unsigned long long ull; const int N=50005,M=100005,Q=50005,K=785,B=325; int Case,n,m,q,all,ce,i,j,k,x,y,l,r,g[N],v[M],nxt[M],que[Q][2],en[M]; ull f[N][K],h[Q*2/B+3][K],cur[K]; struct E{int x,y;E(){}E(int _x,int _y){x=_x,y=_y;}}e[Q*2]; inline bool cmp(const E&a,const E&b){return a.x<b.x;} inline void flip(ull*f,int x){f[x>>6]^=1ULL<<(x&63);} inline void clr(ull*f){for(int i=0;i<=all;i++)f[i]=0;} inline void copy(ull*f,ull*g){for(int i=0;i<=all;i++)f[i]=g[i];} inline void trans(ull*f,ull*g,ull*h){for(int i=0;i<=all;i++)f[i]|=g[i]&h[i];} int main(){ scanf("%d",&Case); while(Case--){ scanf("%d%d%d",&n,&m,&q); for(i=1;i<=m;i++){ scanf("%d%d",&x,&y); v[i]=y; nxt[i]=g[x]; g[x]=i; } all=(q-1)>>6; for(i=1;i<=n;i++)clr(f[i]); for(i=0;i<q;i++){ scanf("%d%d%d%d",&x,&y,&l,&r); que[i][0]=x; que[i][1]=y; flip(f[y],i); e[++ce]=E(l,i); e[++ce]=E(r+1,i); } sort(e+1,e+ce+1,cmp); for(i=1,j=0;i<=m;i++){ while(j<ce&&e[j+1].x<=i)j++; en[i]=j; } clr(cur); for(i=1;i<=ce;i++){ flip(cur,e[i].y); if(i%B==0)copy(h[i/B],cur); } for(i=n;i;i--)for(j=g[i];j;j=nxt[j]){ x=en[j]; copy(cur,h[x/B]); for(k=x/B*B+1;k<=x;k++)flip(cur,e[k].y); trans(f[i],f[v[j]],cur); } for(i=0;i<q;i++)puts(f[que[i][0]][i>>6]>>(i&63)&1?"YES":"NO"); for(i=1;i<=n;i++)g[i]=0; ce=0; } }
K. Taxi
如果没有$w$的限制,那么是经典问题。根据$|x|=\max(x,-x)$,有
\begin{eqnarray*}
&&\max\left\{|x'-x_i|+|y'-y_i|\right\}\\
&=&\max\left\{\max(x'-x_i,-x'+x_i)+\max(y'-y_i,-y'+y_i)\right\}\\
&=&\max\left\{x'-x_i+y'-y_i,-x'+x_i+y'-y_i,x'-x_i-y'+y_i,-x'+x_i-y'+y_i)\right\}\\
&=&\max\left\{(x'+y')+(-x_i-y_i),(x'-y')+(-x_i+y_i),(-x'+y')+(x_i-y_i),(-x'-y')+(x_i+y_i)\right\}
\end{eqnarray*}
分别记录$x_i+y_i$、$x_i-y_i$、$-x_i+y_i$、$-x_i-y_i$的最大值即可在$O(1)$时间内求出所有点到$(x',y')$的曼哈顿距离的最大值。
现在考虑加入$w$的限制。将所有城镇按照$w$从小到大排序,并记录排序后每个后缀的$x_i+y_i$、$x_i-y_i$、$-x_i+y_i$、$-x_i-y_i$的最大值,用于$O(1)$求给定点$(x',y')$到该后缀中所有点的距离最大值。
选取按$w$排序后的第$k$个城镇,$O(1)$求出给定点$(x',y')$到第$k..n$个城镇的距离最大值$d$,有两种情况:
- $w_k<d$,那么第$k..n$个城镇对答案的贡献至少为$w_k$。用$w_k$更新答案后,由于第$1..k$个城镇的$w$值均不超过$w_k$,因此它们不可能接着更新答案,考虑范围缩小至$[k+1,n]$。
- $w_k\geq d$,那么第$k..n$个城镇对答案的贡献为$d$。用$d$更新答案后,考虑范围缩小至$[1,k-1]$。
容易发现每次考虑的范围都是一个区间,如果每次取$k$为区间的中点,那么迭代$O(\log n)$次即可得到最优解。
时间复杂度$O((n+q)\log n)$。
#include<cstdio> #include<algorithm> using namespace std; const int N=100005,inf=2100000000; int Case,n,m,i,x,y,a[N],b[N],c[N],d[N]; struct E{int x,y,w;}e[N]; inline bool cmp(const E&a,const E&b){return a.w<b.w;} inline void up(int&a,int b){a<b?(a=b):0;} int main(){ scanf("%d",&Case); while(Case--){ scanf("%d%d",&n,&m); for(i=1;i<=n;i++)scanf("%d%d%d",&e[i].x,&e[i].y,&e[i].w); sort(e+1,e+n+1,cmp); a[n+1]=b[n+1]=c[n+1]=d[n+1]=-inf; for(i=n;i;i--){ a[i]=max(a[i+1],-e[i].x-e[i].y); b[i]=max(b[i+1],-e[i].x+e[i].y); c[i]=max(c[i+1],e[i].x-e[i].y); d[i]=max(d[i+1],e[i].x+e[i].y); } while(m--){ scanf("%d%d",&x,&y); int l=1,r=n,mid,tmp,ans=0; while(l<=r){ mid=(l+r)>>1; tmp=x+y+a[mid]; up(tmp,x-y+b[mid]); up(tmp,-x+y+c[mid]); up(tmp,-x-y+d[mid]); if(e[mid].w<tmp){ l=mid+1; up(ans,e[mid].w); }else{ r=mid-1; up(ans,tmp); } } printf("%d\n",ans); } } }
L. Two Permutations
首先特判序列$S$中每个数字出现次数不都为$2$的情况,此时答案为$0$。
动态规划,设$f_{i,j}$表示$P$的前$i$项匹配上了$S$,且$P_i$匹配$S$中数字$P_i$第$j$次出现的位置时,有多少种合法的方案。由于$S$中每个数字出现次数都为$2$,因此状态数为$O(n)$。转移时枚举$P_{i+1}$匹配哪个位置,那么$P_i$匹配的位置与$P_{i+1}$匹配的位置中间的那段连续子串需要完全匹配$Q$中对应的子串,使用字符串Hash进行$O(1)$判断即可。
时间复杂度$O(n)$。
#include<cstdio> typedef unsigned long long ull; const int N=300005,P=998244353,S=233; int Case,n,i,j,k,x,y; int a[N],b[N],c[N*2],pc[N][2],f[N][2],ans; ull p[N*2],fb[N],fc[N*2]; inline void up(int&a,int b){a=a+b<P?a+b:a+b-P;} inline ull ask(ull*f,int l,int r){return f[r]-f[l-1]*p[r-l+1];} inline bool check(int bl,int br,int cl,int cr){ if(bl>br)return 1; if(bl<1||br>n||cl<1||cr>n+n)return 0; return ask(fb,bl,br)==ask(fc,cl,cr); } int main(){ for(p[0]=i=1;i<N*2;i++)p[i]=p[i-1]*S; scanf("%d",&Case); while(Case--){ scanf("%d",&n); for(i=1;i<=n;i++)pc[i][0]=pc[i][1]=0; for(i=1;i<=n;i++)scanf("%d",&a[i]); for(i=1;i<=n;i++)scanf("%d",&b[i]),fb[i]=fb[i-1]*S+b[i]; for(i=1;i<=n+n;i++){ scanf("%d",&x); c[i]=x; fc[i]=fc[i-1]*S+x; if(!pc[x][0])pc[x][0]=i;else pc[x][1]=i; } for(i=1;i<=n;i++)if(!pc[i][0]||!pc[i][1])break; if(i<=n){ puts("0"); continue; } for(i=1;i<=n;i++)for(j=0;j<2;j++)f[i][j]=0; for(j=0;j<2;j++){ x=pc[a[1]][j]; if(check(1,x-1,1,x-1))f[1][j]=1; } for(i=1;i<n;i++)for(j=0;j<2;j++)if(f[i][j]){ x=pc[a[i]][j]; for(k=0;k<2;k++){ y=pc[a[i+1]][k]; if(y<=x)continue; if(check(x-i+1,y-i-1,x+1,y-1))up(f[i+1][k],f[i][j]); } } ans=0; for(j=0;j<2;j++)if(f[n][j]){ x=pc[a[n]][j]; if(check(x-n+1,n,x+1,n+n))up(ans,f[n][j]); } printf("%d\n",ans); } }