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$,则有如下三种情况:

  1. $S$完全出现在$B$中:对应的贡献只和$B$的长度有关,可以很方便地计算出答案。
  2. $S$完全出现在$A$中:暴力枚举$A$的每个长度为$|S|$的子串,更新对应询问串的出现次数。
  3. $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. $1\rightarrow n$:这种边必须删除。
  2. $1\rightarrow i\rightarrow n$:边$1\rightarrow i$和边$i\rightarrow n$至少要删掉一条。
  3. $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建图,有两类边:

  1. 对于每条给定的边$(u,v)$:如果不选$u$就必须选$v$,如果不选$v$就必须选$u$。
  2. 对于每个集合:如果选了一个点就不能选其它所有点。

第二类边不能直接建图,但是在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");
}

  

posted @ 2022-01-19 23:27  Claris  阅读(194)  评论(0编辑  收藏  举报