2020牛客暑期多校训练营(第一场)题解
A-B-Suffix Array(后缀数组+sort)
思路:
通过仔细分析与关系,我们可以发现以下的一些规律
对于任意后缀其B数组的第一个元素一定为$0$,并且B数组的开头一定为$01111......$(1的个取决于开头有多少个连续的相同字符)
例如$aaabba = 0111013,aaabb = 01101$
如果两个字符串连续$1$的长度不同,那么更短的那个字典序更小,所以我们可以预处理出一个$dis$数组,其代表$i$位置处的后缀的开头的相同的字符长度
现在我们再接着考虑如果两个$B$数组拥有相同的开头之后我们该怎么处理
对于比较两个字符串的字典序大小,我们肯定希望找到两个字符串第一个不同的位置进行比较,现在问题的关键就在于找到这样的一个位置
在找到位置前,我们还得发现B数组有这样的一个性质
我们可以发现如果长度发现$[i,i+dis[i]]$这个位置的字符串一定是包含了$a$和$b$的(如果不包含那么$dis[i]$会更大),这个是有用的
根据B函数的定义我们可以发现 如果对于一个字符串不断的加入字符,其函数值与$a$和$b$出现的有关,如果$a$和$b$都出现了那么,前面在怎么加入字符对后面的函数都没有影响
即$a$和$b$都出现了可以保护后面的子串的$B$函数,
比如$abaaab=>102114$在前两个都出现了,因此你在前面在怎么加入字符串,都不会改变$2$后面子串的函数值。
有了这样的一个性质,我们就可以对初始的$S$串求$B$数组,然后对于前缀(这里的前缀指开头的$01$序列)已经相同的两个字符串,直接在B数组中比较后半部分
但是,除去前缀的部分可能会有很长的相同部分,如果我们暴力进行比较,肯定会超时,所以此时,我们可以利用后缀数组,在$O(1)$的时间复杂度内求出两个后缀的$LCP$,然后比较$LCP$之后的位置就可以了
最后,我们就可以根据上面描述的规则去编写$cmp$函数进行排序了
此外还有几个值得注意的地方,那就是如果$i+dis[i]$已经大于$n$了,那么起始点更靠后的位置,字典序应该更小
#include<iostream> #include<algorithm> #include<cstring> using namespace std; const int maxn=1e5+10; const int mlog=20; struct Suffix_Array { int s[maxn],sa[maxn],rk[maxn],height[maxn]; int t[maxn],t2[maxn],c[maxn],n; void init() { memset(t, 0, sizeof(int) * (2 * n + 10)); memset(t2, 0, sizeof(int) * (2 * n + 10)); } void build_sa(int m=256) { int *x = t, *y = t2; for(int i=0;i<m;++i) c[i]=0; for(int i=0;i<n;++i) c[x[i]=s[i]]++; for(int i=1;i<m;++i) c[i]+=c[i-1]; for(int i=n-1;i>=0;--i) sa[--c[x[i]]]=i; for(int k=1;k<=n;k<<=1){ int p=0; for(int i=n-1;i>=n-k;--i) y[p++]=i; for(int i=0;i<n;++i) if(sa[i]>=k) y[p++]=sa[i]-k; for(int i=0;i<m;++i) c[i]=0; for(int i=0;i<n;++i) c[x[y[i]]]++; for(int i=1;i<m;++i) c[i] += c[i - 1]; for(int i=n-1;i>=0;--i) sa[--c[x[y[i]]]]=y[i]; swap(x,y); p=1; x[sa[0]]=0; for(int i=1;i<n;++i) x[sa[i]]=y[sa[i-1]]==y[sa[i]]&&y[sa[i-1]+k]==y[sa[i]+k]?p-1:p++; if(p>=n) break; m=p; } } void get_height() { int k=0; for(int i=0;i<n;++i) rk[sa[i]]=i; for(int i=0;i<n;++i){ if(rk[i]>0){ if(k) --k; int j=sa[rk[i]-1]; while(i+k<n&&j+k<n&&s[i+k]==s[j+k]) ++k; height[rk[i]]=k; } } } int d[maxn][mlog],Log[maxn]; void RMQ_init() { Log[0]=-1; for(int i=1;i<=n;++i) Log[i]=Log[i/2]+1; for(int i=0;i<n;++i) d[i][0]=height[i]; for(int j=1;j<=Log[n];++j){ for(int i=0;i+(1<<j)-1<n;++i){ d[i][j]=min(d[i][j-1],d[i+(1<<(j-1))][j-1]); } } } int lcp(int i,int j)//返回下标i开始的后缀与下标j开始的后缀的最长公共前缀。 { if(i==j) return n-i; if(rk[i]>rk[j]) swap(i,j); int x=rk[i]+1,y=rk[j]; int k=Log[y-x+1]; return min(d[x][k],d[y-(1<<k)+1][k]); } pair <int, int> Locate(int l, int r)//返回一个最长的区间[L, R]使得sa中下标从L到R的所有后缀都以s[l, r]为前缀。 { int pos=rk[l],length=r-l+1; int L=0,R=pos,M; while(L<R){ M=(L+R)>>1; if(lcp(l,sa[M])>=length) R=M; else L=M+1; } int tmp=L; L=pos,R=n-1; while(L<R){ M=(L+R+1)>>1; if(lcp(l,sa[M])>=length) L=M; else R=M-1; } return make_pair(tmp,L); } }SA; int b[maxn],dis[maxn],n,ans[maxn]; char s[maxn]; int cmp(int i,int j) { if(dis[i]!=dis[j]) return dis[i]<dis[j]; if(i+dis[i]>=n&&j+dis[j]>=n) return i>j; if(i+dis[i]>=n) return 1; if(j+dis[j]>=n) return 0; int lcp=SA.lcp(i+dis[i],j+dis[j]); return b[i+dis[i]+lcp]<b[j+dis[j]+lcp]; } int main() { while(scanf("%d",&n)!=EOF){ scanf("%s",s); int pa=-1,pb=-1; b[n]=0; for(int i=0;i<n;i++){ if(s[i]=='a'){ if(~pa) b[i]=i-pa; else b[i]=0; pa=i; } else{ if(~pb) b[i]=i-pb; else b[i]=0; pb=i; } } SA.n=n; for(int i=0;i<n;i++) SA.s[i]=b[i]; SA.init(); SA.build_sa(n); SA.get_height(); SA.RMQ_init(); dis[n-1]=2; for(int i=n-2;i>=0;i--){ if(s[i]==s[i+1]) dis[i]=dis[i+1]+1; else dis[i]=2; } for(int i=0;i<n;i++) ans[i]=i; sort(ans,ans+n,cmp); for(int i=0;i<n;i++) cout<<ans[i]+1<<" "; cout<<endl; } return 0; }
F-Infinite String Comparision(思维)
思路:
将两个字符串的分别复制成长度为更长字符串两倍的之后再进行比较即可
题解中说到只要复制到长度为$(n+m)$即可
#include<iostream> #include<algorithm> using namespace std; int main() { string a,b; while(cin>>a>>b){ int l1=a.size(); int l2=b.size(); int len=max(l2,l1)*2; int num=0; string x=a,y=b; for(int i=l1;i<len;i++){ x+=a[num++]; if(num==l1) num=0; } num=0; for(int i=l2;i<len;i++){ y+=b[num++]; if(num==l2) num=0; } if(x<y) cout<<"<"<<endl; else if(x==y) cout<<"="<<endl; else cout<<">"<<endl; } return 0; }
F - Minimum-cost Flow(最小费用最大流)
思路:
我们首先建图然后跑一遍MincostMaxflow,求出每一条从$1$到$n$路径的费用,并用数组记录(根据题目已知没个路劲的流量都为$1$)
我们假设从$1$到$n$总的流量为$sum$,接下来我们考虑每个询问
如果$u*sum<v$的话,则输出$NaN$,因为我们有sum条路,如果每条都有流量也无法满足需求
否则的话,我们不断地加入每一条路径,并且累加费用,直到流量等于$v$时跳出循环,最后总的费用输出时分子分母同时除以分子分母的$GCD$即可
#include <bits/stdc++.h> #define inf 0x3f3f3f3f using namespace std; typedef long long ll; const int INF = 0x3f3f3f3f; const int maxn = 1000 + 10; struct edge { int u,v,c,f,cost; edge(int u,int v,int c,int f,int cost):u(u),v(v),c(c),f(f),cost(cost){} }; vector<edge>e; vector<int>G[maxn]; int a[maxn],p[maxn],d[maxn],inq[maxn]; int n,m,cur; ll cc[maxn],ff[maxn]; void init(int n) { cur=0; for(int i=0;i<=n;i++)G[i].clear(); e.clear(); } void addedge(int u,int v,int c,int cost) { e.push_back(edge(u, v, c, 0, cost)); e.push_back(edge(v, u, 0, 0, -cost)); int m=e.size(); G[u].push_back(m-2); G[v].push_back(m-1); } bool bellman(int s, int t, int& flow, long long & cost) { for(int i=0;i<=n+1;i++)d[i] = INF; memset(inq,0,sizeof(inq)); d[s]=0;inq[s]=1; p[s]=0;a[s]=INF; queue<int>q; q.push(s); while(!q.empty()){ int u=q.front(); q.pop(); inq[u]=0; for(int i=0;i<G[u].size();i++){ edge&now=e[G[u][i]]; int v=now.v; if(now.c>now.f&&d[v]>d[u]+now.cost){ d[v]=d[u]+now.cost; p[v]=G[u][i]; a[v]=min(a[u], now.c - now.f); if(!inq[v]){q.push(v);inq[v] = 1;} } } } if(d[t]==INF)return false; flow+=a[t]; cost+=(long long)d[t] * (long long)a[t]; cc[++cur]=(long long)d[t]*(long long)a[t]; ff[cur]=a[t]; for(int u=t;u!=s;u=e[p[u]].u){ e[p[u]].f += a[t]; e[p[u]^ 1].f-=a[t]; } return true; } int MincostMaxflow(int s, int t, long long & cost) { cost = 0; int flow = 0; while(bellman(s, t, flow, cost)); return flow; } int x,y,z,f,q; int main() { ll mxflow,mincost; while(~scanf("%d%d%",&n,&m)){ init(n); for(int i=1;i<=m;i++){ scanf("%d%d%d",&x,&y,&f); addedge(x,y,1,f); } ll mxflow=MincostMaxflow(1,n,mincost); scanf("%d",&q); while(q--){ ll u,v; scanf("%lld%lld",&u,&v); if(u*mxflow<v) cout<<"NaN"<<endl; else{ ll sum=0,cost=0; for(int i=1;i<=cur;i++){ if(u+sum<v){ sum+=ff[i]*u; cost+=cc[i]*u; } else{ cost+=cc[i]*(v-sum); break; } } ll k=__gcd(cost,v); printf("%lld/%lld\n",cost/k,v/k); } } } return 0; }
I - 1 or 2(一般图最大匹配)
思路:
关键点在于拆点建图上
对于每个点要求的度数$d[i]$,我们将点$i$拆成$d[i]$个
对于每条边$e(u,v)$,我们将$(u,v)$拆成,$x$,$y$,$x$与分别与$u$拆成的$d[x]$个点连边,$y$点同理,最后再将$(x,y)$连一条边(这样做是可以防止$u$或者$v$要求的度数为$0$的情况,此时直接在$x$与$y$之间连一条边就可以解决)
例如,题目中第三组样例
$3 2$
$1 1 2$
$1 3$
$2 3$
第$1$个点拆成$1$个点,编号为$1$。
第$2$个点拆成$1$个点,编号为$2$。
第$3$个点拆成$2$个点,编号分别为$3,4$
第$1$条边拆成两个点,编号为$5,6。(5,6)(5,1)(6,3)(6,4)$连边。
第$2$条边拆成两个点,编号为$7,8。(7,8)(7,2)(8,3)(8,4)$连边。
注意这里的边是无向边。
建图之后跑一般图匹配。如果是完全匹配输出$“Yes”$,否则输出$“No”$
#include <iostream> #include <cstdio> #include <algorithm> #include <cstring> #include <cstdlib> #include <queue> #include <stack> using namespace std; #define REP(i,n) for(int i=0;i<(n);++i) #define FOR(i,l,r) for(int i=(l);i<=(r);++i) #define DSC(i,r,l) for(int i=(r);i>=(l);--i) #define N 1010 #define SET(a,b) memset(a,b,sizeof(a)) deque<int> Q; //g[i][j]存放关系图:i,j是否有边,match[i]存放i所匹配的点 //建图开始初始化g //最终匹配方案为match //复杂度O(n^3) //点是从1到n的 bool g[N][N],inque[N],inblossom[N]; int match[N],pre[N],base[N]; //找公共祖先 int findancestor(int u,int v) { bool inpath[N]= {false}; while(1) { u=base[u]; inpath[u]=true; if(match[u]==-1)break; u=pre[match[u]]; } while(1) { v=base[v]; if(inpath[v])return v; v=pre[match[v]]; } } //压缩花 void reset(int u,int anc) { while(u!=anc) { int v=match[u]; inblossom[base[u]]=1; inblossom[base[v]]=1; v=pre[v]; if(base[v]!=anc)pre[v]=match[u]; u=v; } } void contract(int u,int v,int n) { int anc=findancestor(u,v); SET(inblossom,0); reset(u,anc); reset(v,anc); if(base[u]!=anc)pre[u]=v; if(base[v]!=anc)pre[v]=u; for(int i=1; i<=n; i++) if(inblossom[base[i]]) { base[i]=anc; if(!inque[i]) { Q.push_back(i); inque[i]=1; } } } bool dfs(int S,int n) { for(int i=0; i<=n; i++)pre[i]=-1,inque[i]=0,base[i]=i; Q.clear(); Q.push_back(S); inque[S]=1; while(!Q.empty()) { int u=Q.front(); Q.pop_front(); for(int v=1; v<=n; v++) { if(g[u][v]&&base[v]!=base[u]&&match[u]!=v) { if(v==S||(match[v]!=-1&&pre[match[v]]!=-1))contract(u,v,n); else if(pre[v]==-1) { pre[v]=u; if(match[v]!=-1)Q.push_back(match[v]),inque[match[v]]=1; else { u=v; while(u!=-1) { v=pre[u]; int w=match[v]; match[u]=v; match[v]=u; u=w; } return true; } } } } } return false; } int solve(int n) { SET(match,-1); int ans=0; for(int i=1; i<=n; i++) if(match[i]==-1&&dfs(i,n)) ans++; return ans; } //往上是带花树模板,不用看 int f[60]; int x[210],y[210]; int map[55][210]; bool build(int n,int m) { int num=0; FOR(i,1,n) FOR(j,1,f[i]) map[i][j]=++num;//给i点的第j个度一个标号,方便接下来的建图 REP(i,m){ FOR(j,1,f[x[i]]) g[map[x[i]][j]][num+1]=g[num+1][map[x[i]][j]]=1; FOR(j,1,f[y[i]]) g[map[y[i]][j]][num+2]=g[num+2][map[y[i]][j]]=1; g[num+1][num+2]=g[num+2][num+1]=1; num+=2; }//把边被拆成的两个点分别与两头的每个度相连 if(solve(num)*2==num) return 1; return 0; } int main() { int cas,cas1=1,n,m; while(cin>>n>>m){ memset(g,0,sizeof(g)); FOR(i,1,n) scanf("%d",&f[i]); REP(i,m) scanf("%d%d",&x[i],&y[i]); if(build(n,m)) puts("Yes"); else puts("No"); } return 0; }
J - Easy Intergration (数学)
思路:
求得积分为$\frac{(n+1)!}{(2n+1)!}$,因此我们只要预处理出$[1,2*10^{6}+2]$的阶乘,$O(1)$查询即可
#include"bits/stdc++.h" #define ll long long using namespace std; ll mod=998244353; ll pows(ll a,ll b) { ll ans=1; for(;b;b>>=1,a=a*a%mod) { if(b%2)ans=ans*a%mod; } return ans%mod; } ll dev(ll n) { return pows(n,mod-2); } ll jie[2000005]; //ll ans[1000005]; int main() { jie[1]=1; for(int i=2;i<2000005;i++) { jie[i]=jie[i-1]*i%mod; }/* ans[1]=166374059; for(int i=2;i<1000005;i++) { #define n i ans[i]=ans[i-1]*n%mod*n%mod*dev(2*n)%mod*dev(2*n+1)%mod; #undef n }//cout<<"ok"; for(int i=0;i<1000005;i++)printf("%lld,",ans[i]);*/ while(1) { ll n; if(scanf("%lld",&n)==EOF)return 0; printf("%lld\n",jie[n]*jie[n]%mod*dev(jie[2*n+1])%mod); //cout<<<<endl; } return 0; }