题解 CF1009G 【Allowed Letters】

· · 题解

CF官方题解思路I:

凡是字典序最小的东西都可以贪心,因为只要保证当前位填出的东西尽量大,后面的位都只要保证有个解就行了。比如这道题,就是枚举每一位填什么,然后用网络流判断出后面有无合法解。

我们顺序枚举每一位填什么。我们建出6个点代表'a''f'字符,同时将每位可以填的字符种类状压一下,状压出2^6个点。每个字符向所有包含着它的状态连一条无穷大的边,源点向每个字符连“剩余部分该字符出现次数”边权的边,同时每个bitmask向汇点连“剩余部分该bitmask出现次数”边权的边。这就是相当于用6个字符与2^6个bitmask进行二分图多重匹配

这样,如果跑出来的最大流等于“剩余部分长度”,则当前位填入该字符是合法的。

如果我们就这么直接每次都跑一遍网络流的话,复杂度最差为

代入$n={3*10}^5,A=6$,得复杂度最劣为${3*10}^5*6*{70}^2*192\approx1.7*{10}^{12}$。因为网络流复杂度玄学,不妨乘上一个百分之一的常数,得复杂度约为$1.7*{10}^{10}$,不可能通过本题。 以下是这个TLE的暴力网络流代码: ```cpp #include<bits/stdc++.h> using namespace std; const int lim=1<<6; int n,mask[100100],m,tot[lim],cmp[6]; char s[100100],ss[6],ans[100100]; namespace MaxFlow{ const int N=10000,M=200000; int head[N],cur[N],dep[N],cnt,S,T,res; struct node{ int to,next,val; }edge[M]; void ae(int u,int v,int w){ edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++; edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++; } queue<int>q; inline bool bfs(){ memset(dep,0,sizeof(dep)),q.push(S),dep[S]=1; while(!q.empty()){ register int x=q.front();q.pop(); for(register int i=cur[x]=head[x];i!=-1;i=edge[i].next)if(edge[i].val&&!dep[edge[i].to])dep[edge[i].to]=dep[x]+1,q.push(edge[i].to); } return dep[T]>0; } bool reach; inline int dfs(int x,int flow){ if(x==T){ res+=flow; reach=true; return flow; } int used=0; for(register int &i=cur[x];i!=-1;i=edge[i].next){ if(!edge[i].val||dep[edge[i].to]!=dep[x]+1)continue; register int ff=dfs(edge[i].to,min(edge[i].val,flow-used)); if(ff){ edge[i].val-=ff; edge[i^1].val+=ff; used+=ff; if(used==flow)break; } } return used; } inline void Dinic(){ while(bfs()){ reach=true; while(reach)reach=false,dfs(S,0x3f3f3f3f); } } } using namespace MaxFlow; int che(){ for(register int i=head[S];i!=-1;i=edge[i].next)edge[i].val=cmp[edge[i].to],edge[i^1].val=0; for(register int i=head[T];i!=-1;i=edge[i].next)edge[i^1].val=tot[edge[i].to-6],edge[i].val=0; res=0; Dinic(); return res; } inline void read(int &x){ x=0; register char c=getchar(); while(c>'9'||c<'0')c=getchar(); while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar(); } inline void print(int x){ if(x<=9)putchar('0'+x); else print(x/10),putchar('0'+x%10); } int main(){ scanf("%s",s+1),n=strlen(s+1),S=6+lim,T=6+lim+1; for(register int i=1;i<=n;i++)cmp[s[i]-'a']++; read(m); for(register int i=1,x,y;i<=m;i++){ read(x),scanf("%s",ss),y=strlen(ss); for(register int j=0;j<y;j++)mask[x]|=1<<(ss[j]-'a'); } for(register int i=1;i<=n;i++)if(!mask[i])mask[i]=lim-1; for(register int i=1;i<=n;i++)tot[mask[i]]++; memset(head,-1,sizeof(head)); for(register int i=0;i<6;i++)ae(S,i,cmp[i]); for(register int i=0;i<lim;i++)for(register int j=0;j<6;j++)if(i&(1<<j))ae(j,i+6,0x3f3f3f3f); for(register int i=0;i<lim;i++)ae(i+6,T,tot[i]); Dinic(); if(res!=n){puts("Impossible");return 0;} for(register int i=1;i<=n;i++)for(register int j=0;j<6;j++){ if(!(mask[i]&(1<<j)))continue; if(!cmp[j])continue; tot[mask[i]]--,cmp[j]--; if(che()!=n-i)tot[mask[i]]++,cmp[j]++; else{ans[i]='a'+j;break;} } ans[n+1]='\0'; printf("%s",ans+1); return 0; } ``` 网络流题最有效的优化是什么?**残量网络优化**。在重跑网络流时,不再全体清空边权,而是只清空少量边权,其它部分仍沿用之前结果。 我们考虑当尝试在一个bitmask为$msk$的位置上填入字符$chr$的变化: 首先,我们找出边$(S,chr)$和$(msk,T)$。显然,这两条边必须是满流量的(不然最大流肯定不为$n$)。 我习惯**只记录边的残量**。因此,我们可以观察它们的反边,则反边上的残量即为当前正边上的流量,也就是正边上的流量上限(最大流必定满流)。如果反边残量为0,就意味着正边流量为$0$,就意味着正边流量上限为$0$,就意味着$chr$或$msk$已经一个不剩了,因此肯定是不合法的,直接跳掉。 如果边$(chr,msk)$上流量不为零(即反边有残量),则我们完全可以直接使这条边上流量减一,直接判为合法并退出。 否则,我们找到一条新的增广路$(S\rightarrow 1\rightarrow x\rightarrow T)$,其中这个$x$可以是任何节点。当然,这里面任何一条边,都必须有流量(而不是常规增广路的定义 **“未满流的路径”** ,因为这条增广路是为退流而用)。 当我们找到这样一条增广路后(必然存在,因为边$(S,chr)$出来的流量最终肯定流向汇点),就把增广路上每一条边的流量全都减少一(即反边残量减一,正边残量加一)。 同时,我们从$msk$出发,找一条反向的增广路,道理还是退流,只不过这里找的就是常规增广路(未满流的路径),因为这回的退流就是反边残量加一,正边残量减一。 全都做完以后,删去边$(S,chr)$与$(msk,T)$的各一点流量,因为这次匹配后,$chr$和$msk$的出现次数将会各减少1,固然是需要删去的。 这么一番操作后,我们成功拆掉了两点流量(正增广路的一点,反增广路的一点)。然后,我们再要跑最大流验证不小心多拆的那一点(即边$(chr,msk)$上原应有的那一点)能不能补回来。如果可以,那这次匹配就算成功了,直接退出。否则,匹配失败,去掉两条增广路的所有影响,返回匹配失败。 代码: ```cpp #include<bits/stdc++.h> using namespace std; const int lim=1<<6; int n,mask[100100],m,tot[lim],cmp[6],enm[80][80]; char s[100100],ss[6]; namespace MaxFlow{ const int N=10000,M=200000; int head[N],cur[N],dep[N],cnt,S,T,res; struct node{ int to,next,val; }edge[M]; void ae(int u,int v,int w){ enm[u][v]=cnt,edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++; enm[v][u]=cnt,edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++; } queue<int>q; inline bool bfs(){ memset(dep,0,sizeof(dep)),q.push(S),dep[S]=1; while(!q.empty()){ register int x=q.front();q.pop(); for(register int i=cur[x]=head[x];i!=-1;i=edge[i].next)if(edge[i].val&&!dep[edge[i].to])dep[edge[i].to]=dep[x]+1,q.push(edge[i].to); } return dep[T]>0; } bool reach; inline int dfs(int x,int flow){ if(x==T){ res+=flow; reach=true; return flow; } int used=0; for(register int &i=cur[x];i!=-1;i=edge[i].next){ if(!edge[i].val||dep[edge[i].to]!=dep[x]+1)continue; register int ff=dfs(edge[i].to,min(edge[i].val,flow-used)); if(ff){ edge[i].val-=ff; edge[i^1].val+=ff; used+=ff; if(used==flow)break; } } return used; } inline void Dinic(){ while(bfs()){ reach=true; while(reach)reach=false,dfs(S,0x3f3f3f3f); } } } using namespace MaxFlow; inline void read(int &x){ x=0; register char c=getchar(); while(c>'9'||c<'0')c=getchar(); while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar(); } inline void print(int x){ if(x<=9)putchar('0'+x); else print(x/10),putchar('0'+x%10); } bool fillin(int msk,int cha){ if(!(msk&(1<<cha)))return false; int p1=cha,p2=msk+6; int e1=enm[S][p1],e2=enm[p2][T]; if(!edge[e1^1].val||!edge[e2^1].val)return false; edge[e1^1].val--; edge[e1].val++; vector<int>afe; afe.push_back(e1); for(int i=head[p1],x;i!=-1;i=edge[i].next){ if((i&1)||!edge[i^1].val)continue; afe.push_back(i); edge[i^1].val--; edge[i].val++; x=edge[i].to; for(int j=head[x];j!=-1;j=edge[j].next){ if(j&1)continue; afe.push_back(j); edge[j^1].val--; edge[j].val++; break; } break; } if(edge[e2].val){edge[e2].val--,edge[e1].val--;return true;} afe.push_back(e2); edge[e2^1].val--; edge[e2].val++; for(int i=head[p2],x;i!=-1;i=edge[i].next){ if(!(i&1)||!edge[i].val)continue; afe.push_back(i^1); edge[i^1].val++; edge[i].val--; x=edge[i].to; for(int j=head[x];j!=-1;j=edge[j].next){ if(!(j&1))continue; afe.push_back(j^1); edge[j^1].val++; edge[j].val--; break; } break; } edge[e1].val--; edge[e2].val--; res=0; Dinic(); if(res)return true; edge[e1].val++; edge[e2].val++; for(int i=0;i<afe.size();i++)edge[afe[i]].val--,edge[afe[i]^1].val++; return false; } int main(){ scanf("%s",s+1),n=strlen(s+1),S=6+lim,T=6+lim+1; for(register int i=1;i<=n;i++)cmp[s[i]-'a']++; read(m); for(register int i=1,x,y;i<=m;i++){ read(x),scanf("%s",ss),y=strlen(ss); for(register int j=0;j<y;j++)mask[x]|=1<<(ss[j]-'a'); } for(register int i=1;i<=n;i++)if(!mask[i])mask[i]=lim-1; for(register int i=1;i<=n;i++)tot[mask[i]]++; memset(head,-1,sizeof(head)); for(register int i=0;i<6;i++)ae(S,i,cmp[i]); for(register int i=0;i<lim;i++)for(register int j=0;j<6;j++)if(i&(1<<j))ae(j,i+6,0x3f3f3f3f); for(register int i=0;i<lim;i++)ae(i+6,T,tot[i]); Dinic(); if(res!=n){puts("Impossible");return 0;} for(register int i=1;i<=n;i++)for(register int j=0;j<6;j++)if(fillin(mask[i],j)){putchar('a'+j);break;} return 0; } ``` CF官方题解思路II:有个叫做Hall's theorem的奇怪玩意,我也不会用……