2021“MINIEYE杯”中国大学生算法设计超级联赛(3)

A. Bookshop

对树进行轻重链剖分并求出DFS序,在DFS的过程中先DFS一个点的重儿子,再DFS它的轻儿子们,那么每条重链在DFS序上是连续的。对于每个询问$(x,y,z)$,令$t$为$x$和$y$的LCA,那么从$x$出发走到$t$的过程在DFS序里对应从右往左的$O(\log n)$个区间,从$t$出发走到$y$的过程也对应DFS序里从左往右的$O(\log n)$个区间。考虑离线询问,把所有询问分成两个步骤来统一处理:

  • 对于每个询问,处理从$x$往上走到LCA的过程,即从右往左拆成$O(\log n)$个区间。
  • 对于每个询问,处理从LCA往下走到$y$的过程,即从左往右拆成$O(\log n)$个区间。

上述两个步骤是类似的,以第一步为例,可以维护一个数据结构$T$,从右往左扫描整棵树的DFS序,假设扫到了DFS序的第$i$个位置,那么需要依次处理以下几类事件:

  • 对于每个询问拆出来的右端点为$i$的区间,将对应询问插入数据结构$T$中。
  • 令第$i$个位置代表的点为$x$,则需要将数据结构$T$中所有$z$值至少为$p_x$的询问对应的$z$值都减去$p_x$。
  • 对于每个询问拆出来的左端点为$i$的区间,将对应询问从数据结构$T$中删除。

因此我们需要维护一个数据结构$T$,支持插入数字、删除数字、将所有至少为$k$的数字都减去$k$。考虑用平衡树$T$按$z$从小到大维护所有询问,那么插入删除是基本功能,将所有至少为$k$的$z$值都减去$k$时:

  • $z$值在$[0,k)$的数不需要改动。
  • $z$值在$[k,2k)$的数需要减去$k$,其值减少至少一半,可以暴力修改,每个询问总计会被修改$O(\log z)$次。
  • $z$值在$[2k,+\infty)$的数需要减去$k$,其相对排名不会发生改变,可以打标记实现。

因此最多会有$O(n+q\log n+q\log z)$次平衡树操作,每次操作时间复杂度为$O(\log q)$,总时间复杂度为$O((n+q\log n+q\log z)\log q)$。

#include<cstdio>
const int N=100010,M=N*73,inf=~0U>>1;
int Case,n,m,i,j,x,y,w[N],g[N],v[N<<1],nxt[N<<1],ed;
int f[N],d[N],size[N],son[N],top[N],loc[N],q[N],dfn;
int e[N],ga[N],gd[N],rga[N],rgd[N],V[M],NXT[M],ED;
inline void add(int x,int y){v[++ed]=y;nxt[ed]=g[x];g[x]=ed;}
inline void ADD(int&x,int y){V[++ED]=y;NXT[ED]=x;x=ED;}
void dfs(int x,int y){
  f[x]=y;
  d[x]=d[y]+1;
  size[x]=1;
  for(int i=g[x];i;i=nxt[i])if(v[i]!=y){
    dfs(v[i],x);
    size[x]+=size[v[i]];
    if(size[v[i]]>size[son[x]])son[x]=v[i];
  }
}
void dfs2(int x,int y){
  top[x]=y;
  q[++dfn]=x;
  loc[x]=dfn;
  if(son[x])dfs2(son[x],y);
  for(int i=g[x];i;i=nxt[i])if(v[i]!=son[x]&&v[i]!=f[x])dfs2(v[i],v[i]);
}
inline void put(int x,int y,int z){
  while(top[x]!=top[y]){
    if(d[top[x]]>d[top[y]]){
      ADD(ga[loc[x]],z);
      ADD(gd[loc[top[x]]],z);
      x=f[top[x]];
    }else{
      ADD(rga[loc[top[y]]],z);
      ADD(rgd[loc[y]],z);
      y=f[top[y]];
    }
  }
  if(d[x]>d[y]){
    ADD(ga[loc[x]],z);
    ADD(gd[loc[y]],z);
  }else{
    ADD(rga[loc[x]],z);
    ADD(rgd[loc[y]],z);
  }
}
namespace DS{
int a[N],val[N],tag[N],son[N][2],f[N],root;
inline void add1(int x,int p){
  if(!x)return;
  val[x]+=p;
  tag[x]+=p;
}
inline void pb(int x){
  if(tag[x]){
    add1(son[x][0],tag[x]);
    add1(son[x][1],tag[x]);
    tag[x]=0;
  }
}
inline void rotate(int x){
  int y=f[x],w=son[y][1]==x;
  son[y][w]=son[x][w^1];
  if(son[x][w^1])f[son[x][w^1]]=y;
  if(f[y]){
    int z=f[y];
    if(son[z][0]==y)son[z][0]=x;
    if(son[z][1]==y)son[z][1]=x;
  }
  f[x]=f[y];son[x][w^1]=y;f[y]=x;
}
inline void splay(int x,int w){
  int s=1,i=x,y;a[1]=x;
  while(f[i])a[++s]=i=f[i];
  while(s)pb(a[s--]);
  while(f[x]!=w){
    y=f[x];
    if(f[y]!=w){if((son[f[y]][0]==y)^(son[y][0]==x))rotate(x);else rotate(y);}
    rotate(x);
  }
  if(!w)root=x;
}
inline void ask(int k){//splay first >= k to root
  int x=root,t,y=0;
  while(x){
    pb(y=x);
    if(val[x]>=k)t=x,x=son[x][0];else x=son[x][1];
  }
  if(t!=y)splay(y,0);
  splay(t,0);
}
inline void ins(int x){
  son[x][0]=son[x][1]=f[x]=tag[x]=0;
  int y=root;
  while(1){
    pb(y);
    int w=val[x]>val[y];
    if(!son[y][w]){
      son[y][w]=x;
      f[x]=y;
      break;
    }
    y=son[y][w];
  }
  splay(x,0);
}
inline void change(int k){
  while(1){
    ask(k);
    int x=root;
    if(val[x]<k*2){
      int y=son[x][1];
      while(son[y][0])y=son[y][0];
      splay(y,x);
      son[y][0]=son[x][0];
      f[y]=0;
      f[son[x][0]]=y;
      root=y;
      val[x]-=k;
      ins(x);
    }else{
      val[x]-=k;
      add1(son[x][1],-k);
      while(son[x][1])x=son[x][1];
      splay(x,0);
      val[x]=inf;
      return;
    }
  }
}
inline void init(){
  for(int i=m+1;i<=m+2;i++)son[i][0]=son[i][1]=f[i]=tag[i]=0;
  val[m+1]=-1;
  val[m+2]=inf;
  root=m+1;
  son[m+1][1]=m+2;
  f[m+2]=m+1;
}
inline void Del(int x){
  splay(x,0);
  e[x]=val[x];
  int A=son[x][0],B=son[x][1];
  f[A]=f[B]=0;
  while(son[A][1])A=son[A][1];
  splay(A,0);
  son[A][1]=B;
  f[B]=A;
}
inline void Ins(int x){
  val[x]=e[x];
  ins(x);
}
}
int main(){
  scanf("%d",&Case);
  while(Case--){
    scanf("%d%d",&n,&m);
    for(i=1;i<=n;i++)scanf("%d",&w[i]);
    for(i=0;i<=n;i++)g[i]=f[i]=d[i]=size[i]=son[i]=top[i]=ga[i]=gd[i]=rga[i]=rgd[i]=0;
    ed=dfn=ED=0;
    for(i=1;i<n;i++)scanf("%d%d",&x,&y),add(x,y),add(y,x);
    dfs(1,0);
    dfs2(1,1);
    for(i=1;i<=m;i++)scanf("%d%d%d",&x,&y,&e[i]),put(x,y,i);
    DS::init();
    for(i=n;i;i--){
      for(j=ga[i];j;j=NXT[j])DS::Ins(V[j]);
      DS::change(w[q[i]]);
      for(j=gd[i];j;j=NXT[j])DS::Del(V[j]);
    }
    for(i=1;i<=n;i++){
      for(j=rga[i];j;j=NXT[j])DS::Ins(V[j]);
      DS::change(w[q[i]]);
      for(j=rgd[i];j;j=NXT[j])DS::Del(V[j]);
    }
    for(i=1;i<=m;i++)printf("%d\n",e[i]);
  }
}

  

B. Destinations

每个人有三条可行的路线,第$j$条起点为$s_i$,终点为$e_{i,j}$,代价为$c_{i,j}$,容易发现这三条路线存在公共点(起点),因此与下述问题等价:给定$3m$ 条链,选择$m$条没有公共点的链,使得代价之和最小。注意到最多只能选择$m$条公共点的链,因此这又等价于选择最多条没有公共点的链,在此基础之上需要最小化代价之和。对于一条链,将其收益设置为$10^6(m+1)-c_{i,j}$,则该收益值在$10^6m$进制下为$(1,10^6-c_{i,j})$,总收益不会发生进位。我们的目标是最大化总收益,如果总收益在$10^6m$进制下的高位等于$m$则有解,对应的代价和为$10^6m$减去总收益$10^6m$进制下的低位。

现在问题转化为:给定若干条树链,选择总收益最大的一些链使得两两没有公共点。这是经典问题,一个简单的解法是考虑其对偶问题:在树上选择尽量少的点,每个点可以重复选多次,满足每条树链上选择的点数至少为其收益值。那么对偶问题可以贪心解决:从深到浅DFS这棵树,在考虑$x$点时,处理所有LCA为$x$的链$(u,v,w)$,统计$u$到$v$路径上选择的点数$cnt$,若不足$w$,则在LCA(即$x$点)处补充$w-cnt$个点。因此需要一个数据结构支持单点修改、查询链和,令$f_x$ 表示$x$到根路径上的点权和,那么单点修改对$f$的影响是子树加,可以通过树状数组维护差分实现。

时间复杂度$O((n+m)\log n)$。

#include<cstdio>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=200010,M=100010*3,VAL=1000000;
int Case,n,m,i,j,x,y,z,g[N],v[N<<1],nxt[N<<1],ed;
int f[N],d[N],size[N],son[N],top[N],st[N],en[N],dfn;
int G[N],X[M],Y[M],W[M],NXT[M],ED;
ll base,ans,bit[N];
inline void add(int x,int y){v[++ed]=y;nxt[ed]=g[x];g[x]=ed;}
inline void ADD(int o,int x,int y,int z){X[++ED]=x;Y[ED]=y;W[ED]=z;NXT[ED]=G[o];G[o]=ED;}
void dfs(int x,int y){
  f[x]=y;
  d[x]=d[y]+1;
  size[x]=1;
  for(int i=g[x];i;i=nxt[i])if(v[i]!=y){
    dfs(v[i],x);
    size[x]+=size[v[i]];
    if(size[v[i]]>size[son[x]])son[x]=v[i];
  }
}
void dfs2(int x,int y){
  top[x]=y;
  st[x]=++dfn;
  if(son[x])dfs2(son[x],y);
  for(int i=g[x];i;i=nxt[i])if(v[i]!=f[x]&&v[i]!=son[x])dfs2(v[i],v[i]);
  en[x]=dfn;
}
inline int lca(int x,int y){
  while(top[x]!=top[y]){
    if(d[top[x]]<d[top[y]])swap(x,y);
    x=f[top[x]];
  }
  return d[x]<d[y]?x:y;
}
inline ll ask(int x){
  ll ret=0;
  x=st[x];
  for(;x;x-=x&-x)ret+=bit[x];
  return ret;
}
inline void modify(int x,ll p){for(;x<=n;x+=x&-x)bit[x]+=p;}
void go(int x){
  for(int i=g[x];i;i=nxt[i])if(v[i]!=f[x])go(v[i]);
  ll now=0;
  for(int i=G[x];i;i=NXT[i]){
    ll C=base+VAL-W[i],tmp=ask(X[i])+ask(Y[i])+now;
    if(C>tmp)now+=C-tmp;
  }
  ans+=now;
  modify(st[x],now);
  modify(en[x]+1,-now);
}
int main(){
  scanf("%d",&Case);
  while(Case--){
    scanf("%d%d",&n,&m);
    for(ed=ED=dfn=ans=i=0;i<=n;i++)g[i]=f[i]=d[i]=size[i]=son[i]=top[i]=G[i]=bit[i]=0;
    for(i=1;i<n;i++)scanf("%d%d",&x,&y),add(x,y),add(y,x);
    dfs(1,0);
    dfs2(1,1);
    for(i=1;i<=m;i++){
      scanf("%d",&x);
      for(j=0;j<3;j++)scanf("%d%d",&y,&z),ADD(lca(x,y),x,y,z);
    }
    base=1LL*m*VAL;
    go(1);
    if(ans/base==m){
      ans%=base;
      ans=base-ans;
    }else ans=-1;
    printf("%lld\n",ans);
  }
}

  

C. Forgiving Matching

对于$S$的每个长度为$m$的子串,统计其与$T$匹配的位置数$f_i$,即可得到该子串被认为匹配的最小的$k$值。两个字符匹配当且仅当它们字符相等,或者至少有一个是通配符。假设没有通配符的存在,枚举$0$到$9$每个字符$c$,那么如果$S_i=T_j=c$,则$f_{i-j}$应该加上$1$,可以翻转$T$串后通过FFT求出$f$。

现在考虑通配符的影响,$S$一个子串与$T$通过通配符匹配的位置数$=S$对应子串中通配符的数量$+T$中通配符的数量$-$对应位置都是通配符的位置数量。$S$对应子串中通配符的数量可以使用前缀和求得,对应位置都是通配符的位置数量同样可以通过FFT求得。

时间复杂度$O(11n\log n)$。

#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
const int N=524305;
const double pi=acos(-1.0);
struct comp{
  double r,i;comp(double _r=0,double _i=0){r=_r,i=_i;}
  comp operator+(const comp&x)const{return comp(r+x.r,i+x.i);}
  comp operator-(const comp&x)const{return comp(r-x.r,i-x.i);}
  comp operator*(const comp&x)const{return comp(r*x.r-i*x.i,r*x.i+i*x.r);}
  comp conj(){return comp(r,-i);}
}w[N],ww[N],A[N],B[N];
int pos[N],Case,n,m,k,i,j,o,s[N],f[N],ans[N];char a[N],b[N];
inline void FFT(comp a[],int n,int o){
  for(int i=1;i<n;i++)if(i<pos[i])swap(a[i],a[pos[i]]);
  for(int d=0,k=__builtin_ctz(n);(1<<d)<n;d++){
    int m=1<<d,m2=m<<1;
    for(int i=0;i<n;i+=m2)for(int j=0;j<m;j++){
      comp&A=a[i+j+m],&B=a[i+j],t=(o==1?w[j<<(k-d)]:ww[j<<(k-d)])*A;
      A=B-t;B=B+t;
    }
  }
  if(o==-1)for(int i=0;i<n;i++)a[i].r/=n;
}
int main(){
  scanf("%d",&Case);
  while(Case--){
    scanf("%d%d%s%s",&n,&m,a,b);
    reverse(b,b+m);
    for(k=1;k<=n+m-2;k<<=1);
    j=__builtin_ctz(k)-1;
    for(i=0;i<k;i++)pos[i]=pos[i>>1]>>1|((i&1)<<j);
    for(i=0;i<k;i++)w[i]=comp(cos(pi*i/k),sin(pi*i/k));
    for(i=0;i<k;i++)ww[i]=w[i],ww[i].i*=-1;
    for(i=0;i<n;i++){
      s[i]=0;
      if(i)s[i]+=s[i-1];
      if(a[i]=='*')s[i]++;
    }
    int cnt=0;
    for(i=0;i<m;i++)if(b[i]=='*')cnt++;
    for(i=m-1;i<n;i++){
      f[i]=cnt+s[i];
      if(i>=m)f[i]-=s[i-m];
    }
    for(o=0;o<=10;o++){
      char target=o+'0';
      if(o==10)target='*';
      for(i=0;i<k;i++)A[i]=comp(0,0);
      for(i=0;i<n;i++)if(a[i]==target)A[i].r=1;
      for(i=0;i<m;i++)if(b[i]==target)A[i].i=1;
      FFT(A,k,1);
      for(i=0;i<k;i++){
        j=(k-i)&(k-1);
        B[i]=(A[i]*A[i]-(A[j]*A[j]).conj())*comp(0,-0.25);
      }
      FFT(B,k,-1);
      for(i=m-1;i<n;i++){
        int tmp=((int)(B[i].r+0.5));
        if(o<10)f[i]+=tmp;else f[i]-=tmp;
      }
    }
    for(i=0;i<=m;i++)ans[i]=0;
    for(i=m-1;i<n;i++)ans[m-f[i]]++;
    for(i=0;i<=m;i++){
      if(i)ans[i]+=ans[i-1];
      printf("%d\n",ans[i]);
    }
  }
}

  

D. Game on Plane

两条直线存在公共点当且仅当它们重合或者它们斜率不同,因此Bob的最优策略一定是避开斜率出现次数最多的那些直线。Alice为了让Bob与尽量多的直线相交,最优策略就是最小化斜率出现次数的最大值,所以不断从每种斜率的直线中各选一种即可。

时间复杂度$O(n\log n)$。

#include<cstdio>
#include<algorithm>
using namespace std;
typedef pair<int,int>P;
const int N=100005;
int Case,n,i,j,k,f[N];
P a[N];
inline int abs(int x){return x>0?x:-x;}
int gcd(int a,int b){return b?gcd(b,a%b):a;}
int main(){
  scanf("%d",&Case);
  while(Case--){
    scanf("%d",&n);
    for(i=1;i<=n;i++){
      int x1,y1,x2,y2;
      scanf("%d%d%d%d",&x1,&y1,&x2,&y2);
      int dx=x2-x1,dy=y2-y1;
      if(dx==0)dy=1;
      else if(dy==0)dx=1;
      else{
        if(dx<0)dx=-dx,dy=-dy;
        int d=gcd(abs(dx),abs(dy));
        dx/=d,dy/=d;
      }
      a[i]=P(dx,dy);
    }
    sort(a+1,a+n+1);
    for(i=1;i<=n;i++)f[i]=0;
    for(i=1;i<=n;i=j){
      for(j=i;j<=n&&a[i]==a[j];j++);
      for(k=1;k<=j-i;k++)f[k]++;
    }
    for(i=j=1;i<=n;i++){
      while(!f[j])j++;
      f[j]--;
      printf("%d\n",i-j);
    }
  }
}

  

E. Kart Race

从1号点开始DFS整个图,并把出栈序列记下来,那么若$x$能到达$y$,显然$x$晚于$y$出栈。因为图是平面图,考虑最有代表性的两种遍历方式:顺时针遍历和逆时针遍历,那么可以得到两个出栈序列。设$a_i$表示顺时针遍历图时$i$点的出栈序,$b_i$表示逆时针遍历图时$i$点的出栈序,那么$x$能到达$y$当且仅当$a_x>a_y$且$b_x>b_y$。

按照题意,选出来的点应当满足两两不可达,因此把$a_i$看作横坐标,$b_i$看作纵坐标,那么选了$(a_i,b_i)$就不能选它左下角以及右上角的所有点,因此选出来的点一定满足从左往右$a$递增且$b$递减,问题转化为求价值和最大的下降子序列,可以用树状数组优化朴素DP在$O(n\log n)$的时间内得到最优解。

对于字典序最小最优解的求解,有一个方法是在DP值里直接用长度为$n$的01串来记录当前方案里选了哪些点,转移的时候需要支持字典序大小的比较、拷贝以及把单点从0修改成1。因此考虑使用可持久线段树来记录方案,那么单点修改的时间复杂度为$O(\log n)$,比较两个方案的字典序时根据左子树是否相同来决定往左还是往右递归比较,时间复杂度也为$O(\log n)$。在这里,注意到每个点只会加入一次,因此如果两棵线段树的根节点的指针不同,那么表示的01串一定不同,不需要额外维护Hash值。

时间复杂度$O(n\log^2n)$。

#include<cstdio>
#include<algorithm>
#include<vector>
using namespace std;
typedef long long ll;
const int N=100005,M=N*21;
int Case,n,m,i,x,y,w[N],cnt,dfn1[N],dfn2[N],q[N],fin[N];bool vis[N];
vector<int>g[N];
struct P{
  int x,y;
  P(){}
  P(int _x,int _y){x=_x,y=_y;}
  P operator-(const P&b)const{return P(x-b.x,y-b.y);}
}a[N],pivot;
int tot,l[M],r[M];
struct E{
  ll sum;int root;
  E(){}
  E(ll _sum,int _root){sum=_sum,root=_root;}
}bit[N],tmp,ans;
inline ll cross(const P&a,const P&b){return 1LL*a.x*b.y-1LL*a.y*b.x;}
inline bool cmp(int x,int y){return cross(a[x]-pivot,a[y]-pivot)>0;}
int ins(int x,int a,int b,int c){
  int y=++tot;
  if(a==b)return y;
  int mid=(a+b)>>1;
  if(c<=mid)l[y]=ins(l[x],a,mid,c),r[y]=r[x];
  else l[y]=l[x],r[y]=ins(r[x],mid+1,b,c);
  return y;
}
inline bool smaller(int x,int y){
  if(x==y)return 0;
  int a=1,b=n,mid;
  while(a<b){
    mid=(a+b)>>1;
    if(l[x]==l[y]){
      a=mid+1;
      x=r[x];
      y=r[y];
    }else{
      b=mid;
      x=l[x];
      y=l[y];
    }
  }
  return x>y;
}
void go(int x,int a,int b){
  if(!x)return;
  if(a==b){
    fin[++cnt]=a;
    return;
  }
  int mid=(a+b)>>1;
  go(l[x],a,mid);
  go(r[x],mid+1,b);
}
inline void up(E&a,const E&b){
  if(a.sum>b.sum)return;
  if(a.sum<b.sum){a=b;return;}
  if(smaller(b.root,a.root))a.root=b.root;
}
void dfs1(int x){
  if(vis[x])return;
  vis[x]=1;
  for(int i=0;i<g[x].size();i++)dfs1(g[x][i]);
  dfn1[x]=++cnt;
}
void dfs2(int x){
  if(vis[x])return;
  vis[x]=1;
  for(int i=((int)g[x].size())-1;i>=0;i--)dfs2(g[x][i]);
  dfn2[x]=++cnt;
  q[cnt]=x;
}
inline void modify(int x,const E&p){for(;x<=n;x+=x&-x)up(bit[x],p);}
inline E ask(int x){E t(0,0);for(;x;x-=x&-x)up(t,bit[x]);return t;}
int main(){
  scanf("%d",&Case);
  while(Case--){
    scanf("%d%d",&n,&m);
    cnt=0;
    for(i=0;i<=n;i++){
      g[i].clear();
      vis[i]=0;
      bit[i]=E(0,0);
    }
    for(i=1;i<=n;i++)scanf("%d%d%d",&a[i].x,&a[i].y,&w[i]);
    while(m--)scanf("%d%d",&x,&y),g[x].push_back(y);
    for(i=1;i<=n;i++){
      pivot=a[i];
      sort(g[i].begin(),g[i].end(),cmp);
    }
    dfs1(1);
    for(cnt=0,i=1;i<=n;i++)vis[i]=0;
    dfs2(1);
    ans=E(0,0);
    for(i=n;i;i--){
      x=q[i];
      tmp=ask(dfn1[x]);
      tmp.sum+=w[x];
      tmp.root=ins(tmp.root,1,n,x);
      up(ans,tmp);
      modify(dfn1[x],tmp);
    }
    printf("%lld\n",ans.sum);
    cnt=0;
    go(ans.root,1,n);
    for(i=1;i<=cnt;i++)printf("%d%c",fin[i],i<cnt?' ':'\n');
    for(i=0;i<=tot;i++)l[i]=r[i]=0;
    tot=0;
  }
}

  

F. New Equipments II

网络流建图:

  • 左边$n$个点表示$n$个工人,右边$n$个点表示$n$台设备,引入源汇$S$和$T$。
  • 由$S$向第$i$个工人连边,容量$1$,费用$a_i$。
  • 由第$i$个设备向$T$连边,容量$1$,费用$b_i$。
  • 如果工人$i$可以匹配设备$j$,那么由工人$i$向设备$j$连边,容量$1$,费用$0$。

我们的目标就是求出流量$=1,2,3,\dots,n$时的最大费用流,那么依次增广$n$次,每次需要高效地找到增广路然后进行增广,由于增广路长度为$O(n)$,因此算法的瓶颈位于寻找增广路之上。

在每一次增广中,我们需要找到一条$S$到$T$的费用最大的路径,注意到非$0$费用只会出现在每个点连到源汇的边上,因此等价于寻找一个工人$x$和一个设备$y$,满足它们在之前的增广中没有被使用过,$x$通过一开始的边以及增广后添加的$O(n)$条用于反悔的匹配边可以到达$y$,且$a_x+b_y$最大。枚举右侧每个点$y$,只要找到左侧能到达它的点权最大的点,那么就可以找到最优的点对以及对应的增广路。按照$a$从大到小依次从左侧每个点开始BFS,记录每个点第一次被左侧哪个点遍历到,那么它就表示左侧能到达它的点权最大的点。在这里由于图是用补图的方式给出的,需要使用补图BFS的方式,即维护目前还未遍历过的点集$R$,遍历到点$x$时,将$R$修改为$x$在补图中的出边集,由于补图只有$m$条边,因此按照这种方式BFS的时间复杂度为$O(n+m)$。

一共要增广$n$轮,每轮寻找增广路的时间复杂度为$O(n+m)$,故总时间复杂度为$O(n^2+nm)$。

#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=4005;
int Case,n,m,i,j,x,y,a[N],b[N],p[N];ll ans,tmp;
vector<int>g[N];
int pool[N],cp,q[N],h,t,S,fl[N],fr[N],who[N],pre[N];bool vis[N],ban[N];
inline bool cmp(int x,int y){return a[x]>a[y];}
inline void ext(int x){
  if(vis[x])return;
  vis[x]=1;
  int i,m=0,y;
  for(i=0;i<g[x].size();i++)ban[g[x][i]]=1;
  for(i=1;i<=cp;i++){
    y=pool[i];
    if(who[y])continue;
    if(ban[y]){pool[++m]=y;continue;}
    who[y]=S;
    pre[y]=x;
    q[++t]=y;
  }
  cp=m;
  for(i=0;i<g[x].size();i++)ban[g[x][i]]=0;
}
inline void bfs(){
  h=1,t=0;
  ext(S);
  while(h<=t){
    int x=q[h++];
    if(fr[x])ext(fr[x]);
  }
}
inline int aug(){
  int i,x,y,best=-1,X,Y,now;
  for(i=1;i<=n;i++)vis[i]=who[i]=0;
  for(i=1;i<=n;i++)pool[i]=i;
  cp=n;
  for(i=1;i<=n;i++){
    S=p[i];
    if(!fl[S])bfs();
  }
  for(i=1;i<=n;i++)if(!fr[i]&&who[i]){
    now=b[i]+a[who[i]];
    if(now>best)best=now,X=i;
  }
  if(best<0)return -1;
  Y=who[X];
  while(pre[X]!=Y){
    y=pre[X];
    fr[X]=y;
    swap(X,fl[y]);
  }
  fl[Y]=X;
  fr[X]=Y;
  return best;
}
int main(){
  scanf("%d",&Case);
  while(Case--){
    scanf("%d%d",&n,&m);
    for(i=1;i<=n;i++)fl[i]=fr[i]=0,g[i].clear();
    for(i=1;i<=n;i++)scanf("%d",&a[i]);
    for(i=1;i<=n;i++)scanf("%d",&b[i]);
    while(m--)scanf("%d%d",&x,&y),g[x].push_back(y);
    for(i=1;i<=n;i++)p[i]=i;
    sort(p+1,p+n+1,cmp);
    ans=0;
    for(i=1;i<=n;i++){
      tmp=aug();
      if(tmp<0){
        for(j=i;j<=n;j++)puts("-1");
        break;
      }
      ans+=tmp;
      printf("%lld\n",ans);
    }
  }
}

  

G. Photoshop Layers

预处理出$f_i$表示图层$i$左侧第一个合成方式为''普通''的图层。对于每个询问,求出$r$左侧第一个合成方式为''普通''的图层$f_r$,则中间的部分都是''线性减淡'',可以用前缀和求出结果,最后与$255$取最小值。

时间复杂度$O(n+q)$。

#include<cstdio>
const int N=100005;
int Case,n,m,i,x,y,t[N],r[N],g[N],b[N],f[N];
inline int ask(int*s,int l,int r){
  int x=f[r],ret;
  if(x<l)ret=s[r]-s[l-1];
  else ret=s[r]-s[x-1];
  return ret<255?ret:255;
}
int main(){
  scanf("%d",&Case);
  while(Case--){
    scanf("%d%d",&n,&m);
    for(i=1;i<=n;i++){
      scanf("%d%X",&t[i],&x);
      b[i]=x&255;
      x>>=8;
      g[i]=x&255;
      x>>=8;
      r[i]=x;
      r[i]+=r[i-1];
      g[i]+=g[i-1];
      b[i]+=b[i-1];
      if(t[i]==1)f[i]=i;else f[i]=f[i-1];
    }
    while(m--){
      scanf("%d%d",&x,&y);
      printf("%02X%02X%02X\n",ask(r,x,y),ask(g,x,y),ask(b,x,y));
    }
  }
}

  

H. Restore Atlantis II

考虑一维的情况,随机数据下$n$条线段的线段并可以用特别少的极长线段来表示,在二维情况下也是成立的。一个简单的实现方式是在$x$方向将所有关键点提取出来,相邻两个关键点之间用std::vector记录$y$方向的线段并由哪些极长线段构成,接着依次考虑相邻两段$x$方向的关键点中间的部分,如果它们记录的std::vector相同,那么就把这两段合并成同一段,起到压缩的效果。如此一来,随机数据下,任何一个区间的矩形并都可以用大约几十的信息来记录。

剩下的问题就是查询区间矩形并,如果使用线段树维护区间矩形并,那么预处理需要$O(n)$次信息合并,每次查询需要$O(\log n)$次信息合并,效率较低。考虑将$n$个矩形分成$O(\frac{n}{\log n})$块,每块$O(\log n)$个矩形,预处理出每一块块内的前后缀矩形并,同时用ST表预处理出任意两块之间的矩形并,那么预处理需要$O(n)$次信息合并。对于每个查询$[l,r]$:

  • 如果$l$和$r$位于不同的块里,那么最终需要的信息等于$l$所在块内的后缀信息并$+r$所在块内的前缀信息并$+$中间跨越的整块的信息并,需要$O(1)$次信息合并。
  • 如果$l$和$r$位于同一块里,由于数据随机,这样的询问数量期望为$O(\log n)$,直接暴力求出信息并即可。

最终一共需要$O(n+q)$次信息合并,程序运行效率受信息合并实现方式的影响较大。

#include<cstdio>
#include<vector>
using namespace std;
typedef long long ll;
typedef pair<int,int>P;
typedef vector<P>V;
typedef pair<int,V>PI;
typedef vector<PI>VI;
const int N=100010,K=7,M=(N>>K)+5,MAXL=1005;
int Case,n,m,tot,i,j,x,y,st[M],en[M],Log[M];
struct E{int xl,xr,yl,yr;}e[N];
VI pre[N],suf[N],f[10][M];
//pool[i]:[pool[i-1].first,pool[i].first]
inline void addpoint(VI&pool,int x){
  int n=pool.size(),i;
  for(i=0;i<n;i++)if(pool[i].first==x)return;
  static PI now[MAXL];
  int m=0;
  for(i=0;i<n;i++)if(pool[i].first<x)now[m++]=pool[i];else break;
  now[m].first=x;
  now[m].second.clear();
  if(i<n)now[m].second=pool[i].second;
  for(m++;i<n;i++)now[m++]=pool[i];
  pool.resize(m);
  for(i=0;i<m;i++)pool[i]=now[i];
}
inline void addinterval(V&pool,int l,int r){
  static P now[MAXL];
  int n=pool.size(),i,m=0,L=0,R=-1;
  for(i=0;i<n;i++)if(pool[i].first<l){
    if(pool[i].first>R){
      if(L<R)now[m++]=P(L,R);
      L=pool[i].first;
    }
    if(R<pool[i].second)R=pool[i].second;
  }else break;
  if(l>R){
    if(L<R)now[m++]=P(L,R);
    L=l;
  }
  if(R<r)R=r;
  for(;i<n;i++){
    if(pool[i].first>R){
      if(L<R)now[m++]=P(L,R);
      L=pool[i].first;
    }
    if(R<pool[i].second)R=pool[i].second;
  }
  if(L<R)now[m++]=P(L,R);
  pool.resize(m);
  for(i=0;i<m;i++)pool[i]=now[i];
}
inline void ext(VI&pool,const E&p){
  addpoint(pool,p.xl);
  addpoint(pool,p.xr);
  int n=pool.size(),i,m=0;
  for(i=1;i<n;i++)if(pool[i-1].first>=p.xl&&pool[i].first<=p.xr)addinterval(pool[i].second,p.yl,p.yr);
  for(i=0;i<n;i++){
    while(m&&pool[m-1].second==pool[i].second)m--;
    pool[m++]=pool[i];
  }
  pool.resize(m);
}
inline void mergeinterval(const V&A,const V&B,V&C){
  int ca=A.size(),cb=B.size(),i=0,j=0,m=0,L=0,R=-1;P tmp;
  static P now[MAXL];
  while(i<ca||j<cb){
    if(i>=ca)tmp=B[j++];
    else if(j>=cb)tmp=A[i++];
    else tmp=A[i].first<B[j].first?A[i++]:B[j++];
    if(tmp.first>R){
      if(L<R)now[m++]=P(L,R);
      L=tmp.first;
    }
    if(R<tmp.second)R=tmp.second;
  }
  if(L<R)now[m++]=P(L,R);
  C.resize(m);
  for(i=0;i<m;i++)C[i]=now[i];
}
inline void mergepool(const VI&A,const VI&B,VI&C){
  int ca=A.size(),cb=B.size(),cc=0,i=0,j=0,m=0;
  static PI now[MAXL];
  while(i<ca&&j<cb){
    mergeinterval(A[i].second,B[j].second,now[cc].second);
    if(A[i].first<B[j].first)now[cc++].first=A[i++].first;
    else if(A[i].first>B[j].first)now[cc++].first=B[j++].first;
    else{
      now[cc++].first=A[i++].first;
      j++;
    }
  }
  while(i<ca)now[cc++]=A[i++];
  while(j<cb)now[cc++]=B[j++];
  for(i=0;i<cc;i++){
    while(m&&now[m-1].second==now[i].second)m--;
    now[m++]=now[i];
  }
  C.resize(m);
  for(i=0;i<m;i++)C[i]=now[i];
}
inline ll ask(const VI&pool){
  ll ans=0;
  for(int i=1;i<pool.size();i++){
    int tmp=0,n=pool[i].second.size();
    for(int j=0;j<n;j++)tmp+=pool[i].second[j].second-pool[i].second[j].first;
    ans+=1LL*tmp*(pool[i].first-pool[i-1].first);
  }
  return ans;
}
inline ll query(int l,int r){
  int L=l>>K,R=r>>K,i;
  VI pool;
  if(L==R)for(i=l;i<=r;i++)ext(pool,e[i]);
  else{
    mergepool(suf[l],pre[r],pool);
    L++,R--;
    if(L<=R){
      i=Log[R-L+1];
      mergepool(f[i][L],pool,pool);
      mergepool(f[i][R-(1<<i)+1],pool,pool);
    }
  }
  return ask(pool);
}
int main(){
  scanf("%d",&Case);
  for(i=2;i<M;i++)Log[i]=Log[i>>1]+1;
  while(Case--){
    scanf("%d%d",&n,&m);
    tot=n>>K;
    for(i=1;i<=n;i++)en[i>>K]=i;
    for(i=n;i;i--)st[i>>K]=i;
    for(i=1;i<=n;i++)scanf("%d%d%d%d",&e[i].xl,&e[i].yl,&e[i].xr,&e[i].yr);
    for(i=0;i<=tot;i++){
      x=st[i],y=en[i];
      VI pool;
      for(j=x;j<=y;j++){
        ext(pool,e[j]);
        pre[j]=pool;
      }
      pool.clear();
      for(j=y;j>=x;j--){
        ext(pool,e[j]);
        suf[j]=pool;
      }
      f[0][i]=pre[y];
    }
    for(i=1;(1<<i)<=tot+1;i++)for(j=0;j+(1<<i)-1<=tot;j++)mergepool(f[i-1][j],f[i-1][j+(1<<(i-1))],f[i][j]);
    while(m--){
      scanf("%d%d",&x,&y);
      printf("%lld\n",query(x,y));
    }
  }
}

  

I. Rise in Price

设$f_{i,j,k}$表示从$(1,1)$走到$(i,j)$,一路上收集了$k$个钻石时,钻石的单价最高能涨到多少,则$ans=\max(k\times f_{n,n,k})$。

对于固定的$(i,j)$来说,考虑两个状态$f_{i,j,x}$和$f_{i,j,y}$,其中$x<y$,如果$f_{i,j,x}\leq f_{i,j,y}$,则状态$f_{i,j,x}$一定不可能发展为最优解,可以剔除。对于每个$(i,j)$,用列表按照$k$升序保存所有状态,并剔除不可能成为最优解的状态即可。

随机数据下当$n=100$时,单个$(i,j)$的有效状态的峰值$k$大约为几千。时间复杂度$O(n^2k)$。

#include<cstdio>
#include<algorithm>
#include<vector>
using namespace std;
typedef long long ll;
typedef pair<int,int>P;
typedef vector<P>V;
const int N=105;
int Case,n,m,i,j,k,a[N][N],b[N][N];ll ans;V f[N][N];P pool[1000005];
inline void ext(const P&t){
  while(m&&pool[m].second<=t.second)m--;
  if(!m||pool[m].first<t.first)pool[++m]=t;
}
inline void merge(const V&A,const V&B,V&C){
  int ca=A.size(),cb=B.size(),i=0,j=0;
  m=0;
  while(i<ca&&j<cb)ext(A[i].first<B[j].first?A[i++]:B[j++]);
  while(i<ca)ext(A[i++]);
  while(j<cb)ext(B[j++]);
  C.resize(m);
  for(i=0;i<m;i++)C[i]=pool[i+1];
}
int main(){
  scanf("%d",&Case);
  while(Case--){
    scanf("%d",&n);
    for(i=1;i<=n;i++)for(j=1;j<=n;j++)scanf("%d",&a[i][j]);
    for(i=1;i<=n;i++)for(j=1;j<=n;j++)scanf("%d",&b[i][j]);
    f[1][1].resize(1);
    f[1][1][0]=P(a[1][1],b[1][1]);
    for(i=1;i<=n;i++)for(j=1;j<=n;j++){
      if(i==1&&j==1)continue;
      if(i==1)f[i][j]=f[i][j-1];
      else if(j==1)f[i][j]=f[i-1][j];
      else merge(f[i-1][j],f[i][j-1],f[i][j]);
      for(k=0;k<f[i][j].size();k++){
        f[i][j][k].first+=a[i][j];
        f[i][j][k].second+=b[i][j];
      }
    }
    ans=0;
    for(i=0;i<f[n][n].size();i++)ans=max(ans,1LL*f[n][n][i].first*f[n][n][i].second);
    printf("%lld\n",ans);
  }
}

  

J. Road Discount

将原始边作为白边,折扣边作为黑边,由于同一条边不可能选择两次,那么问题等价于求包含恰好$k$条黑边的最小生成树。这是一个经典问题,令$f(k)$表示包含恰好$k$条黑边的最小生成树的边权和,则$f(k)$是一个凸函数,求出$f(k)$的方法为:

  • 选择参数$c$,将每条黑边的边权都加上$c$。
  • 求出修改边权后的图的最小生成树,令$sum(c)$为对应的边权和,$l(c)$为最小生成树中使用黑边数量的最小值,$r(c)$为最小生成树中使用黑边数量的最大值。
  • 二分找到合适的参数$c$,满足$l(c)\leq k\leq r(c)$,则$f(k)=sum(c)-k\times c$。

由于边权在$[1,1000]$之间,因此可以预处理出$c=-1000\dots1000$的所有信息,一共需要求$O(c)$次最小生成树。注意到如果对黑边或者白边单独求最小生成树,则非树边不可能用到,因此可以将边数缩减至$O(n)$。

总时间复杂度$O(m\log n+nc\log n)$。

#include<cstdio>
#include<algorithm>
using namespace std;
typedef pair<int,int>P;
const int N=1005,M=200005,V=1000;
int Case,n,m,i,f[N];P fl[V*2+5];
struct E{int x,y,w;}a[M],b[M];
inline bool cmp(const E&a,const E&b){return a.w<b.w;}
int F(int x){return f[x]==x?x:f[x]=F(f[x]);}
inline bool merge(int x,int y){
  if(F(x)==F(y))return 0;
  f[f[x]]=f[y];
  return 1;
}
inline void reduce(E*e){
  sort(e+1,e+m+1,cmp);
  for(int i=1;i<=n;i++)f[i]=i;
  for(int i=1,cnt=0;i<=m;i++)if(merge(e[i].x,e[i].y))e[++cnt]=e[i];
}
inline P call(int k){
  for(int i=1;i<=n;i++)f[i]=i;
  int A=1,B=1,sum=0,cnt=0;
  while(A<n&&B<n){
    if(a[A].w<=b[B].w+k){
      if(merge(a[A].x,a[A].y))sum+=a[A].w;
      A++;
    }else{
      if(merge(b[B].x,b[B].y))sum+=b[B].w+k,cnt++;
      B++;
    }
  }
  while(A<n){
    if(merge(a[A].x,a[A].y))sum+=a[A].w;
    A++;
  }
  while(B<n){
    if(merge(b[B].x,b[B].y))sum+=b[B].w+k,cnt++;
    B++;
  }
  return P(sum,cnt);
}
inline int ask(int k){
  for(int i=-V;i<=V;i++)if(fl[i+V].second<=k)return fl[i+V].first-k*i;
  return -1;
}
int main(){
  scanf("%d",&Case);
  while(Case--){
    scanf("%d%d",&n,&m);
    for(i=1;i<=m;i++){
      scanf("%d%d%d%d",&a[i].x,&a[i].y,&a[i].w,&b[i].w);
      b[i].x=a[i].x,b[i].y=a[i].y;
    }
    reduce(a);
    reduce(b);
    for(i=-V;i<=V;i++)fl[i+V]=call(i);
    for(i=0;i<n;i++)printf("%d\n",ask(i));
  }
}

  

K. Segment Tree with Pruning

线段树上代表区间长度相同的节点的子树点数相同,且最多只有$O(\log n)$种本质不同的区间长度,对区间长度记忆化搜索即可。

时间复杂度$O(\log n\log\log n)$。

#include<cstdio>
#include<map>
using namespace std;
typedef long long ll;
int Case;ll n,k;map<ll,ll>T;
ll build(ll n){
  if(T.find(n)!=T.end())return T[n];
  if(n<=k)return T[n]=1;
  return T[n]=build(n/2)+build(n-n/2)+1;
}
int main(){
  scanf("%d",&Case);
  while(Case--){
    scanf("%lld%lld",&n,&k);
    T.clear();
    printf("%lld\n",build(n));
  }
}

  

L. Tree Planting

算法一:设$f_{i,S}$表示考虑了前$i$个位置,$i$往前$k$个位置的种树情况为$S$的总贡献,时间复杂度$O(n2^k)$。

算法二:将位置$i$放在二维矩阵的$(i\bmod k,\lfloor\frac{i}{k}\rfloor)$处,那么这个矩阵有$k$行$\lceil\frac{n}{k}\rceil$列。如果$(i,j)$种了树,那么$(i-1,j)$、$(i+1,j)$、$(i,j-1)$、$(i,j+1)$都不能种树,除此之外,第一行和最后一行也存在冲突。从上往下,从左往右轮廓线DP,设$f_{i,j,S,T}$表示考虑到了$(i,j)$这个格子,第一行种树情况为$S$,轮廓线上种树情况为$T$的总贡献,时间复杂度$O(n2^{\frac{2n}{k}})$。

注意到两种算法里$S$和$T$必定是独立集,而$k$个点的链的独立集数量为斐波那契数的第$k$项,大约为$O(1.618^k)$,因此算法一可以降至$O(n1.618^k)$,算法二降至$O(n1.618^{\frac{2n}{k}})$。当$k\leq\frac{2n}{k}$,即$k\leq\sqrt{2n}$时使用算法一,当$k>\sqrt{2n}$时使用算法二,可以得到最优复杂度$O(n1.618^{\sqrt{2n}})$。

#include<cstdio>
#include<map>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=305,P=1000000007;
int Case,n,m,i,w[N];
inline void up(int&a,int b){a=a+b<P?a+b:a+b-P;}
inline void upl(int&a,ll b){a=(a+b)%P;}
namespace SMALL{
const int MAXL=121505;
int cnt,q[MAXL],g[MAXL][2];
map<int,int>id;
int pre[MAXL],now[MAXL];
inline void clr(){for(int i=1;i<=cnt;i++)now[i]=0;}
inline void nxt(){for(int i=1;i<=cnt;i++)pre[i]=now[i];}
void dfs(int x,int S){
  if(x==m){
    id[S]=++cnt;
    q[cnt]=S;
    return;
  }
  dfs(x+1,S<<1);
  if(!(S&1))dfs(x+1,S<<1|1);
}
void solve(){
  //m<=24
  cnt=0;
  id.clear();
  dfs(0,0);
  int i,j,S,T,x;
  for(i=1;i<=cnt;i++){
    S=q[i],T=(S<<1)&((1<<m)-1);
    g[i][0]=id[T];
    g[i][1]=0;
    if(!(S>>(m-1)&1))g[i][1]=id[T|1];
  }
  clr();
  now[1]=1;
  nxt();
  for(i=0;i<n;i++){
    clr();
    x=w[i];
    for(j=1;j<=cnt;j++){
      up(now[g[j][0]],pre[j]);
      upl(now[g[j][1]],1LL*pre[j]*x);
    }
    nxt();
  }
  int ans=P-1;
  for(i=1;i<=cnt;i++)up(ans,pre[i]);
  printf("%d\n",ans);
}
}
namespace BIG{
const int K=15,MAXL=505;
int r,c,val[N][K],cnt[K],q[K][MAXL],g[K][MAXL][2];
map<int,int>id[K];
int pre[MAXL],now[MAXL];
inline void clr(int cnt){for(int i=1;i<=cnt;i++)now[i]=0;}
inline void nxt(int cnt){for(int i=1;i<=cnt;i++)pre[i]=now[i];}
inline bool check(int S,int l,int r){
  for(int i=l;i<r;i++)if((S>>i&1)&&(S>>(i+1)&1))return 0;
  return 1;
}
void solve(){
  //c=ceil(n/m)<=12,r>1
  r=c=0;
  int i,j,k,o,S,T,x,y;
  for(i=0;i<n;i++)r=max(r,i%m),c=max(c,i/m);
  r++,c++;
  for(i=0;i<r;i++)for(j=0;j<c;j++)val[i][j]=0;
  for(i=0;i<n;i++)val[i%m][i/m]=w[i];
  for(i=0;i<c;i++){
    //[0..i-1] [i..c-1]
    cnt[i]=0;
    id[i].clear();
    for(S=0;S<1<<c;S++){
      if(!check(S,0,i-1))continue;
      if(!check(S,i,c-1))continue;
      id[i][S]=++cnt[i];
      q[i][cnt[i]]=S;
    }
  }
  for(i=0;i<c;i++)for(j=1;j<=cnt[i];j++){
    S=q[i][j];
    x=S>>i&1;
    T=S^(x<<i);
    g[i][j][0]=id[(i+1)%c][T];
    g[i][j][1]=0;
    if(x)continue;
    g[i][j][1]=id[(i+1)%c][S|(1<<i)];
  }
  int ans=P-1;
  for(o=1;o<=cnt[0];o++){
    clr(cnt[0]);
    S=q[0][o];
    int tmp=1;
    for(i=0;i<c;i++)if(S>>i&1)tmp=1LL*tmp*val[0][i]%P;
    now[o]=tmp;
    nxt(cnt[0]);
    for(i=1;i<r;i++)for(j=0;j<c;j++){
      int A=cnt[j],B=cnt[(j+1)%c],C=val[i][j];
      clr(B);
      for(k=1;k<=A;k++){
        up(now[g[j][k][0]],pre[k]);
        upl(now[g[j][k][1]],1LL*pre[k]*C);
      }
      nxt(B);
    }
    for(i=1;i<=cnt[0];i++)if(!((S>>1)&q[0][i]))up(ans,pre[i]);
  }
  printf("%d\n",ans);
}
}
int main(){
  scanf("%d",&Case);
  while(Case--){
    scanf("%d%d",&n,&m);
    for(i=0;i<n;i++)scanf("%d",&w[i]);
    if(m*m<=n*2)SMALL::solve();else BIG::solve();
  }
}

  

posted @ 2022-01-14 21:08  Claris  阅读(180)  评论(0编辑  收藏  举报