2019 Multi-University Training Contest 6
A. Salty Fish
假设要偷走所有的苹果,那么现在需要放弃一些苹果,并且黑掉一些监控相机以保证能偷走没有放弃的苹果,我们需要最小化放弃的苹果的收益之和加上黑掉相机支付的代价的总和。
考虑最小割建图:
- 源点$S$向每个监控相机连边,割掉这条边的代价为黑掉它的代价,割掉这条边表示黑掉这个监控相机。
- 每个节点向汇点$T$连边,割掉这条边的代价为这个点的苹果数,割掉这条边表示放弃这个节点的苹果。
- 每个监控相机向其监控范围内的所有节点连边,割掉这条边的代价为$+\infty$,表示不能破坏监控关系。
那么每条$S$到$T$的路径都表示某个节点既没有并放弃,又被一些相机监控着,这是不合法的。我们需要割掉代价之和最少的边,使得$S$和$T$不连通,因此最终的答案就是所有节点的苹果数量之和减去这个图的最小割。
因为最小割=最大流,从叶子向根节点依次考虑每棵子树,计算最大流。设$v[i][j]$表示$i$的子树中离$i$距离为$j$的那些节点还能提供多少流量,那么考虑监控范围最高点在$i$的每个监控相机,显然应该优先接收距离较大的那些节点的流量。
用std::map存储每个值非零的$v[i]$,可以在总计$O(m\log n)$的时间内求出最大流。至于$v[i]$的计算,可以由$i$的儿子的$v$启发式合并而来。
注意到这是关于深度的一个启发式合并,如果将这棵树长链剖分,计算出每个点$x$子树内离$x$距离最远的点到$x$的距离$d[x]$,那么可以选择将$d$最大的儿子的$v$继承给$x$,然后将其它儿子的$v$暴力插入到$v[x]$中。因为每个点仅属于一条长链,且一条长链只会在链顶位置作为短儿子被暴力合并一次,所以合并的时间复杂度为$O(n)$。
总时间复杂度为$O((n+m)\log n)$。
#include<cstdio> #include<map> #include<algorithm> using namespace std; typedef long long ll; const int N=300010; int Case,n,m,i,j,x,A,B,f[N],d[N],a[N],e[N][2],g[N],nxt[N];ll ans;map<int,ll>T[N]; inline void add(int x,int y){nxt[y]=g[x];g[x]=y;} int main(){ scanf("%d",&Case); while(Case--){ scanf("%d%d",&n,&m); ans=0; for(i=1;i<=n;i++)g[i]=0,T[i].clear(); for(i=2;i<=n;i++)scanf("%d",&f[i]),d[i]=d[f[i]]+1; for(i=1;i<=n;i++)scanf("%d",&a[i]),ans+=a[i]; for(i=1;i<=m;i++)scanf("%d%d%d",&x,&e[i][0],&e[i][1]),add(x,i); for(i=n;i;i--){ T[i][d[i]]+=a[i]; for(j=g[i];j;j=nxt[j]){ A=d[i]+e[j][0],B=e[j][1]; while(B&&T[i].size()){ map<int,ll>::iterator it=T[i].upper_bound(A); if(it==T[i].begin())break; it--; x=B<it->second?B:it->second; B-=x; it->second-=x; ans-=x; if(!it->second)T[i].erase(it); } } if(i>1){ x=f[i]; if(T[x].size()<T[i].size())swap(T[x],T[i]); for(map<int,ll>::iterator it=T[i].begin();it!=T[i].end();it++)if(it->second)T[x][it->first]+=it->second; } } printf("%lld\n",ans); } }
B. Nonsense Time
考虑时间倒流,看作一个完整的排列按照一定顺序依次删除每个数,然后每次需要计算LIS的长度。
首先在$O(n\log n)$的时间内求出LIS,并找到一个LIS。当删除$x$时,如果$x$不在之前找到的那个LIS中,那么显然LIS的长度是不会变化的,否则暴力重新计算出新的LIS即可。
因为数据随机,因此LIS的期望长度是$O(\sqrt{n})$,删除的$x$位于LIS中的概率是$\frac{1}{\sqrt{n}}$,也就是说期望删除$O(\sqrt{n})$个数才会修改LIS,那么LIS变化的次数不会很多。
期望时间复杂度为$O(n\sqrt{n}\log n)$。
#include<cstdio> const int N=50010; int Case,n,i,x,a[N],b[N],ans[N],pre[N],nxt[N],f[N],g[N],used[N],bit[N]; inline void up(int&a,int b){if(f[a]<f[b])a=b;} inline void build(){ int i,j,k; for(i=nxt[0];i<=n+1;i=nxt[i]){ used[i]=0; k=0; for(j=a[i];j;j-=j&-j)up(k,bit[j]); f[i]=f[k]+1; g[i]=k; for(j=a[i];j<=n+2;j+=j&-j)up(bit[j],i); } for(i=nxt[0];i<=n+1;i=nxt[i])for(j=a[i];j<=n+2;j+=j&-j)bit[j]=0; for(i=n+1;i;i=g[i])used[i]=1; } int main(){ scanf("%d",&Case); while(Case--){ scanf("%d",&n); for(i=1;i<=n;i++)scanf("%d",&a[i]),a[i]++; a[0]=1; a[n+1]=n+2; for(i=0;i<=n+1;i++)pre[i]=i-1,nxt[i]=i+1,bit[i]=used[i]=0; bit[n+2]=0; for(i=1;i<=n;i++)scanf("%d",&b[i]); build(); for(i=n;i;i--){ ans[i]=f[n+1]-1; x=b[i]; pre[nxt[x]]=pre[x]; nxt[pre[x]]=nxt[x]; if(used[x])build(); } for(i=1;i<=n;i++)printf("%d%c",ans[i],i<n?' ':'\n'); } }
C. Milk Candy
建立一张$n+1$个点的图,点的编号为$0$到$n$,点$i$表示$s_i=x_1+x_2+\dots+x_i$。如果我们知道了$x_l+x_{l+1}+\dots+x_r$,那么我们就知道了$s_r-s_{l-1}$ 的值,在$l-1$和$r$之间连一条边。如果这个图是连通的,那么我们就能根据$s_0=0$推出所有$s$,从而推出所有$x$。问题转化为从每个NPC手中恰好购买$k_i$条边,使得这个图连通,且代价之和最小。
从另外一个角度考虑这个问题:先购买所有边,然后从每个NPC手中删除不超过$c_i-k_i$条边,总计删除恰好$\sum(c_i-k_i)$条边,使得剩下的图仍然连通,且删去的边代价之和最大。由于删去边后图连通等价于剩下的边存在生成树,生成树是图拟阵的基,所以这是图拟阵的对偶拟阵$M_1$;而从每个边集中选择不超过若干条边的条件,则是划分拟阵$M_2$。
所以我们的目标就是找到这两个拟阵的交的大小为$\sum(c_i-k_i)$的权值和最大的独立集,可以用拟阵交算法解决:
- 令初始解$I$为空集,即没有边被删除。
- 每条边作为有向图中的一个点,并新建源点$S$和汇点$T$。
- 对于$x\notin I$的某条边$x$,将$x$的点权设置为$w_x$,表示额外删掉这条边的代价。若$I\cup x$满足$M_1$,则连边$S\rightarrow x$;若$I\cup x$满足$M_2$,则连边$x\rightarrow T$。
- 对于$x\in I$的某条边$x$,将$x$的点权设置为$-w_x$,表示取消删除这条边的代价。
- 对于$x\in I$的某条边$x$以及$y\notin I$的某条边$y$,若$I\setminus x\cup y$满足$M_1$,则连边$x\rightarrow y$;若$I\setminus x\cup y$满足$M_2$,则连边$y\rightarrow x$。
- 在构造出来的图中SPFA找到$S$到$T$的最长路作为增广路,将上面每条边的选择情况取反,得到新的解$I'$,此时$I'$的大小比$I$刚好大$1$。不断重复构图找增广路直至$I$的大小为$\sum(c_i-k_i)$。
不妨认为$n,m,\sum c$同阶,则一共$O(n)$次增广,每次增广建图需要$O(n^3)$的时间,寻找增广路需要$O(n^3)$的SPFA,总时间复杂度为$O(n^4)$。
#include<cstdio> const int N=85,M=100000,inf=~0U>>1; int Case,n,cnt,m,goal,have,num[N],lim[N],i,j; int S,T,x,y,g[N],v[N<<1],nxt[N<<1],ed,vis[N]; int cost[N],col[N],use[N],ans; inline void add(int x,int y){v[++ed]=y;nxt[ed]=g[x];g[x]=ed;} void dfs(int x){ if(vis[x])return; vis[x]=1; for(int i=g[x];i;i=nxt[i])if(use[i>>1])dfs(v[i]); } inline bool check(){ int i; for(i=0;i<=n;i++)vis[i]=0; dfs(0); for(i=0;i<=n;i++)if(!vis[i])return 0; return 1; } inline bool check2(){ for(int i=1;i<=cnt;i++)if(num[i]<lim[i])return 0; return 1; } namespace Matroid{ int g[N],v[M],nxt[M],ed,q[M],h,t,d[N],pre[N],w[N];bool in[N]; inline void add(int x,int y){v[++ed]=y;nxt[ed]=g[x];g[x]=ed;} inline void ext(int x,int y,int z){ if(d[x]<=y)return; d[x]=y; pre[x]=z; if(in[x])return; q[++t]=x; in[x]=1; } inline bool find(){ int i,j; S=m+1,T=m+2; for(ed=0,i=1;i<=T;i++)g[i]=0; for(i=1;i<=m;i++)if(use[i]){ w[i]=-cost[i]; use[i]^=1; num[col[i]]--; if(check())add(S,i); if(check2())add(i,T); num[col[i]]++; use[i]^=1; }else w[i]=cost[i]; for(i=1;i<=m;i++)if(use[i])for(j=1;j<=m;j++)if(!use[j]){ use[i]^=1,use[j]^=1; num[col[i]]--;num[col[j]]++; if(check())add(j,i); if(check2())add(i,j); num[col[i]]++;num[col[j]]--; use[i]^=1,use[j]^=1; } for(i=1;i<=T;i++)d[i]=inf,in[i]=0; q[h=t=1]=S; d[S]=0,in[S]=1; while(h<=t){ x=q[h++]; for(i=g[x];i;i=nxt[i])ext(v[i],d[x]+w[v[i]],x); in[x]=0; } if(d[T]==inf)return 0; ans+=d[T]; while(pre[T]!=S){ T=pre[T]; if(use[T])num[col[T]]--;else num[col[T]]++; use[T]^=1; } return 1; } } int main(){ scanf("%d",&Case); while(Case--){ scanf("%d%d",&n,&cnt); m=goal=ans=0; for(i=0;i<=n;i++)g[i]=0; for(ed=i=1;i<=cnt;i++){ scanf("%d%d",&num[i],&lim[i]); goal+=lim[i]; for(j=0;j<num[i];j++){ m++; col[m]=i; scanf("%d%d%d",&x,&y,&cost[m]); add(x-1,y); add(y,x-1); use[m]=1; ans+=cost[m]; } } if(!check()){ puts("-1"); continue; } have=m; while(have>goal){ if(!Matroid::find())break; have--; } if(have!=goal)ans=-1; printf("%d\n",ans); } }
D. Speed Dog
问题等价于找到一堆$x_i(0\leq x_i\leq 1)$,使得下面式子的值最小:
\[
\max\left(\sum_{i=1}^n a_ix_i,\sum_{i=1}^n b_i(1-x_i)\right)
\]
因为
\[
\max\left(A,B\right)=\max_{0\leq k\leq 1}\left(kA+(1-k)B\right)
\]
所以
\begin{eqnarray*}
&&\max\left(\sum_{i=1}^n a_ix_i,\sum_{i=1}^n b_i(1-x_i)\right)\\
&=&\max_{0\leq k\leq 1}\left(\sum_{i=1}^n ka_ix_i+(1-k)b_i(1-x_i)\right)
\end{eqnarray*}
根据Minimax Theorem,有
\begin{eqnarray*}
&&\min_{0\leq x_1,x_2,\dots,x_n\leq 1}\left(\max_{0\leq k\leq 1}\left(\sum_{i=1}^n ka_ix_i+(1-k)b_i(1-x_i)\right)\right)\\
&=&\max_{0\leq k\leq 1}\left(\min_{0\leq x_1,x_2,\dots,x_n\leq 1}\left(\sum_{i=1}^n ka_ix_i+(1-k)b_i(1-x_i)\right)\right)\\
&=&\max_{0\leq k\leq 1}\left(\sum_{i=1}^n \min_{0\leq x_i\leq 1}\left(ka_ix_i+(1-k)b_i(1-x_i)\right)\right)\\
&=&\max_{0\leq k\leq 1}\left(\sum_{i=1}^n \min\left(ka_i,(1-k)b_i\right)\right)
\end{eqnarray*}
注意到对于固定的$i$来说,$\min\left(ka_i,(1-k)b_i\right)$关于$k$的函数是一个凸函数,而凸函数的和$f(k)$也为凸函数,因此可以通过三分这个$k$得到答案。
对于固定的$i$来说,当$ka_i=(1-k)b_i$,也就是$k=\frac{b_i}{a_i+b_i}$时取得极值,所以只有$O(n)$个这样的$k$是有用的,只需要在它们之间三分,也因此避免了浮点数运算。注意这里需要对这些$k$进行去重,否则三分时可能会出现函数平台导致三分失败。
现在剩下的问题就是如何快速得到答案。对于这些$k$建立一棵权值线段树,将每个二元组$(a_i,b_i)$放在分界线$\frac{b_i}{a_i+b_i}$的位置上。线段树每个节点维护对应区间内$a$的和、$b$的和以及区间左端点和右端点对应的$a$和$b$的和,插入一个新的二元组的时间复杂度为$O(\log n)$。
查询最优解时,只需要从线段树根节点开始,假设左子树表示$[l,mid]$,右子树表示$[mid+1,r]$,那么通过比较$f(mid)$和$f(mid+1)$的大小即可知道极值位于左子树还是右子树,通过线段树维护的信息可以$O(1)$算出$f(mid)$和$f(mid+1)$的值。
时间复杂度$O(n\log n)$。
#include<cstdio> #include<algorithm> using namespace std; typedef long long ll; const int N=250010,M=524305; int Case,n,m,_,i,x,y,pos[N];ll sa[M],sb[M],la[M],lb[M],alla; struct E{int x,y;}e[N]; ll gcd(ll a,ll b){return b?gcd(b,a%b):a;} struct Num{ ll u,d; Num(){}Num(ll _u,ll _d){u=_u,d=_d;} void write(){ ll z=gcd(u,d); printf("%lld/%lld\n",u/z,d/z); } }q[N]; inline int cmp(const Num&a,const Num&b){ ll t=a.u*b.d-b.u*a.d; if(t<0)return -1; return t?1:0; } inline bool cmpn(const Num&a,const Num&b){return cmp(a,b)<0;} inline int lower(int A,int B){ Num x(A,B); int l=1,r=m,mid,t; while(1){ mid=(l+r)>>1; t=cmp(x,q[mid]); if(!t)return mid; if(t<0)r=mid-1;else l=mid+1; } } void build(int x,int a,int b){ sa[x]=sb[x]=la[x]=lb[x]=0; if(a==b){pos[a]=x;return;} int mid=(a+b)>>1; build(x<<1,a,mid),build(x<<1|1,mid+1,b); } inline void modify(int A,int B){ int x=pos[lower(B,A+B)]; alla+=A; sa[x]+=A; sb[x]+=B; la[x]+=A; lb[x]+=B; for(x>>=1;x;x>>=1){ sa[x]+=A; sb[x]+=B; la[x]=la[x<<1]; lb[x]=lb[x<<1]; } } inline void query(){ int x=1,a=1,b=m,mid; ll prea=0,preb=0; Num f,g; while(a<b){ mid=(a+b)>>1; x<<=1; f=Num((alla-(prea+sa[x])-(preb+sb[x]))*q[mid].u+(preb+sb[x])*q[mid].d,q[mid].d); g=Num((alla-(prea+sa[x]+la[x+1])-(preb+sb[x]+lb[x+1]))*q[mid+1].u+(preb+sb[x]+lb[x+1])*q[mid+1].d,q[mid+1].d); if(cmp(f,g)<0){ prea+=sa[x]; preb+=sb[x]; x++; a=mid+1; }else b=mid; } prea+=sa[x]; preb+=sb[x]; f=Num((alla-prea-preb)*q[a].u+preb*q[a].d,q[a].d); f.write(); } int main(){ scanf("%d",&Case); while(Case--){ scanf("%d",&n); q[1]=Num(0,1); q[m=2]=Num(1,1); for(i=1;i<=n;i++){ scanf("%d%d",&x,&y); e[i].x=x; e[i].y=y; q[++m]=Num(y,x+y); } sort(q+1,q+m+1,cmpn); for(_=0,i=1;i<=m;i++)if(i==1||cmp(q[i],q[_]))q[++_]=q[i]; m=_; build(1,1,m); alla=0; for(i=1;i<=n;i++)modify(e[i].x,e[i].y),query(); } }
E. Snowy Smile
首先将纵坐标离散化到$O(n)$的范围内,方便后续的处理。
将所有点按照横坐标排序,枚举矩形的上边界,然后往后依次加入每个点,这样就确定了矩形的上下边界。设$v[y]$表示矩形内部纵坐标为$y$的点的权值和,则答案为$v$的最大子段和,用线段树维护带修改的最大子段和即可。
时间复杂度$O(n^2\log n)$。
#include<cstdio> #include<algorithm> using namespace std; typedef long long ll; const int N=2010,M=4100; int Case,n,m,i,j,k,cb,b[N],pos[N];ll pre[M],suf[M],s[M],v[M],ans; struct E{int x,y,z;}e[N]; inline bool cmp(const E&a,const E&b){return a.x<b.x;} void build(int x,int a,int b){ pre[x]=suf[x]=s[x]=v[x]=0; if(a==b){ pos[a]=x; return; } int mid=(a+b)>>1; build(x<<1,a,mid),build(x<<1|1,mid+1,b); } inline void change(int x,int p){ x=pos[x]; s[x]+=p; if(s[x]>0)pre[x]=suf[x]=v[x]=s[x];else pre[x]=suf[x]=v[x]=0; for(x>>=1;x;x>>=1){ pre[x]=max(pre[x<<1],s[x<<1]+pre[x<<1|1]); suf[x]=max(suf[x<<1|1],s[x<<1|1]+suf[x<<1]); s[x]=s[x<<1]+s[x<<1|1]; v[x]=max(max(v[x<<1],v[x<<1|1]),suf[x<<1]+pre[x<<1|1]); } } int main(){ scanf("%d",&Case); while(Case--){ scanf("%d",&n); for(cb=0,i=1;i<=n;i++){ scanf("%d%d%d",&e[i].x,&e[i].y,&e[i].z); b[++cb]=e[i].y; } sort(b+1,b+cb+1); for(m=0,i=1;i<=cb;i++)if(i==1||b[i]!=b[m])b[++m]=b[i]; sort(e+1,e+n+1,cmp); ans=0; for(i=1;i<=n;i++)e[i].y=lower_bound(b+1,b+m+1,e[i].y)-b; for(i=1;i<=n;i++)if(i==1||e[i].x!=e[i-1].x){ build(1,1,m); for(j=i;j<=n;j=k){ for(k=j;k<=n&&e[j].x==e[k].x;k++)change(e[k].y,e[k].z); if(ans<v[1])ans=v[1]; } } printf("%lld\n",ans); } }
F. Faraway
将$|x_i-x_e|+|y_i-y_e|$的绝对值拆掉,则每个点$(x_i,y_i)$会将平面分割成$4$个部分,每个部分里距离的表达式没有绝对值符号,一共$O(n^2)$个这样的区域。
枚举每个区域,计算该区域中可能的终点数量。注意到$lcm(2,3,4,5)=60$,所以只需要枚举$x_e$和$y_e$模$60$的余数,$O(n)$判断是否可行,然后$O(1)$计算该区域中有多少这样的点即可。
时间复杂度为$O(60^2n^3)$。
#include<cstdio> #include<algorithm> using namespace std; const int N=15,K=60; int Case,n,m,x,y,i,j,ca,cb,a[N],b[N];long long ans; struct E{int x,y,k,t;}e[N]; inline int abs(int x){return x>0?x:-x;} inline bool check(int x,int y){ for(int i=0;i<n;i++)if((abs(x-e[i].x)+abs(y-e[i].y))%e[i].k!=e[i].t)return 0; return 1; } inline int cal(int l,int r){ r-=l+1; if(r<0)return 0; return r/K+1; } int main(){ scanf("%d",&Case); while(Case--){ scanf("%d%d",&n,&m); a[ca=1]=b[cb=1]=m+1; for(i=0;i<n;i++){ scanf("%d%d%d%d",&e[i].x,&e[i].y,&e[i].k,&e[i].t); a[++ca]=e[i].x; b[++cb]=e[i].y; } sort(a+1,a+ca+1); sort(b+1,b+cb+1); ans=0; for(i=0;i<ca;i++)if(a[i]<a[i+1])for(j=0;j<cb;j++)if(b[j]<b[j+1]) for(x=0;x<K;x++)for(y=0;y<K;y++)if(check(a[i]+x,b[j]+y)) ans+=1LL*cal(a[i]+x,a[i+1])*cal(b[j]+y,b[j+1]); printf("%lld\n",ans); } }
G. Support or Not
首先考虑找到第$k$小的球对距离,二分答案$mid$,统计有多少对球的距离不超过$mid$。我们需要找到最小的$mid$,使得有至少$k$对球的距离不超过$mid$。
将每个球的半径都加上$\frac{mid}{2}$,那么我们的目标是统计有多少对球存在公共点。
假设最大的球半径为$R$,以$2R$为棱长将三维空间划分为一个个立方体格子,那么每个球只需要检查球心在附近$27$个格子内部的所有球。考虑所有球的半径相等的情况,那么每个格子内部一旦有超过$O(\sqrt{k})$个球时,我们必然已经找到了$k$对相交的球。因此在找到$k$对相交的球时及时结束二分答案的检查过程即可。
但是当球的半径不尽相同时,上述分析不成立。那么在当前球的半径不足$\frac{R}{2}$时重构网格,则最多会重构$O(\log r)$次,且每个球依然只会检查均摊$O(\sqrt{k})$个球与它是否相交。
找到第$k$小解$ans$后,我们只需要取$mid=k-1$,继续运行检查算法,将找到的这些相交球对之间的距离作为最终的答案的即可,如果不足$k$个,那么剩下的答案肯定都是$ans$。
利用Hash表定位格子,则总时间复杂度为$O(n\log^2r+n\sqrt{k}\log r)$,常数较小。
#include<cstdio> #include<algorithm> using namespace std; typedef long long ll; typedef unsigned long long ull; const int N=100010,M=310,inf=3000000,MO=(1<<19)-1; unsigned int wx[inf],wy[inf],wz[inf]; int Case,n,m,lim,K,i,ans[M]; struct E{int x,y,z,r;}e[N]; inline bool cmp(const E&a,const E&b){return a.r>b.r;} inline ll sqr(ll x){return x*x;} inline bool check(const E&a,const E&b){return sqr(a.x-b.x)+sqr(a.y-b.y)+sqr(a.z-b.z)<=sqr(a.r+b.r+lim*2);} inline int dis(const E&a,const E&b){ ll tmp=sqr(a.x-b.x)+sqr(a.y-b.y)+sqr(a.z-b.z); int l=0,r=inf,mid,ret; while(l<=r){ mid=(l+r)>>1; if(tmp<=sqr(a.r+b.r+mid*2))r=(ret=mid)-1;else l=mid+1; } return ret; } struct EV{ull v;int w;EV*nxt;}*g[MO+7],pool[N],*cur,*p; int pos[N],at[N],cnt,d[N],en[N],id[N],last[MO+7],CUR; inline int ins(int A,int B,int C){ int u=(wx[A]^wy[B]^wy[C])&MO; ull v=(((ull)A)<<42)|(((ull)B)<<21)|C; if(last[u]<CUR)last[u]=CUR,g[u]=NULL; for(p=g[u];p;p=p->nxt)if(p->v==v)return p->w; cnt++; d[cnt]=0; p=cur++; p->v=v; p->w=cnt; p->nxt=g[u]; g[u]=p; return cnt; } inline int ask(int A,int B,int C){ int u=(wx[A]^wy[B]^wy[C])&MO; ull v=(((ull)A)<<42)|(((ull)B)<<21)|C; if(last[u]<CUR)return 0; for(p=g[u];p;p=p->nxt)if(p->v==v)return p->w; return 0; } inline void build(int st,int pre){ cnt=0; cur=pool; CUR++; for(int i=st;i<=n;i++){ pos[i]=ins(e[i].x/pre,e[i].y/pre,e[i].z/pre); d[pos[i]]++; } for(int i=1;i<=cnt;i++)d[i]+=d[i-1]; for(int i=1;i<=cnt;i++)en[i]=d[i]; for(int i=st;i<=n;i++)id[d[pos[i]]--]=i; } inline int cal(int _lim,int mode=0){ int pre=~0U>>1; lim=_lim; m=0; for(int i=1;i<=n;i++){ int now=(e[i].r+lim)*2; if(now*2<pre&&i<n)build(i+1,pre=now); int A=e[i].x/pre,B=e[i].y/pre,C=e[i].z/pre; for(int x=A-1;x<=A+1;x++)if(x>=0) for(int y=B-1;y<=B+1;y++)if(y>=0) for(int z=C-1;z<=C+1;z++)if(z>=0){ int o=ask(x,y,z); if(!o)continue; for(int j=en[o-1]+1;j<=en[o];j++){ int k=id[j]; if(k<=i)break; if(check(e[i],e[k])){ m++; if(mode)ans[m]=dis(e[i],e[k]); if(m>=K)return m; } } } } return m; } int main(){ for(wx[0]=324673,i=1;i<inf;i++)wx[i]=wx[i-1]*233+17; for(wy[0]=812376,i=1;i<inf;i++)wy[i]=wy[i-1]*13331+97; for(wz[0]=921375,i=1;i<inf;i++)wz[i]=wz[i-1]*10007+53; scanf("%d",&Case); while(Case--){ scanf("%d%d",&n,&K); for(i=1;i<=n;i++){ scanf("%d%d%d%d",&e[i].x,&e[i].y,&e[i].z,&e[i].r); e[i].x<<=1; e[i].y<<=1; e[i].z<<=1; e[i].r<<=1; } sort(e+1,e+n+1,cmp); int l=0,r=inf,mid,fin; while(l<=r){ mid=(l+r)>>1; if(cal(mid)<K)l=mid+1;else r=(fin=mid)-1; } for(i=1;i<=K;i++)ans[i]=fin; if(fin)cal(fin-1,1); sort(ans+1,ans+K+1); for(i=1;i<=K;i++)printf("%d\n",ans[i]); } }
H. TDL
考虑枚举$f(n,m)-n$的值$t$,则$n=t\oplus k$,$O(t\log n)$检查这个$n$是否满足条件即可。
注意到$t$显然不会超过第$m$个与$n$互质的质数,而$n$最多只有$O(\log\log n)<m=100$个质数,根据质数密度可以得到$t$的一个比较松的上界$O(m\log m)$。
时间复杂度$O(m^2\log^2m\log n)$。
#include<cstdio> typedef long long ll; int Case,m,d;ll k,ans; ll gcd(ll a,ll b){return b?gcd(b,a%b):a;} inline ll cal(ll n,int m){ if(n<1)return 0; for(ll i=n+1;;i++)if(gcd(n,i)==1){ m--; if(!m)return i-n; } } int main(){ scanf("%d",&Case); while(Case--){ scanf("%lld%d",&k,&m); ans=-1; for(d=1;d<700;d++)if(cal(k^d,m)==d){ if(ans==-1)ans=k^d; else if(ans>(k^d))ans=k^d; } printf("%lld\n",ans); } }
I. Three Investigators
考虑将数字$a[i]$拆成$a[i]$个$a[i]$,比如4,1,2$\rightarrow$4,4,4,4,1,2,2,则问题转化为:找到最多$5$个不共享元素的不下降子序列,使得这些子序列包含的元素总量最多。可以证明,这等于杨氏图表前$5$层的长度之和。
考虑杨氏图表求解答案的过程:
- 从$1$到$n$依次考虑序列中的每个数,将其插入杨氏图表的第一层中。
- 插入$x$时,如果$x$不小于这一层的最大的数,则将$x$放在这一层的末尾;否则找到大于$x$的最小的数$y$,将$y$替换为$x$,并将$y$插入下一层。
因为每一层的元素都有序,所以可以用数组维护,寻找$y$的过程可以用二分查找加速。
但是对于本题来说,我们不能暴力地插入$a[i]$个$a[i]$。考虑将杨表每一层中相同的元素合并,用std::map记录每个元素的个数,那么当我们一次性插入$x$个$x$时,只需要将其插入std::map中,然后不断消费后继,将后继的元素个数减少即可,在减少的时候要将其作为“$p$个$q$”插入下一层中。
每一类数字被消费完毕后需要及时从std::map中删除,而每次插入会导致最多一种其它数字被拆分,所以每层的插入次数至多为上一层的两倍。
假设要求不超过$k$个子序列的答案,本题中$k=5$,则时间复杂度为$O(2^kn\log n)$。
#include<cstdio> #include<map> #include<algorithm> using namespace std; typedef long long ll; const int K=5; int Case,n,i,x;ll ans;map<int,ll>T[K]; void ins(int o,int x,ll p){ if(o>=K)return; T[o][x]+=p; ans+=p; while(p){ map<int,ll>::iterator it=T[o].lower_bound(x+1); if(it==T[o].end())return; ll t=min(p,it->second); ans-=t; p-=t; ins(o+1,it->first,t); if(t==it->second)T[o].erase(it);else it->second-=t; } } int main(){ scanf("%d",&Case); while(Case--){ scanf("%d",&n); ans=0; for(i=0;i<K;i++)T[i].clear(); for(i=1;i<=n;i++){ scanf("%d",&x); ins(0,x,x); printf("%lld%c",ans,i<n?' ':'\n'); } } }
J. Ridiculous Netizens
取一个根,将这棵树转化为有根树,考虑连通块包含根节点的情况,那么对于一个点来说,如果它选了,它的父亲就必须选。
求出DFS序括号序列,设$f[i][\lfloor\frac{m}{j}\rfloor]$表示考虑了DFS序的前$i$项,目前连通块点权乘积为$j$的方案数。因为当$j\geq\sqrt{m}$时$\lfloor\frac{m}{j}\rfloor$只有$O(\sqrt{m})$种取值,所以状态数为$O(n\sqrt{m})$。注意到$\lfloor\frac{m}{jk}\rfloor=\lfloor\frac{\lfloor\frac{m}{j}\rfloor}{k}\rfloor$,所以可以转移。
如果$i$是一个左括号,那么把$f$传给儿子,并强制选择儿子;如果$i$是个右括号,那么这个子树既可以选又可以不选,将对应状态的方案数累加即可,转移$O(1)$。
接下来考虑连通块不包含根节点的情况,那么可以去掉这个根,变成若干棵树的子问题。取重心作为根进行点分治,则考虑的总点数为$O(n\log n)$。
时间复杂度$O(n\sqrt{m}\log n)$。
#include<cstdio> const int N=2010,K=2010,P=1000000007; int Case,n,m,cnt,val[K],i,x,y,a[N],ans; int g[N],nxt[N<<1],v[N<<1],ok[N<<1],ed,son[N],f[N],all,now; int dp[N][K],tmp[K]; inline void up(int&a,int b){a=a+b<P?a+b:a+b-P;} inline void add(int x,int y){v[++ed]=y;nxt[ed]=g[x];ok[ed]=1;g[x]=ed;} void findroot(int x,int y){ son[x]=1;f[x]=0; for(int i=g[x];i;i=nxt[i])if(ok[i]&&v[i]!=y){ findroot(v[i],x); son[x]+=son[v[i]]; if(son[v[i]]>f[x])f[x]=son[v[i]]; } if(all-son[x]>f[x])f[x]=all-son[x]; if(f[x]<f[now])now=x; } void dfs(int x,int y){ int i,j,k=a[x]; for(i=1;i<=cnt;i++)tmp[i]=0; for(i=j=1;i<=cnt;i++){ int t=val[i]/k; if(!t)continue; while(val[j]>t)j++; up(tmp[j],dp[x][i]); } for(i=1;i<=cnt;i++)dp[x][i]=tmp[i]; for(i=g[x];i;i=nxt[i])if(ok[i]){ int u=v[i]; if(u==y)continue; for(j=1;j<=cnt;j++)dp[u][j]=dp[x][j]; dfs(u,x); for(j=1;j<=cnt;j++)up(dp[x][j],dp[u][j]); } } void solve(int x){ int i; for(i=1;i<=cnt;i++)dp[x][i]=0; dp[x][1]=1; dfs(x,0); for(i=1;i<=cnt;i++)up(ans,dp[x][i]); for(i=g[x];i;i=nxt[i])if(ok[i]){ ok[i^1]=0; f[0]=all=son[v[i]]; findroot(v[i],now=0); solve(now); } } int main(){ scanf("%d",&Case); while(Case--){ scanf("%d%d",&n,&m); cnt=ans=0; for(i=1;i<=n;i++)g[i]=son[i]=f[i]=0; for(i=1;i<=m;i=m/(m/i)+1)val[++cnt]=m/i; for(i=1;i<=n;i++)scanf("%d",&a[i]); for(ed=i=1;i<n;i++)scanf("%d%d",&x,&y),add(x,y),add(y,x); f[0]=all=n;findroot(1,now=0);solve(now); printf("%d\n",ans); } }
K. 11 Dimensions
设$f[i][j]$表示$[1,i-1]$这些位的数字已经确定,且$[1,i-1]$的数字模$m=j$时,有多少种在$[i,n]$这些位填数字的方法使得最终的数字模$m=0$。
初始值:$f[n+1][0]=1$。
转移:$f[i][j]=\sum f[i+1][(10j+k)\bmod m]$,其中$0\leq k\leq 9$,且第$i+1$位可以填$k$。
最终满足条件的总方案数即为$f[1][0]$,这样就可以判断每个询问是否有解。
考虑在有解的情况下如何找到第$k$小的方案。我们称如果状态$i$由状态$j$等累加得到,则$j$是$i$的一个后继状态。从初始状态$(1,0)$开始,按照下一位填的数字从小到大枚举当前状态$S$的每个后继状态$T$,如果$T$的DP值$\geq k$,则说明我们要找的方案在$T$中,且这个方案下这一位已经确定,然后走到$T$状态即可;否则$T$的DP值$<k$,那么将$k$减去$T$的DP值,然后继续考虑其它更大的后继即可。这样单次询问是$O(n)$的,不能接受。
类似树的轻重链剖分,对于每个状态,取其后继状态中DP值最大的状态作为重后继,则每个状态最多只有一个重后继,我们可以倍增求出每个状态往后走$2^k$次重后继后会到达哪个状态,以及那个状态相对当前来说是第几小的方案。对于每个询问,我们先在倍增数组中沿着重后继不断往前走直到必须要走轻后继为止,然后走一次轻后继,再接着沿着倍增数组走重后继。
因为重后继是DP值最大的后继,这意味着每个轻后继的DP值不超过总方案数的一半,所以每走一次轻后继,$k$至少会除以二,最多$O(\log k)$次。
时间复杂度$O(nm\log n+q\log n\log k)$。
#include<cstdio> typedef long long ll; const ll inf=1000000000000000010LL; const int N=50010,M=20,K=17,P=1000000007; int Case,n,m,q,i,j,k,p[N];ll _; char a[N]; int g[M][10]; bool can[N][10]; ll f[N][M],st[K][N][M],en[K][N][M]; char go[K][N][M]; int val[K][N][M]; inline ll fix(ll x){return x<inf?x:inf;} inline int query(ll k){ if(k>f[1][0])return -1; int x=1,y=0,ret=0,i; while(x<=n){ for(i=K-1;~i;i--)if(x+(1<<i)<=n+1&&st[i][x][y]<k&&k<=en[i][x][y]){ ret=(1LL*ret*p[1<<i]+val[i][x][y])%P; k-=st[i][x][y]; y=go[i][x][y]; x+=1<<i; } if(x>n)break; for(i=0;i<10;i++)if(can[x][i]){ ll tmp=f[x+1][g[y][i]]; if(k>tmp)k-=tmp; else{ ret=(10LL*ret+i)%P; x++; y=g[y][i]; break; } } } return ret; } int main(){ for(p[0]=i=1;i<N;i++)p[i]=10LL*p[i-1]%P; scanf("%d",&Case); while(Case--){ scanf("%d%d%d%s",&n,&m,&q,a+1); for(i=0;i<m;i++)for(j=0;j<10;j++)g[i][j]=(i*10+j)%m; for(i=1;i<=n;i++){ if(a[i]=='?')for(j=0;j<10;j++)can[i][j]=1; else{ for(j=0;j<10;j++)can[i][j]=0; can[i][a[i]-'0']=1; } } for(j=0;j<m;j++)f[n+1][j]=j==0; for(i=n;i;i--)for(j=0;j<m;j++){ ll tmp=0; int nxt=-1; ll sz=-1; for(k=0;k<10;k++)if(can[i][k]){ ll now=f[i+1][g[j][k]]; tmp=fix(tmp+now); if(now>sz)nxt=k,sz=now; } f[i][j]=tmp; go[0][i][j]=g[j][nxt]; val[0][i][j]=nxt; ll sum=0; for(k=0;k<nxt;k++)if(can[i][k])sum=fix(sum+f[i+1][g[j][k]]); st[0][i][j]=sum; en[0][i][j]=fix(sum+f[i+1][g[j][nxt]]); } for(k=1;k<K;k++)for(i=1;i+(1<<k)<=n+1;i++)for(j=0;j<m;j++){ int x=go[k-1][i][j],len=1<<(k-1); go[k][i][j]=go[k-1][i+len][x]; val[k][i][j]=(1LL*val[k-1][i][j]*p[len]+val[k-1][i+len][x])%P; st[k][i][j]=fix(st[k-1][i][j]+st[k-1][i+len][x]); en[k][i][j]=fix(st[k-1][i][j]+en[k-1][i+len][x]); } while(q--)scanf("%lld",&_),printf("%d\n",query(_)); } }
L. Stay Real
小根堆中,每个点的权值总是不小于父亲节点的权值。所以无论怎么取,先拿走的数一定不小于后面拿走的数。
此时双方的最优策略就是:贪心选择能取的数字之中最大的数。
时间复杂度$O(n\log n)$。
#include<cstdio> #include<algorithm> using namespace std; typedef long long ll; const int N=100010; int Case,n,i,j,a[N];ll A,B; int main(){ scanf("%d",&Case); while(Case--){ scanf("%d",&n); for(i=1;i<=n;i++)scanf("%d",&a[i]); sort(a+1,a+n+1); A=B=0; for(i=n,j=1;i;i--,j^=1)if(j)A+=a[i];else B+=a[i]; printf("%lld %lld\n",A,B); } }