Potyczki Algorytmiczne 2010
Trial Round:
Rectangles
枚举子矩阵的长和宽,乘以对应的子矩形数。
时间复杂度$O(nm)$。
#include<cstdio> int n,m,p,i,j,ans; int main(){ scanf("%d%d%d",&n,&m,&p); for(i=1;i<=n;i++)for(j=1;j<=m;j++)if(2*(i+j)>=p)ans+=(n-i+1)*(m-j+1); printf("%d",ans); }
Round 1:
Orienteering [B]
将环倍长,破环成链。递推预处理出每个点往前一路数值不下降/不上升能到达哪里,即可$O(1)$判断每个起点是否合法。
时间复杂度$O(n)$。
#include<cstdio> const int N=200005; int n,i,a[N],f[N],g[N]; int main(){ scanf("%d",&n); for(i=1;i<=n;i++)scanf("%d",&a[i]),a[i+n]=a[i]; for(i=1;i<=n+n;i++){ f[i]=i; if(i>1&&a[i-1]<=a[i])f[i]=f[i-1]; g[i]=i; if(i>1&&a[i-1]>=a[i])g[i]=g[i-1]; if(f[i]<=i-n+1||g[i]<=i-n+1)return puts("TAK"),0; } puts("NIE"); }
Round 2:
Mushrooms [B]
最优方案一定是从起点出发往右走到某个点$x$,然后在$x$与$x-1$这两个位置左右横跳,枚举右端点$x$后计算对应方案的值,更新答案。
时间复杂度$O(n)$。
#include<cstdio> #include<algorithm> using namespace std; typedef long long ll; const int N=1000005,BUF=8100000; int n,i,t,a[N];ll s[N],ans; char Buf[BUF],*buf=Buf; inline void read(int&a){for(a=0;*buf<48;buf++);while(*buf>47)a=a*10+*buf++-48;} inline ll cal(ll a,ll b,ll k){ ll ret=(a+b)*(k/2); if(k%2)ret+=a; return ret; } int main(){ fread(Buf,1,BUF,stdin),read(n),read(t); for(i=1;i<=n&&i<=t+1;i++){ read(a[i]),s[i]=s[i-1]+a[i]; ans=max(ans,s[i]+cal(a[i-1],a[i],t-i+1)); } printf("%lld",ans); }
Coins [A]
令字符'O'的值为$1$,字符'R'的值为$-k$,问题转化为求最长的值的总和为$0$的子串。
求出前缀和后,将所有下标按前缀和分组,每组里面选取最小的下标$+1$作为左端点、最大的下标作为右端点,更新答案。
时间复杂度$O(n\log n)$。
#include<cstdio> #include<algorithm> using namespace std; const int N=1000005; int n,k,i,j,q[N],ans;long long s[N];char a[N]; inline bool cmp(int x,int y){return s[x]==s[y]?x<y:s[x]<s[y];} int main(){ scanf("%d%d%s",&n,&k,a+1); for(i=1;i<=n;i++){ if(a[i]=='O')s[i]=s[i-1]+1;else s[i]=s[i-1]-k; q[i]=i; } sort(q,q+n+1,cmp); for(i=j=0;i<=n;i=j){ for(j=i;j<=n&&s[q[i]]==s[q[j]];j++); ans=max(ans,q[j-1]-q[i]); } printf("%d",ans); }
Round 3:
Fragments [A]
对于每个给定的区间$[l,r]$,从高位到低位递归枚举数字的每一位,在递归的过程中,如果发现当前已经脱离了上下界$[l,r]$的限制且之前不全是$0$,那么后面这些位可以任填,此时令前面已经确定的部分对应的数字串为$A$,后面未确定的部分的数字串为$B$,询问串为$S$,则有如下三种情况:
- $S$完全出现在$B$中:对应的贡献只和$B$的长度有关,可以很方便地计算出答案。
- $S$完全出现在$A$中:暴力枚举$A$的每个长度为$|S|$的子串,更新对应询问串的出现次数。
- $A$的某个后缀是$S$的前缀:对所有询问串建立Trie,暴力枚举$A$的每个后缀,那么满足条件的$S$在Trie上对应一个子树,更新子树的答案即可。
令$l$为数字的位数,$k$为字符集大小,则时间复杂度为$O(nl^2k+ml)$,需要很精细地实现使得时间复杂度不多$l$。
此外,由于内存限制很紧,需要将Trie的节点数压缩至$O(m)$,且需要将询问分批来做以减小$m$的大小。
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; typedef long long ll; const int N=5005,M=250005,K=19,E=M*2; ll po[K+5]; int n,m,_m,i,j,cur,cnt[K+5]; char l[N][K],r[N][K],e[M][K+1],len[M]; ll ans[M],full[K+5]; int tot; char d[E];//每个点到根的字符数 int who[E];//每个点到根是哪个串 int son[E][10];short mask[E];char low[1027]; int loc[M]; ll f[E]; int at[K+5][K+5]; inline void read(char*a){ ll t; scanf("%lld",&t); for(int i=K-1;~i;i--)a[i]=t%10,t/=10; } inline void ins(int o){ static char s[K+5]; scanf("%s",s+1); int l=strlen(s+1),i; len[o]=l; for(i=1;i<=l;i++){ s[i]-='0'; e[o][i]=s[i]; } int x=1,y=1,z; for(i=1;i<=l;i++){ char w=s[i]; if(x==y){ if(!son[x][w]){ son[x][w]=++tot; d[tot]=l; who[tot]=o; loc[o]=tot; mask[x]|=1<<w; return; } y=son[x][w]; z=w; } if(e[who[y]][i]==w){ if(i==d[y]){ x=y; if(i==l)loc[o]=y; }else if(i==l){ tot++; d[tot]=l; who[tot]=o; loc[o]=tot; son[tot][e[who[y]][i+1]]=y; son[x][z]=tot; mask[tot]|=1<<e[who[y]][i+1]; } continue; } tot++; d[tot]=i-1; who[tot]=who[y]; son[tot][e[who[y]][i]]=y; son[x][z]=tot; mask[tot]|=1<<e[who[y]][i]; x=y=tot; i--; } } inline int go(int x,int y,int z){ if(!x)return 0; if(d[x]>=y){ if(e[who[x]][y]==z)return x; return 0; } return son[x][z]; } ll dfs(int o,int x,int el,int er,int fir){ if(x==K)return 1; if(!el&&!er&&fir<K){ cnt[K-x]++; return po[K-x]; } ll ret=0; for(int i=0;i<10;i++){ int nel=el,ner=er,nfir=fir; if(el){ if(i<l[o][x])continue; if(i!=l[o][x])nel=0; } if(er){ if(i>r[o][x])continue; if(i!=r[o][x])ner=0; } if(fir==K&&i)nfir=x; for(int j=nfir;j<=x;j++){ if(j==x)at[x+1][j]=go(1,1,i); else at[x+1][j]=go(at[x][j],x-j+1,i); } ll tmp=dfs(o,x+1,nel,ner,nfir); for(int j=fir;j<x;j++){ int u=at[x][j]; if(!u)continue; if(d[u]==x-j)f[u]+=tmp; } ret+=tmp; } return ret; } void dfs2(int o,int x,int el,int er,int fir,int u){ if(((!el&&!er)||(x==K))&&fir<K){ /* A=[0..x)B=[x..K) for each suffix T (can be non-empty) of A */ if(u<=1)return; if(fir>cur)return; f[u]++; return; } for(int i=0;i<10;i++){ int nel=el,ner=er,nfir=fir,nu=u; if(el){ if(i<l[o][x])continue; if(i!=l[o][x])nel=0; } if(er){ if(i>r[o][x])continue; if(i!=r[o][x])ner=0; } if(fir==K&&i)nfir=x; if(nfir<K&&x>=cur)nu=go(u,x-cur+1,i); dfs2(o,x+1,nel,ner,nfir,nu); } } void dfs3(int x){ for(int i=mask[x];i;i-=i&-i){ int y=son[x][low[i]]; f[y]+=f[x]; dfs3(y); } } inline ll cal(int x){ ll ret=0; for(int i=x;i<=K;i++)ret+=po[i-x]*cnt[i]*(i-x+1); return ret; } int main(){ scanf("%d%d",&n,&_m); for(po[0]=i=1;i<=K;i++)po[i]=po[i-1]*10; for(i=1;i<=n;i++){ read(l[i]); read(r[i]); } for(i=1;i<1024;i++)low[i]=__builtin_ctz(i); while(_m){ m=min(_m,M-5); _m-=m; tot=1; for(i=1;i<=m;i++)ins(i); for(i=1;i<=n;i++)dfs(i,0,1,1,K); for(i=1;i<=m;i++)ans[i]=f[loc[i]]; for(cur=0;cur<K;cur++){ for(i=1;i<=tot;i++)f[i]=0; for(i=1;i<=n;i++)dfs2(i,0,1,1,K,1); dfs3(1); for(i=1;i<=m;i++){ j=K-cur-len[i]; if(j>=0)ans[i]+=f[loc[i]]*po[j]; } } for(i=1;i<=K;i++)full[i]=cal(i); for(i=1;i<=m;i++)printf("%lld\n",ans[i]+full[len[i]]); for(i=0;i<=K;i++)cnt[i]=0; for(i=0;i<=tot;i++){ mask[i]=f[i]=0; for(j=0;j<10;j++)son[i][j]=0; } } }
Squared Words [B]
$ans=\max_i\left\{LCS(S[1..i],S[i+1..n])\right\}$,共$O(n)$次LCS询问。使用ALCS在$O(n^2)$时间内完成预处理,$O(n)$时间内回答每个询问。
总时间复杂度$O(n^2)$。
#include<cstdio> #include<algorithm> using namespace std; const int N=1005; int n,i,j,f[N][N],g[N][N],ans;char s[N]; inline int ask(int o,int l,int r){ int len=r-l+1,i,j,k,ret=0; for(i=1;i<=len;i++){ j=r-i+1; if(j>=f[o+1][j+1]){ ret++; k=f[o+1][j+1]-1; if(k>=l)ret--; } } return ret; } int main(){ scanf("%d%s",&n,s); for(i=1;i<=n;i++)f[0][i]=i; for(i=1;i<=n;i++)for(j=1;j<=n;j++){ if(s[i-1]==s[j-1]){ f[i][j]=g[i][j-1]; g[i][j]=f[i-1][j]; }else{ f[i][j]=max(f[i-1][j],g[i][j-1]); g[i][j]=min(g[i][j-1],f[i-1][j]); } } for(i=0;i<n-1;i++)ans=max(ans,ask(i,i+1,n-1)); printf("%d",n-ans*2); }
Round 4:
Evacuation [A]
$1$到$n$的边数不超过$3$的路径有如下三种:
- $1\rightarrow n$:这种边必须删除。
- $1\rightarrow i\rightarrow n$:边$1\rightarrow i$和边$i\rightarrow n$至少要删掉一条。
- $1\rightarrow i\rightarrow j\rightarrow n$:边$1\rightarrow i$、边$i\rightarrow j$和边$j\rightarrow n$至少要删掉一条,不难发现删掉中间的边$i\rightarrow j$不优,因此可以视作边$1\rightarrow i$和边$j\rightarrow n$至少要删掉一条。
建立左右各$n$个点的二分图,将第二类和第三类中涉及的边作为连源点/汇点的边加入,并将对应的二选一限制作为无穷边加入,则问题转化为求最小割,即二分图最大匹配,使用bitset优化的匈牙利算法即可。
时间复杂度$O(\frac{n^3}{w})$。
#include<cstdio> typedef unsigned int U; const int N=1005,M=(N>>5)+5; int n,m,l,r,all,i,j,x,y,ans,idl[N],idr[N],who[N]; bool g[N][N];U f[N][M],v[M]; inline void flip(U v[],int x){v[x>>5]^=1U<<(x&31);} bool find(int x){ for(int i=0;i<=all;i++)while(1){ U t=v[i]&f[x][i]; if(!t)break; int y=i<<5|__builtin_ctz(t); flip(v,y); if(!who[y]||find(who[y]))return who[y]=x,1; } return 0; } int main(){ scanf("%d%d",&n,&m); while(m--){ scanf("%d%d",&x,&y); if(x==1&&y==n){ans++;continue;} g[x][y]=1; } for(i=1;i<=n;i++){ if(g[1][i]&&g[i][n])idl[i]=idr[i]=1; for(j=1;j<=n;j++)if(g[1][i]&&g[i][j]&&g[j][n])idl[i]=idr[j]=1; } for(i=1;i<=n;i++){ if(idl[i])idl[i]=++l; if(idr[i])idr[i]=++r; } for(i=1;i<=n;i++){ if(g[1][i]&&g[i][n])flip(f[idl[i]],idr[i]); for(j=1;j<=n;j++)if(g[1][i]&&g[i][j]&&g[j][n])flip(f[idl[i]],idr[j]); } all=r>>5; for(i=1;i<=l;i++){ for(j=0;j<=all;j++)v[j]=~0U; if(find(i))ans++; } printf("%d",ans); }
Map [B]
从左往右考虑每个点,维护考虑过的点中$y$坐标的最小值和最大值,即可$O(1)$判断每个点左上角和左下角是否有点,右上角和右下角的判断同理。
时间复杂度$O(n\log n)$。
#include<cstdio> #include<algorithm> using namespace std; const int N=1000005; int n,d,i,j,l,r,ans,f[N]; struct P{int x,y,p;}a[N]; inline bool cmp(const P&a,const P&b){return a.x<b.x;} void gao(){ l=d,r=0; for(i=1;i<=n;i=j){ for(j=i;j<=n&&a[i].x==a[j].x;j++){ if(l<a[j].y)f[a[j].p]++; if(r>a[j].y)f[a[j].p]++; } for(j=i;j<=n&&a[i].x==a[j].x;j++){ l=min(l,a[j].y); r=max(r,a[j].y); } } } int main(){ scanf("%d%d",&n,&d); for(i=1;i<=n;i++){ scanf("%d%d",&a[i].x,&a[i].y); a[i].p=i; } sort(a+1,a+n+1,cmp); gao(); reverse(a+1,a+n+1); gao(); for(i=1;i<=n;i++)if(f[i]==4)ans++; printf("%d",ans); }
Round 5:
The Goat [A]
首先在$O(n^2\log n)$时间内求出$area[i]$表示被恰好$i$个圆覆盖部分的面积,则每个位置在$k$轮中至少被覆盖一次的概率为$1-(1-\frac{i}{n})^k$,将概率乘以$area[i]$后贡献给答案即可。
#include<cstdio> #include<cmath> #include<algorithm> using namespace std; const int N=1010; const double eps=1e-9; const double PI=acos(-1.0); int sgn(double x){ if(x>eps)return 1; if(x<-eps)return -1; return 0; } struct P{ double x,y; P(){} P(double _x,double _y){x=_x,y=_y;} P operator+(const P&b)const{return P(x+b.x,y+b.y);} P operator-(const P&b)const{return P(x-b.x,y-b.y);} P operator*(double b)const{return P(x*b,y*b);} P operator/(double b)const{return P(x/b,y/b);} double det(const P&b)const{return x*b.y-y*b.x;} P rot90()const{return P(-y,x);} P unit(){return *this/abs();} double abs(){return hypot(x,y);} }; struct Circle{ P o;double r; bool contain(const Circle&v,const int&c)const{ return sgn(r-(o-v.o).abs()-v.r)>c; } bool disjuct(const Circle&v,const int&c)const{ return sgn((o-v.o).abs()-r-v.r)>c; } }; bool isCC(Circle a,Circle b,P&p1,P&p2){ if(a.contain(b,0)||b.contain(a,0)||a.disjuct(b,0))return 0; double s1=(a.o-b.o).abs(); double s2=(a.r*a.r-b.r*b.r)/s1; double aa=(s1+s2)/2,bb=(s1-s2)/2; P mm=(b.o-a.o)*(aa/(aa+bb))+a.o; double h=sqrt(max(0.0,a.r*a.r-aa*aa)); P vv=(b.o-a.o).unit().rot90()*h; p1=mm+vv,p2=mm-vv; return 1; } struct EV{ P p;double ang;int add; EV(){} EV(const P&_p,double _ang,int _add){p=_p,ang=_ang,add=_add;} bool operator<(const EV&a)const{return ang<a.ang;} }eve[N*2]; int E,cnt,C,K,L,i,j;Circle c[N]; bool g[N][N],overlap[N][N]; double Area[N],ans; int cX[N],cY[N],cR[N]; bool contain(int i,int j){ return (sgn(c[i].r-c[j].r)>0||sgn(c[i].r-c[j].r)==0&&i<j)&&c[i].contain(c[j],-1); } int main(){ scanf("%d%d%d",&C,&K,&L); for(i=0;i<C;i++){ scanf("%d%d",&cX[i],&cY[i]); cR[i]=L; c[i].o=P(cX[i],cY[i]); c[i].r=cR[i]; } for(i=0;i<=C;i++)Area[i]=0; for(i=0;i<C;i++)for(j=0;j<C;j++)overlap[i][j]=contain(i,j); for(i=0;i<C;i++)for(j=0;j<C;j++)g[i][j]=!(overlap[i][j]||overlap[j][i]||c[i].disjuct(c[j],-1)); for(i=0;i<C;i++){ E=0;cnt=1; for(j=0;j<C;j++)if(j!=i&&overlap[j][i])cnt++; for(j=0;j<C;j++)if(i!=j&&g[i][j]){ P aa,bb; isCC(c[i],c[j],aa,bb); double A=atan2(aa.y-c[i].o.y,aa.x-c[i].o.x); double B=atan2(bb.y-c[i].o.y,bb.x-c[i].o.x); eve[E++]=EV(bb,B,1); eve[E++]=EV(aa,A,-1); if(B>A)cnt++; } if(E==0)Area[cnt]+=PI*c[i].r*c[i].r; else{ sort(eve,eve+E); eve[E]=eve[0]; for(j=0;j<E;j++){ cnt+=eve[j].add; Area[cnt]+=eve[j].p.det(eve[j+1].p)*0.5; double theta=eve[j+1].ang-eve[j].ang; if(theta<0)theta+=PI*2; Area[cnt]+=theta*c[i].r*c[i].r*0.5-sin(theta)*c[i].r*c[i].r*0.5; } } } for(i=1;i<=C;i++)ans+=(Area[i]-Area[i+1])*(1-pow(1-1.0*i/C,K)); return printf("%.15f",ans),0; }
Map 2 [B]
将横坐标离散化,对于离散化后的每一段连续的横坐标,预处理出它左边/右边所有点的纵坐标的最小值和最大值,则这一段的纵坐标能取的范围是对应区间的交,将交集大小乘以这一段横坐标对应的横坐标数,贡献给答案即可。
时间复杂度$O(n\log n)$。
#include<cstdio> #include<algorithm> using namespace std; const int N=1000005; int n,d,i,j,l,r,sufl[N],sufr[N];long long ans; struct P{int x,y;}a[N]; inline bool cmp(const P&a,const P&b){return a.x<b.x;} inline int cal(int L,int R){return max(min(R,r)-max(L,l)-1,0);} int main(){ scanf("%d%d",&n,&d); for(i=1;i<=n;i++)scanf("%d%d",&a[i].x,&a[i].y); sort(a+1,a+n+1,cmp); l=d,r=0; for(i=n+1;i;i--){ if(i<=n)l=min(l,a[i].y),r=max(r,a[i].y); sufl[i]=l,sufr[i]=r; } l=d,r=0; for(i=1;i<=n;i=j){ ans+=1LL*(a[i].x-a[i-1].x-1)*cal(sufl[i],sufr[i]); for(j=i;j<=n&&a[i].x==a[j].x;j++); ans+=cal(sufl[j],sufr[j]); for(j=i;j<=n&&a[i].x==a[j].x;j++)l=min(l,a[j].y),r=max(r,a[j].y); } printf("%lld",ans); }
Termites [A]
两个人拿走的石子堆数是定值$cnt$,石子数总和是定值$sum$,考虑求出先手总和$-$后手总和的值$dif$,那么就能推出两人各自的总和。
对于相邻的三个元素$A,B,C$,如果$A\leq B$且$B\geq C$,那么$A$和$C$一定属于同一个人,$B$一定属于另一个人,可以将这三个数合并为一个数$A-B+C$。不断迭代直至无法继续合并数字。
迭代完毕后,对于最左侧或者最右侧贴墙的部分,令贴墙的数为$A$,$A$旁边的数为$B$,那么如果$A\geq B$,则这两个数一定是最后由两人轮流拿走,可以将它们消去,把墙推进两格,并给$dif$加上$(-1)^{cnt}\times(B-A)$。不断迭代直至无法继续消除数字。
如上处理完毕后,可以保证先手每次一定能取到全局最大值,因此将所有数混在一起排序,从大到小贪心取即可。
时间复杂度$O(n\log n)$。
#include<cstdio> #include<algorithm> typedef long long ll; const int N=1000010; int n,m,all,i,j,a[N],cnt;ll st[N],q[N],sum,dif; inline void ext(int x){ st[++cnt]=x; while(cnt>2&&st[cnt-2]<=st[cnt-1]&&st[cnt-1]>=st[cnt]){ st[cnt-2]+=st[cnt]-st[cnt-1]; cnt-=2; } } inline void solve(int l,int r){ int i; cnt=0; if(r==n)for(i=r;i>=l;i--)ext(a[i]); else for(i=l;i<=r;i++)ext(a[i]); if(l==1||r==n){ for(i=1;i<cnt;i+=2)if(st[i]>=st[i+1]){ if(all&1)dif-=st[i+1]-st[i]; else dif+=st[i+1]-st[i]; }else break; for(;i<=cnt;i++)q[++m]=st[i]; }else for(i=1;i<=cnt;i++)q[++m]=st[i]; } int main(){ scanf("%d",&n); for(i=1;i<=n;i++)scanf("%d",&a[i]),sum+=a[i],all+=!!a[i]; for(i=1;i<=n;)if(a[i]){ for(j=i;j<=n&&a[j];j++); solve(i,j-1); i=j; }else i++; std::sort(q+1,q+m+1); for(i=m,j=0;i;i--,j^=1)if(!j)dif+=q[i];else dif-=q[i]; printf("%lld %lld",(sum+dif)/2,(sum-dif)/2); }
Round 6:
Byton Tree [B]
按照可采摘时间区间的右端点$r$从小到大依次考虑每个叶子。
假设当前考虑到了叶子$x$,暴力找到$x$最高的祖先$y$,满足$y$子树内$l$的最大值不超过$r[x]$,并标记沿途经过的点。
若一个点已经被标记过则说明$x$已经被摘走,否则此时需要在$y$处进行一次采摘。
时间复杂度$O(n\log n)$。
#include<cstdio> #include<algorithm> using namespace std; const int N=1000005; int n,m,i,l[N],r[N],f[N],q[N],ans;bool vis[N]; inline bool cmp(int x,int y){return r[x]<r[y];} void init(int y){ int k,x=++n; f[x]=y; scanf("%d",&k); if(!k){ scanf("%d%d",&l[x],&r[x]); q[++m]=x; } while(k--)init(x); } inline void gao(int x){ int o=r[x]; vis[x]=1; while(f[x]&&l[f[x]]<=o){ x=f[x]; if(vis[x])return; vis[x]=1; } ans++; } int main(){ init(0); for(i=n;i;i--)l[f[i]]=max(l[f[i]],l[i]); sort(q+1,q+m+1,cmp); for(i=1;i<=m;i++)gao(q[i]); printf("%d",ans); }
Firm [B]
按深度离线后,使用树状数组支持单点修改以及子树点数查询。
时间复杂度$O(n\log n)$。
#include<cstdio> #include<vector> using namespace std; typedef vector<int>V; #define rep(x) for(V::iterator it=x.begin();it!=x.end();it++) const int N=100005,M=N*2; int m,i,x,y,e[N][2],ans[N],d[N],st[N],en[N],dfn,f[N]; V g[N],G[M];char op[N][5]; void dfs(int x){ st[x]=++dfn; rep(g[x])dfs(*it); en[x]=dfn; } inline void add(int x,int p){for(;x<=dfn;x+=x&-x)f[x]+=p;} inline int ask(int x){int t=0;for(;x;x-=x&-x)t+=f[x];return t;} int main(){ scanf("%d",&m); for(i=1;i<=m;i++){ scanf("%s%d%d",op[i],&x,&y); e[i][0]=x,e[i][1]=y; if(op[i][0]=='Z'){ d[x]=d[y]+1; g[y].push_back(x); G[d[x]].push_back(x); }else{ G[d[x]+y+1].push_back(-i); } } dfs(1); for(i=0;i<M;i++){ rep(G[i]){ x=*it; if(x>0)add(st[x],1); else{ y=e[-x][0]; ans[-x]=ask(en[y])-ask(st[y]-1); } } rep(G[i])if(*it>0)add(st[*it],-1); } for(i=1;i<=m;i++)if(op[i][0]=='P')printf("%d\n",ans[i]); }
Planning the Roadworks [A]
Kosaraju求出SCC后,缩点得到一个DAG。
对于DAG上的某条边$x\rightarrow y$,如果去掉这条边后$x$仍然能到达$y$,那么可以删掉这条边,即要判断能否通过拓扑序介于$x$和$y$之间的点从$x$间接到达$y$。按拓扑序DP出$can[x][y]$表示$x$能否到$y$,对于每个点按照另一个端点的拓扑序枚举边进行转移,假设现在枚举到了边$x\rightarrow y$,则拓扑序介于它们之间的点已经更新过$can[x][y]$,利用$can[x][y]$即可判断出去掉边$x\rightarrow y$后$x$是否仍然能到达$y$。使用bitset优化,时间复杂度$O(\frac{nm}{w})$。
对于SCC内部的边,由于Kosaraju只用到了不超过$2n$条边,留下这些边后对于每条边暴力检查去掉它后还能否从$x$到达$y$即可,时间复杂度$O(n^2)$。
总时间复杂度为$O(\frac{nm}{w}+n^2)$。
#include<cstdio> #include<bitset> #include<vector> #include<algorithm> using namespace std; typedef pair<int,int>P; typedef vector<P>V; #define rep(it,v) for(V::iterator it=v.begin();it!=v.end();it++) const int N=5005,M=100005; int n,m,cnt,i,j,x,y,e[M][2],q[N],t,f[N],pool[N],tot,ans; V g[N],h[N]; bool vis[N],used[M],del[M]; bitset<N>can[N]; void dfs1(int x){ vis[x]=1; rep(it,g[x])if(!vis[it->first])used[it->second]=1,dfs1(it->first); q[++t]=x; } void dfs2(int x){ vis[x]=0,f[x]=cnt; pool[++tot]=x; rep(it,h[x])if(vis[it->first])used[it->second]=1,dfs2(it->first); } void dfs3(int x){ vis[x]=1; rep(it,g[x])if(!vis[it->first]&&!del[it->second])dfs3(it->first); } int main(){ scanf("%d%d",&n,&m); for(i=1;i<=m;i++){ scanf("%d%d",&x,&y); e[i][0]=x,e[i][1]=y; g[x].push_back(P(y,i)); h[y].push_back(P(x,i)); } for(i=1;i<=n;i++)if(!vis[i])dfs1(i); for(i=n;i;i--)if(vis[q[i]])cnt++,dfs2(q[i]); for(i=1;i<=n;i++)g[i].clear(); for(i=1;i<=n;i++)rep(it,h[pool[i]]){ x=f[pool[i]],y=f[it->first]; if(x==y)continue; g[y].push_back(P(x,it->second)); } for(i=1;i<=cnt;i++)can[i][i]=1; for(i=cnt;i;i--)rep(it,g[i]){ if(can[i][it->first])del[it->second]=1; else can[i]|=can[it->first]; } for(i=1;i<=n;i++)g[i].clear(); for(i=1;i<=m;i++){ x=e[i][0],y=e[i][1]; if(f[x]!=f[y])continue; if(!used[i]){del[i]=1;continue;} g[x].push_back(P(y,i)); } for(i=1;i<=m;i++){ x=e[i][0],y=e[i][1]; if(f[x]!=f[y])continue; if(!used[i])continue; del[i]=1; for(j=1;j<=n;j++)vis[j]=0; dfs3(x); if(!vis[y])del[i]=0; } for(i=1;i<=m;i++)if(del[i])ans++; printf("%d\n",ans); for(i=1;i<=m;i++)if(del[i])printf("%d\n",i); }
Riddle [A]
注意到“每个集合恰好选择一个点”可以放宽成“每个集合最多选择一个点”,对于最后求出的方案里,如果某个集合没选点,任选一个就好了。
考虑2-SAT建图,有两类边:
- 对于每条给定的边$(u,v)$:如果不选$u$就必须选$v$,如果不选$v$就必须选$u$。
- 对于每个集合:如果选了一个点就不能选其它所有点。
第二类边不能直接建图,但是在Kosaraju算法中DFS图的时候,每个点$x$和$x$所在集合内除了$x$之外的所有点都连了一条第二类边,需要用一个数据结构跳过那些已经搜过的且不是$x$的点。用一个支持双端pop的队列维护就可以了,如果这个集合不是只剩$x$没搜过,那么两端至少可以消费一个点。
时间复杂度$O(n+m+k)$。
#include<cstdio> const int N=2000010,M=1000010,BUF=25000000; char Buf[BUF],*buf=Buf; int n,m,K,o,i,j,x,y,S[M],T[M],st[M],en[M],pool[M],tot,at[M]; int e[M][2],g[N],v[N],nxt[N],ed; int q[N],t,f[N]; bool vis[N]; inline void read(int&a){for(a=0;*buf<48;buf++);while(*buf>47)a=a*10+*buf++-48;} inline void add(int x,int y){v[++ed]=y;nxt[ed]=g[x];g[x]=ed;} void dfs1(int x){ if(vis[x])return; vis[x]=1; for(int i=g[x];i;i=nxt[i])dfs1(v[i]); if(x>n)while(S[at[x-n]]<=T[at[x-n]]){ if(pool[S[at[x-n]]]!=x-n)dfs1(pool[S[at[x-n]]++]); else if(pool[T[at[x-n]]]!=x-n)dfs1(pool[T[at[x-n]]--]); else break; } q[++t]=x; } void dfs2(int x){ if(!vis[x])return; vis[x]=0,f[x]=o; for(int i=g[x];i;i=nxt[i])dfs2(v[i]); if(x<=n)while(S[at[x]]<=T[at[x]]){ if(pool[S[at[x]]]!=x)dfs2(pool[S[at[x]]++]+n); else if(pool[T[at[x]]]!=x)dfs2(pool[T[at[x]]--]+n); else break; } } int main(){ fread(Buf,1,BUF,stdin);read(n),read(m),read(K); for(i=1;i<=m;i++){ read(x),read(y); e[i][0]=x,e[i][1]=y; add(x,y+n),add(y,x+n); } for(i=1;i<=K;i++){ read(y); st[i]=tot+1; while(y--){ read(x); at[x]=i; pool[++tot]=x; } en[i]=tot; } for(i=1;i<=K;i++)S[i]=st[i],T[i]=en[i]; for(i=1;i<=n+n;i++)if(!vis[i])dfs1(i); for(ed=0,i=1;i<=n+n;i++)g[i]=0; for(i=1;i<=m;i++){ x=e[i][0],y=e[i][1]; add(y+n,x),add(x+n,y); } for(i=1;i<=K;i++)S[i]=st[i],T[i]=en[i]; for(i=t;i;i--)if(vis[q[i]])o++,dfs2(q[i]); for(i=1;i<=n;i++)if(f[i]==f[i+n])return puts("NIE"),0; puts("TAK"); for(i=1;i<=K;i++)st[i]=pool[st[i]]; for(i=1;i<=n;i++)if(f[i]<f[i+n])st[at[i]]=i; for(i=1;i<=K;i++)printf("%d ",st[i]); return 0; }
Trial Finals:
Variable Subsequences
设$f_i$表示以$i$为结尾的合法子序列数,枚举上一个点转移,由于只有一类转移不合法,可以使用总和减去那一类的贡献来进行$O(1)$转移。
时间复杂度$O(n)$。
#include<cstdio> const int N=500005,P=1000000007; int n,s,t,x,f[N]; int main(){ s=1; scanf("%d",&n); while(n--){ scanf("%d",&x); t=(s-f[x]+P)%P; f[x]=(f[x]+t)%P; s=(s+t)%P; } printf("%d",(s+P-1)%P); }
Rectangles 2
同Rectangles。
Finals:
Sweets
假设最后分成的三组糖果分别为$A$个、$B$个、$C$个,其中$A\leq B\leq C$,则要最小化$C-A$。
将$n$箱糖果分成前一半和后一半,对于每一半暴力枚举出所有$O(3^{\frac{n}{2}})$个可能的分组方案,对于一个方案记录$a=B-A,b=C-B$,那么需要在前一半方案中找到一个方案$i$,在后一半方案中找到一个方案$j$,满足$A\leq B\leq C$,即$B-A\geq 0$且$C-B\geq 0$,也就是:
- $a_i+a_j\geq 0$
- $b_i+b_j\geq 0$
并最小化$C-A$,即$(C-B)+(B-A)=a_i+a_j+b_i+b_j=(a_i+b_i)+(a_j+b_j)$。
将所有方案按$a$排序,枚举前一半的方案$i$,双指针出满足$a_i+a_j\geq 0$的$j$的范围,按$b$建立树状数组,查询区间$a_j+b_j$的最大值,更新答案。
时间复杂度$O(n3^{\frac{n}{2}})$。
#include<cstdio> #include<algorithm> using namespace std; typedef long long ll; const int N=27,M=535005; const ll inf=1LL<<60; int n,m,ca,cb,i,j,w[N];ll ans,tmp,c[M],f[M]; struct P{ll x,y;}a[M],b[M]; inline bool cmp(const P&a,const P&b){return a.x<b.x;} void dfsl(int o,ll x,ll y){ if(o>m){ a[++ca].x=-x; a[ca].y=-y; return; } dfsl(o+1,x-w[o],y); dfsl(o+1,x+w[o],y-w[o]); dfsl(o+1,x,y+w[o]); } void dfsr(int o,ll x,ll y){ if(o>n){ b[++cb].x=x; b[cb].y=y; return; } dfsr(o+1,x-w[o],y); dfsr(o+1,x+w[o],y-w[o]); dfsr(o+1,x,y+w[o]); } inline void up(ll&a,ll b){a>b?(a=b):0;} inline void add(int x,ll p){for(;x<=ca;x+=x&-x)up(f[x],p);} inline ll ask(int x){ll t=inf;for(;x;x-=x&-x)up(t,f[x]);return t;} int main(){ scanf("%d",&n); for(i=1;i<=n;i++)scanf("%d",&w[i]); m=n/2; dfsl(1,0,0); dfsr(m+1,0,0); ans=inf; for(i=1;i<=ca;i++)c[i]=a[i].y,f[i]=inf; sort(c+1,c+ca+1); sort(a+1,a+ca+1,cmp); sort(b+1,b+cb+1,cmp); for(i=j=1;i<=cb;i++){ while(j<=ca&&a[j].x<=b[i].x){ add(lower_bound(c+1,c+ca+1,a[j].y)-c,-a[j].x-a[j].y); j++; } up(ans,ask(upper_bound(c+1,c+ca+1,b[i].y)-c-1)+b[i].x+b[i].y); } printf("%lld",ans); }
Acyclic Decomposition
DFS判环,若不存在环则答案为$1$。
否则可以构造答案为$2$的一组解:第一组包含所有满足$u<v$的边,第二组包含所有满足$u>v$的边。
时间复杂度$O(n+m)$。
#include<cstdio> const int N=100005; int n,m,i,x,y,ca,cb,a[N],b[N],g[N],v[N],nxt[N],vis[N],in[N]; bool dfs(int x){ if(vis[x])return 0; vis[x]=in[x]=1; for(int i=g[x];i;i=nxt[i]){ if(in[v[i]])return 1; if(dfs(v[i]))return 1; } return in[x]=0; } int main(){ scanf("%d%d",&n,&m); for(i=1;i<=m;i++){ scanf("%d%d",&x,&y); if(x<y)a[++ca]=i;else b[++cb]=i; v[i]=y,nxt[i]=g[x],g[x]=i; } for(i=1;i<=n;i++)if(dfs(i)){ puts("2"); for(printf("%d",ca),i=1;i<=ca;i++)printf(" %d",a[i]);puts(""); for(printf("%d",cb),i=1;i<=cb;i++)printf(" %d",b[i]);puts(""); return 0; } puts("1"); printf("%d",m); for(i=1;i<=m;i++)printf(" %d",i); }
Divisors
Pollard-Rho分解质因数,计算结果唯一当且仅当每个质因数的计算结果唯一。
对于每个质因数,将所有变量分别设为最小和最大值,计算出式子的结果,判断是否相同即可。
#include<cstdio> #include<algorithm> #include<cstring> using namespace std; typedef long long ll; const int C=2730,S=5,N=600005; int Case,m,cnt,i,j,k,tot,l[N],r[N],f[N],g[N]; ll n,q[100],v[N]; char s[2111111]; ll gcd(ll a,ll b){return b?gcd(b,a%b):a;} inline ll mul(ll a,ll b,ll n){ a%=n,b%=n; if(n<=1100000000)return a*b%n; return(a*b-(ll)(a/(long double)n*b+1e-8)*n+n)%n; } inline ll pow(ll a,ll b,ll n){ ll d=1; a%=n; while(b){ if(b&1)d=mul(d,a,n); a=mul(a,a,n); b>>=1; } return d; } inline bool check(ll a,ll n){ ll m=n-1,x,y;int i,j=0; while(!(m&1))m>>=1,j++; x=pow(a,m,n); for(i=1;i<=j;x=y,i++){ y=pow(x,2,n); if((y==1)&&(x!=1)&&(x!=n-1))return 1; } return y!=1; } inline bool miller_rabin(int times,ll n){ ll a; if(n==1)return 0; if(n==2)return 1; if(!(n&1))return 0; while(times--)if(check(rand()%(n-1)+1,n))return 0; return 1; } inline ll pollard_rho(ll n,int c){ ll i=1,k=2,x=rand()%n,y=x,d; while(1){ i++,x=(mul(x,x,n)+c)%n,d=gcd(y-x,n); if(d>1&&d<n)return d; if(y==x)return n; if(i==k)y=x,k<<=1; } } void findfac(ll n,int c){ if(n==1)return; if(miller_rabin(S,n)){ q[++cnt]=n; return; } ll m=n; while(m==n)m=pollard_rho(n,c--); findfac(m,c),findfac(n/m,c); } int build(){ int x=++tot; l[x]=r[x]=v[x]=0; scanf("%s",s); if(s[0]>='a'&&s[0]<='z')return x; if(s[0]>='0'&&s[0]<='9'){ int len=strlen(s); for(int i=0;i<len;i++)(v[x]*=10)+=s[i]-'0'; return x; } if(s[2]=='D')v[x]=1;else v[x]=2; l[x]=build(); r[x]=build(); return x; } inline bool solve(){ scanf("%lld",&n); tot=0; build(); cnt=0; findfac(n,C); sort(q+1,q+cnt+1); for(i=1;i<=cnt;i=j){ for(j=i;j<=cnt&&q[i]==q[j];j++); ll A=q[i];int B=j-i; for(k=tot;k;k--){ if(l[k]){ if(v[k]==1){//gcd f[k]=min(f[l[k]],f[r[k]]); g[k]=min(g[l[k]],g[r[k]]); }else{ f[k]=max(f[l[k]],f[r[k]]); g[k]=max(g[l[k]],g[r[k]]); } }else if(v[k]){ ll C=v[k];int D=0; while(C%A==0)C/=A,D++; f[k]=g[k]=D; }else{ f[k]=0; g[k]=B; } } if(f[1]!=g[1])return 0; } return 1; } int main(){ scanf("%d",&Case); while(Case--)puts(solve()?"TAK":"NIE"); }
Byteball Match
枚举每个球队$S$,判断$S$能否成为冠军。
由于每个球队都还没结束比赛,因此只要在剩下比赛里都比对方领先$+\infty$个球即可达到最高预期分数,那么只需要判断剩下的比赛能否合理分配$2$分给双方使得每个队伍的分数都不超过$S$的最高预期分数。
将剩下每个球队看作点,点权表示$S$的最高预期分数减去该球队已有的分数。
将剩下还未进行的比赛看作边,那么类似于Hall定理,这有解当且仅当任何一个点集都满足点权和$\geq $诱导子图边数$\times2$,即$\min(点权和-诱导子图边数\times 2)\geq 0$,需要找到一个点集$V$最小化点权和$-$诱导子图边数$\times 2$。
注意到诱导子图边数$\times 2=V$中所有点的度数之和$-$横跨$V$内外的边数,因此问题等价于最小化"每个点的(点权$-$度数)之和$+$横跨$V$内外的边数",对该问题建立最小割模型:
- 源点到每个点的边的边权为$n+$点权$-$度数,割掉这条边表示将这个点选入$V$。
- 每个点到汇点的边边权为$n$,割掉这条边表示不将这个点选入$V$。
- 每条边正边反边的代价都是$1$,表示如果一方选了且另一方没选,则要割掉这条边。
利用HLPP算法$O(n^3)$求最大流判断最小割是否$\geq (n-1)\times n$即可,总时间复杂度$O(n^4)$。
#include<cstdio> #include<cstring> #include<queue> #include<stack> #include<algorithm> using namespace std; namespace HLPP{ const int N = 105, M = 120000, INF = 0x3f3f3f3f; int n, m, s, t; struct E{ int nex, t, v; }; E e[M * 2 + 1]; int h[N + 1], cnt; inline void add_path(int f, int t, int v) { e[++cnt] = (E){h[f], t, v}, h[f] = cnt; } inline void add_flow(int f, int t, int v) { add_path(f, t, v); add_path(t, f, 0); } int ht[N + 1], ex[N + 1], gap[N]; // 高度; 超额流; gap 优化 gap[i] 为高度为 i 的节点的数量 stack<int> B[N]; // 桶 B[i] 中记录所有 ht[v]==i 的v int level; // 溢出节点的最高高度 inline void init(int _n,int _s,int _t){ n=_n,s=_s,t=_t; cnt=1; level=0; memset(h, 0, sizeof(h)); memset(ht, 0, sizeof(ht)); memset(ex, 0, sizeof(ex)); memset(gap, 0, sizeof(gap)); for(int i=0;i<N;i++)while(!B[i].empty())B[i].pop(); } inline int push(int u) { // 尽可能通过能够推送的边推送超额流 bool init = u == s; // 是否在初始化 for (int i = h[u]; i; i = e[i].nex) { const int &v = e[i].t, &w = e[i].v; if (!w || init == false && ht[u] != ht[v] + 1) // 初始化时不考虑高度差为1 continue; int k = init ? w : min(w, ex[u]); // 取到剩余容量和超额流的最小值,初始化时可以使源的溢出量为负数。 if (v != s && v != t && !ex[v]) B[ht[v]].push(v), level = max(level, ht[v]); ex[u] -= k, ex[v] += k, e[i].v -= k, e[i ^ 1].v += k; // push if (!ex[u]) return 0; // 如果已经推送完就返回 } return 1; } inline void relabel(int u) { // 重贴标签(高度) ht[u] = INF; for (int i = h[u]; i; i = e[i].nex) if (e[i].v) ht[u] = min(ht[u], ht[e[i].t]); if (++ht[u] < n) { // 只处理高度小于 n 的节点 B[ht[u]].push(u); level = max(level, ht[u]); ++gap[ht[u]]; // 新的高度,更新 gap } } inline bool bfs_init() { memset(ht, 0x3f, sizeof(ht)); static int q[N + 5]; int head = 0, tail = 0; ht[q[0] = t] = 0; while (head <= tail) { // 反向 BFS, 遇到没有访问过的结点就入队 int u = q[head++]; for (int i = h[u]; i; i = e[i].nex) { const int &v = e[i].t; if (e[i ^ 1].v && ht[v] > ht[u] + 1) ht[v] = ht[u] + 1, q[++tail] = v; } } return ht[s] != INF; // 如果图不连通,返回 0 } // 选出当前高度最大的节点之一, 如果已经没有溢出节点返回 0 inline int select() { while (B[level].size() == 0 && level > -1) level--; return level == -1 ? 0 : B[level].top(); } inline int hlpp() { // 返回最大流 if (!bfs_init()) return 0; // 图不连通 memset(gap, 0, sizeof(gap)); for (int i = 1; i <= n; i++) if (ht[i] != INF) gap[ht[i]]++; // 初始化 gap ht[s] = n; push(s); // 初始化预流 int u; while ((u = select())) { B[level].pop(); if (push(u)) { // 仍然溢出 if (!--gap[ht[u]]) for (int i = 1; i <= n; i++) if (i != s && i != t && ht[i] > ht[u] && ht[i] < n + 1) ht[i] = n + 1; // 这里重贴成 n+1 的节点都不是溢出节点 relabel(u); } } return ex[t]; } } const int N=105; int n,m,i,x,y,A,B,f[N],v[N][N],mx,id[N],deg[N]; inline bool check(int S){ int i,j,now=f[S],cnt=2; for(i=1;i<=n;i++)if(!v[S][i])now+=2; if(now<mx)return 0; for(i=1;i<=n;i++)if(i!=S)id[i]=++cnt,deg[i]=0; HLPP::init(n+1,1,2); for(i=1;i<=n;i++)for(j=1;j<=n;j++)if(!v[i][j]&&i!=S&&j!=S){ HLPP::add_flow(id[i],id[j],1); deg[i]++; } for(i=1;i<=n;i++)if(i!=S){ HLPP::add_flow(1,id[i],N+now-f[i]-deg[i]); HLPP::add_flow(id[i],2,N); } return HLPP::hlpp()>=(n-1)*N; } int main(){ scanf("%d%d",&n,&m); for(i=1;i<=n;i++)v[i][i]=1; while(m--){ scanf("%d%d%d%d",&x,&y,&A,&B); v[x][y]=v[y][x]=1; if(A==B)f[x]++,f[y]++; if(A>B)f[x]+=2; if(A<B)f[y]+=2; } mx=n-1; for(i=1;i<=n;i++)mx=max(mx,f[i]); for(i=1;i<=n;i++)if(check(i))printf("%d ",i); }
Army Training
选择最左边的点$O$作为原点,将所有点按$O$极角排序,求出每个点的排名$rk[i]$。
对于任意一个三角形$OAB$ ($rk[A]<rk[B]$),预处理出严格在它内部的点数,即$rk$在$(rk[A],rk[B])$之间且在直线$AB$一侧的点数。枚举$A$后将所有点按$A$极角排序,转一圈的同时用树状数组维护$rk$。
对于每个询问,首先由预处理的三角形信息乘以叉积的正负得到一个初步的答案。这里存在一些顶点$P$满足射线$OP$与多边形交点为奇数,没有被正确地减掉。
枚举多边形上相邻的三个点$X,Y,Z$,根据$X,Y,Z$的相对关系分类讨论判断$Y$是否是这样多算的点即可。
时间复杂度$O(n^2\log n+\sum k)$。
#include<cstdio> #include<algorithm> using namespace std; typedef long long ll; const int N=1005; int n,m,q,i,j,x,y,z,k,ans,rk[N],f[N],w[N],g[N][N],id[N];bool vis[N]; struct P{ int x,y,p; P(){} P(int _x,int _y){x=_x,y=_y;} P operator-(const P&b)const{return P(x-b.x,y-b.y);} int sgn()const{return x?x>0:y>0;} }a[N],b[N],c[N<<1],pivot; inline ll cross(const P&a,const P&b){return 1LL*a.x*b.y-1LL*a.y*b.x;} inline bool cmp(const P&a,const P&b){return cross(a-pivot,b-pivot)>0;} inline bool cmp2(const P&a,const P&b){ if(a.sgn()!=b.sgn())return a.sgn()<b.sgn(); return cross(a,b)>0; } inline void ins(int x){for(;x<n;x+=x&-x)f[x]++;} inline int ask(int x){int t=0;for(;x;x-=x&-x)t+=f[x];return t;} inline bool check(int x,int y,int z){ if(!rk[y])return 0; if(!rk[x])return rk[z]>rk[y]; if(!rk[z])return rk[x]<rk[y]; if(rk[x]<rk[y]&&rk[y]<rk[z])return 1; if((rk[x]<rk[y]&&rk[z]<rk[y])||(rk[x]>rk[y]&&rk[z]>rk[y]))return cross(a[y]-a[x],a[z]-a[y])>0; return 0; } int main(){ scanf("%d%d",&n,&q); for(i=1;i<=n;i++)scanf("%d%d",&a[i].x,&a[i].y),a[i].p=i; for(pivot=a[1],i=2;i<=n;i++)if(a[i].x<pivot.x)pivot=a[i]; for(i=1;i<=n;i++)if(a[i].p!=pivot.p)b[++m]=a[i]; sort(b+1,b+n,cmp); for(i=1;i<n;i++)rk[b[i].p]=i; for(i=1;i<n-1;i++){ for(m=0,j=i+1;j<n;j++){ c[++m]=b[j]; c[m].x-=b[i].x; c[m].y-=b[i].y; } for(j=1;j<=m;j++){ c[j+m]=c[j]; c[j+m].x*=-1; c[j+m].y*=-1; c[j+m].p*=-1; } sort(c+1,c+m+m+1,cmp2); for(j=1;j<=n;j++)f[j]=w[j]=vis[j]=0; for(j=1;j<=m+m;j++){ k=c[j].p; if(k>0){ ins(rk[k]); w[k]-=ask(rk[k]-1); vis[k]=1; }else{ k*=-1; w[k]+=ask(rk[k]-1); if(!vis[k])w[k]+=rk[k]-i-1; } } for(j=i+1;j<n;j++){ k=w[b[j].p]; if(cross(a[b[i].p]-pivot,a[b[j].p]-pivot)>0)k*=-1; g[b[i].p][b[j].p]=k; g[b[j].p][b[i].p]=-k; } } while(q--){ scanf("%d",&m); for(i=0;i<m;i++)scanf("%d",&id[i]); for(ans=i=0;i<m;i++){ x=id[i],y=id[(i+1)%m],z=id[(i+2)%m]; ans+=g[x][y]; if(check(x,y,z))ans--; } printf("%d\n",ans); } }
Blindfold Nim
全$0$时先手胜率为$0$,否则最优策略一定是从上限最大的那一堆拿走一个石子。
假设上限最大的那一堆的石子数在$[0,k]$之间随机,则先手在这一轮的操作有$\frac{k}{k+1}$的概率合法,然后交换先后手,因此令之后子问题的先手胜率为$nxt$,则答案为$\frac{k}{k+1}(1-nxt)$,可以倒推求出原始局面的先手胜率。
时间复杂度$O(n+\sum a)$。
#include<cstdio> const int N=1000000; int n,i,c[N+5],f[N+5];long double ans; int main(){ scanf("%d",&n); while(n--)scanf("%d",&i),c[i]++; for(i=N,n=0;i;i--)while(c[i])c[i]--,c[i-1]++,f[++n]=i; for(i=n;i;i--)ans=(1-ans)*f[i]/(f[i]+1); printf("%.15f",(double)ans); }
Termites 2
按时间顺序从前往后考虑每条边,看作$n$个独立点然后依次加入每条边将它们逐渐连通,那么每个连通块的游戏是独立的,且对于每个连通块,只有一个点可以留下来。
因此对于$x$点所在的连通块,设$a[x]$表示玩家1可以保证最后留下来的点集,设$b[x]$表示玩家2可以保证最后留下来的点集。
按时间顺序从前往后考虑到边$(u,v)$时,不妨设现在轮到了玩家1,令合并$u,v$后的连通块对应的信息为$a',b'$:
- 如果$a[u]$不能保证留下$u$且$a[v]$不能保证留下$v$,那么游戏结束。
- 如果$a[u]$能保证留下$u$且$a[v]$能保证留下$v$,那么如果吃掉$u$,则可以保全$a[v]$;如果吃掉$v$,则可以保全$a[u]$,而对手保全不了任何点,因此$a'=a[u]\ or\ a[v]$,$b'=NULL$。
- 如果$a[u]$能保证留下$u$但是$a[v]$不能保证留下$v$,那么只能吃掉$u$和$v$,保全$a[v]$和$b[v]-v$,因此$a'=a[v]$,$b'=b[v]-v$。
用数组记录每个点是否能被两个玩家保全,并查集维护连通块,建二叉树表示集合的or关系。在清除一个集合时,暴力DFS对应子树修正数组的值即可。
时间复杂度$O(n)$。
#include<cstdio> #include<algorithm> using namespace std; const int N=500005; int n,m,i,x,y,X,Y,f[N<<1],a[N],b[N],ga[N<<1][2],gb[N<<1][2]; int F(int x){return f[x]==x?x:f[x]=F(f[x]);} void dfsa(int x){ if(!x)return; dfsa(ga[x][0]); dfsa(ga[x][1]); if(x<=n)a[x]=0; } void dfsb(int x){ if(!x)return; dfsb(gb[x][0]); dfsb(gb[x][1]); if(x<=n)b[x]=0; } int main(){ scanf("%d",&n); m=n; for(i=1;i<=n;i++)a[i]=b[i]=f[i]=i; for(i=1;i<n;i++){ scanf("%d%d",&x,&y); ++m; f[m]=m; if(i&1){ if(!a[x]&&!a[y])return printf("%d",i),0; if(a[x]&&a[y]){ X=F(x),Y=F(y); //a(M)=a(X) or a(Y), b(M)=NULL ga[m][0]=X; ga[m][1]=Y; dfsb(X); dfsb(Y); }else{ if(!a[x])swap(x,y); X=F(x),Y=F(y); //a(M)=a(Y), b(M)=b(Y)-y b[y]=0; ga[m][0]=gb[m][0]=Y; dfsa(X); dfsb(X); } }else{ if(!b[x]&&!b[y])return printf("%d",i),0; if(b[x]&&b[y]){ X=F(x),Y=F(y); //b(M)=b(X) or b(Y), a(M)=NULL gb[m][0]=X; gb[m][1]=Y; dfsa(X); dfsa(Y); }else{ if(!b[x])swap(x,y); X=F(x),Y=F(y); //b(M)=b(Y), a(M)=a(Y)-y a[y]=0; ga[m][0]=gb[m][0]=Y; dfsa(X); dfsb(X); } } f[X]=f[Y]=m; } puts("-1"); }