2020牛客暑期多校第一场赛后感
结果挺惨的。
《关于我和队友啥都不会就来比赛结果被AK大佬虐得体无完肤这档事》
来来来,写题解了。
A. B-Suffix Array
题意:对于一个只有'a'和'b'的字符串t1t2...tk,定义函数B(t1t2...tk)=b1b2...bk,满足:如果存在下标j<i使得tj=ti,那么bi=min1≤j<i,tj=ti{i-j},否则bi=0。现在给一个字符串s1s2...sn,对它的n个后缀按B()函数的字典序递增排序,并输出顺序。1≤n≤105
如果知道后缀数组的话,会发现这题很像。后缀数组nlog2n或nlogn,复杂度是可行的。然而我们队一个人都不知道。
但是不同后缀的B()的前缀是不同的。一个很自然的思路是另设一个函数C(),使它与B()等效,且能用后缀数组解。
于是大佬们如此定义C():如果存在下标j>i使得tj=ti,那么bi=min1≤i<j,tj=ti{j-i},否则bi=*(关于*是什么,一会儿解释)。然后将各C()递减排序。
为什么C()与B()等效?现在开始证明:
我们的证明方法是这个形式:对于两个字符串s和t,如果B(s)>B(t),那么一定C(s)<C(t)。
位置 |
s,t(省略的字符相同) |
B() |
C() |
备注 |
开头 |
aaaab... aaab... |
01110...大 0110... |
111xy... 11xy...大 |
只要让x>1即可 |
中部 |
...aaab... ...aab... |
...x11y... ...x1z...大 |
...11mn...大 ...1mn... |
若前面没有b,则只要让z,m>1即可 若前面有b,则一定符合 |
尾部 |
...aaab ...aab |
...x11y ...x1z |
...11mn ...1mn |
若前面没有b,也就是y=z=0,则只要让m>1即可 若前面有b,也就是y>z>0,则一定符合 |
特殊 |
a ba |
0 00大 |
m大 nm |
只要让m>n即可。 |
为了让a和ba的C()最大,这里*我设置的是:如果a后面没有a和b,那么为n+n;如果a后面有b无a,那么为n+n-1。也就是C(a)=2n,C(ba)=2n-1 2n。
代码奉上:
#include<cstdio> #include<cstring> #include<iostream> #include<algorithm> using namespace std; const int N=100000+10; int n, k; char s[N]; int c[N], d[N], y[N]; bool cmp(int a, int b){ if(c[a]!=c[b]) return c[a]<c[b]; int a1, b1; if(a+k>n) a1=-1; else a1=c[a+k]; if(b+k>n) b1=-1; else b1=c[b+k]; return a1<b1; } void suffix(int c[], int n){ for(k=1; k<=n; k<<=1){ sort(d+1,d+n+1,cmp); y[d[1]]=1; for(int i=2; i<=n; ++i) y[d[i]]=y[d[i-1]]+cmp(d[i-1],d[i]); for(int i=1; i<=n; ++i) c[i]=y[i]; } } int main(){ while(~scanf("%d", &n)){ scanf("%s", s+1); int last[2]={0,0}; for(int i=n; i; --i){ s[i]-='a'; if(last[s[i]]==0){ if(last[1-s[i]]==0) c[i]=n+n; else c[i]=n+n-1; }else c[i]=last[s[i]]-i; last[s[i]]=i; } for(int i=1; i<=n; ++i) d[i]=i; suffix(c, n); for(int i=n; i>=1; --i) printf("%d%c", d[i], " \n"[i==1]); } return 0; }
话说这个标题适合作为第二题。
B. Infinite Tree
题意:有一棵无限大的树,对于所有的u>1,u与u/mindiv(u)有边相连。mindiv(u)意思是u除了1之外的最小的因数。现在给出 m 和$w_1w_2...w_m$,求$\min_u \sum_{i=1}^{m}w_i\delta(u,i!)$,其中$\delta(u,v)$表示两个点u和v之间的边的条数。多组数据,$1\le m \le 10^5 , 0 \le w_i \le 10^4, \sum_m\le 10^6 $
这道题m!实在大得吓人,想必因数很多,因此这是一颗巨大的树。我们假设已经有了这样一课树,现在怎么求解呢?
以1为根节点,设w[u]表示在u这个子树内的w之和(不能用阶乘表示的数的w设为0),那么w[1]是所有的w之和。如果选u为答案所在节点,此时答案记为f(u),那么可以由u推导f(v),v是u的一个孩子。容易得出f(v)=f(u)+w[1]-2*w[v]。
如果w[1]-2*w[v]<0,那么v比u更优。又容易想到如果u不是能用阶乘表示的数,那么w[u]=0。这时如果u不是两个阶乘数的lca的话,它与父亲或孩子的w是一样的,因此u必然可以不被选为答案节点。
这样的点可以忽略掉,那么我们可以建立一棵虚树,这棵树最多2m个结点。
其实看到这么大的树应该自然就想到建一棵虚树,然后树上dp一下。
可能这有打马后炮的意味。当时见到这道题的时候根本不知道该用什么算法,尤其是我根本不知道虚树。
虚树的自我修养:依据dfs序加点,能求lca,知道新边的长度。
根据定义,n!自然包括(n-1)!的所有因数,因此,从1到m遍历就已经是符合dfs序了。
根节点1到其他点u的路径是由u的质因数从大到小排序得到的。lca(n!,(n+1)!)的深度等于n!中大于等于mindiv(n+1)的因数个数(同一个因数可以出现多次)。而这里知道lca的深度即可。
如果我们也统计了每个u!的深度,那么虚树中边的长度直接是两个点的深度相减,也容易求出。
总结一下,先以1为根节点建立一棵虚树,树上的点只有1到m的阶乘和它们的lca这最多2m个点,边长就是两个点之间实际的边数。然后以1为u求下结果,再dfs一遍求出每个点的结果。虚树建立及dp的复杂度只有O(m),但建立虚树所作的准备我是$O(mlog^2m)$。
大体思路就这样,细节很多,代码见下:
// code by cyh #include<cstdio> #include<cstring> #include<iostream> #include<algorithm> #include<vector> #include<stack> using namespace std; const int M=100000+10; typedef long long LL; struct Edge{ int to, next, w; Edge(int v=0, int n=0, int w=0):to(v),next(n),w(w){} }; struct Graph{ int h[M<<1], hn; Edge e[M<<2]; void init(int n){ for(int i=0; i<=n; ++i) h[i]=h[M+i]=0; hn=0; } void add(int u, int v, int w){ e[++hn]=Edge(v,h[u],w); h[u]=hn; e[++hn]=Edge(u,h[v],w); h[v]=hn; } }; int m; int d[M<<1], size[M<<1]; int b[M]; LL w[M<<1]; LL ans; vector<int> V[M]; stack<int> S; Graph g; int prime[M], pn; bool not_prime[M]; void getprime(){ not_prime[1]=true; for(int i=2; i<M; ++i){ if(!not_prime[i]) prime[pn++]=i; for(int j=0; j<pn&&i*1ll*prime[j]<M; ++j){ not_prime[i*prime[j]]=true; if(i%prime[j]==0) break; } } } int lowbit(int x){ return x&(-x); } void change(int w, int x){ for(; w<M; w+=lowbit(w)) b[w]+=x; } int query(int w){ int res=0; for(; w; w-=lowbit(w)) res+=b[w]; return res; } void init(){ //prime getprime(); //the prime factors of every number for(int i=0; i<pn; ++i) for(int j=prime[i]; j<M; j+=prime[i]) V[j].push_back(prime[i]); //the number of every number's prime factors //cal d[] d[1]=0; for(int i=2, t, sz, cnt; i<M; ++i){ sz=V[i].size(); t=i; d[i]=d[i-1]; d[M+i]=query(M-V[i][sz-1]); for(int j=0, p; j<sz; ++j){ p=V[i][j]; cnt=0; while(t%p==0){ t/=p; cnt++; } d[i]+=cnt; change(M-V[i][j],cnt); } } } void build(int m){ S.push(1); for(int i=2, x, y, l; i<=m; ++i){ l=M+i; while(1){ x=S.top(); if(d[l]==d[x]){ S.push(i); break; } S.pop(); y=S.top(); if(d[y]>d[l]){ g.add(y,x,d[x]-d[y]); }else if(d[y]==d[l]){ g.add(y,x,d[x]-d[y]); S.push(i); break; }else{ g.add(l,x,d[x]-d[l]); S.push(l); S.push(i); break; } } } int x=S.top(), y; S.pop(); while(!S.empty()){ y=S.top(); S.pop(); g.add(y,x,d[x]-d[y]); x=y; } } LL initTree(int u, int fa){ LL res=0; size[u]=1; for(int p=g.h[u]; p; p=g.e[p].next){ int v=g.e[p].to; if(v==fa) continue; res+=initTree(v,u); res+=w[v]*1ll*g.e[p].w; w[u]+=w[v]; size[u]+=size[v]; } return res; } void calAns(int u, int fa, LL res){ ans=min(ans,res); for(int p=g.h[u]; p; p=g.e[p].next){ int v=g.e[p].to; if(v==fa) continue; calAns(v,u,res+(w[1]-2ll*w[v])*g.e[p].w); } } int main(){ // freopen("data.txt","r",stdin); // freopen("my.txt","w",stdout); init(); while(~scanf("%d", &m)){ for(int i=1; i<=m; ++i) scanf("%lld", w+i); build(m); ans=initTree(1,0); calAns(1,0,ans); printf("%lld\n", ans); g.init(m<<1); for(int i=0; i<=m; ++i) w[M+i]=0; } return 0; }
F. Infinite String Comparision
题意:给两个字符串s和t,比较s+s+...和t+t+...的大小,分别对应输出">" "=" "<"。|s|,|t|≤106
据说比较s+t和t+s就可以了。
更普通的做法是先判断gcd(len(s),len(t)),比较是否相等,然后普普通通地一个一个比较是">"还是"<",应该最多比较两遍就行了。
代码就不写了。
H. Minimum-cost Flow
题意:给一张n个点,m条边有边权的有向图,有q个询问,每个询问包含两个整数u,v,意为这个网络流每条边容量都为u/v,对每个询问输出从点1到点n运送1单位需要的最小费用,并用 "a/b" 形式表示,a,b为整数且互质。$2\le n \le 50, 1 \le m \le 100, q \le 10^5, 0 \le u_i \le v_i \le 10^9, v_i > 0, 1 \le 边权 \le 10^5$
几乎就是最小费用最大流了呵,只不过有q个询问,使得每次询问有了不同的容量。一个很重要的点是所有边容量相同。
一个最简单的最小费用最大流算法是这么做的:每次用bellman-ford找一条从源点到终点的费用最小的路,直到找不到,算法结束。
这道题也可以这样。先用这个算法求出最大流,并再这过程中每次找到一条路后记录下这一路的边权。也就是依次记录下了所有的路。
由于每次都是运输u/v,那么需要运输v/u(整数)次u/v,或v/u(下取整)次u/v+1次(v%u)/v。只需要上述算法有没有v/u(上取整)个路径就可以了。
复杂度$O(n^2m+qm)$
代码见下:
#include<cstdio> #include<cstring> #include<iostream> #include<algorithm> #include<vector> #include<queue> using namespace std; const int N=50+5, M=100+5, INF=1000000000; typedef long long LL; struct Edge{ int from, to, cap, flow, cost; Edge(int u, int v, int c, int f, int w):from(u), to(v), cap(c), flow(f), cost(w){} }; vector<int> res; struct MCMF{ int n, m; vector<Edge> edges; vector<int> G[N]; int inq[N]; int d[N]; //BellmanFord int p[N]; //上一条弧 int a[N]; //可改进量 void init(int n){ this->n=n; for(int i=1; i<=n; ++i) G[i].clear(); edges.clear(); } void AddEdge(int from, int to, int cap, int cost){ edges.push_back(Edge(from,to,cap,0,cost)); edges.push_back(Edge(to,from,0,0,-cost)); m=edges.size(); G[from].push_back(m-2); G[to].push_back(m-1); // printf("%d->%d cap=%d cost=%d\n", from, to, cap, cost); } bool BellmanFord(int s, int t, int& flow, LL& cost){ for(int i=1; i<=n; ++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& e=edges[G[u][i]]; if(e.cap>e.flow && d[e.to]>d[u]+e.cost){ d[e.to]=d[u]+e.cost; p[e.to]=G[u][i]; a[e.to]=min(a[u],e.cap-e.flow); if(!inq[e.to]) { Q.push(e.to); inq[e.to]=1; } } } } if(d[t]==INF) return false; flow+=a[t]; res.push_back(d[t]); cost+=(LL)d[t]*(LL)a[t]; for(int u=t; u!=s; u=edges[p[u]].from){ edges[p[u]].flow+=a[t]; edges[p[u]^1].flow-=a[t]; } return true; } int MincostMaxflow(int s, int t, LL& cost){ int flow=0; cost=0; while(BellmanFord(s,t,flow,cost));// printf("flow=%d cost=%d\n", flow, cost); return flow; } }; int n, m, q; MCMF mcmf; LL gcd(LL a, LL b){ if(b==0) return a; return gcd(b,a%b); } int main(){ while(~scanf("%d%d", &n, &m)){ mcmf.init(n); res.clear(); for(int i=1, u, v, w; i<=m; ++i){ scanf("%d%d%d", &u, &v, &w); mcmf.AddEdge(u,v,1,w); } LL cost; LL f=mcmf.MincostMaxflow(1,n,cost); scanf("%d", &q); while(q--){ LL u, v; scanf("%lld%lld", &u, &v); if(f*u<v){ printf("NaN\n"); continue; } LL cur=0, ans1, ans2, g; double ans=0; int i=0; while(1){ if(cur+u<v){ ans+=res[i++]; cur+=u; }else{ ans1=u*ans+(v-cur)*res[i]; ans2=v; g=gcd(ans1,ans2); printf("%lld/%lld\n", ans1/g, ans2/g); break; } } } } }
I.1 or 2
题意:给一张n个点,m条边的图,判断能否删去一些边使得每个点$i$的度为$d_i$。$1 \le d_i \le 2, 1 \le n \le 50, 1 \le m \le 100, 无自环,无重边$
据说有类似的题。
看这道题的数据范围就知道一定是某个复杂度顶大的算法。这个算法就是一般图最大匹配,算法复杂度$O(n(nlogn+m))$。也不是很大哈
首先拆点:如果一个点u的度数为2,那么就把它拆成两个点,多的那个点我们记作u2。
然后拆边:如果有一条连接u,v的边,就把这条边拆成2个点 u', v',然后连接 u-u', v-v',如果度数为2那就把u2和v2也对应连上;再连上u'-v'。
最后一般图最大匹配,如果是完美匹配就是"yes",否则就是"no"。复杂度$O((n+m)((n+m)log(n+m)+m))$,不到$10^6$吧。
奉上代码:
#include<cstdio> #include<cstring> #include<iostream> #include<algorithm> #include<vector> #include<queue> using namespace std; const int PN=50+5, PM=100+5; const int N=PN+PM<<1; struct CGM{ vector<int> G[N]; queue<int> q; int p[N], dfn[N], pre[N], match[N], vst[N]; static int cnt, ans; void init(int n){ for(int i=0; i<=n; ++i) G[i].clear(); while(!q.empty()) q.pop(); memset(p,0,sizeof(p)); memset(dfn,0,sizeof(dfn)); memset(pre,0,sizeof(pre)); memset(match,0,sizeof(match)); memset(vst,0,sizeof(vst)); cnt=0; ans=0; } void add(int u, int v) { G[u].push_back(v); G[v].push_back(u); } int find(int x) { return x==p[x]?x:p[x]=find(p[x]); } int lca(int u, int v){ for(++cnt,u=find(u),v=find(v); dfn[u]!=cnt; ){ dfn[u]=cnt; u=find(pre[match[u]]); if(v)swap(u,v); } return u; } void blossom(int x, int y, int w){ while(find(x)!=w){ pre[x]=y,y=match[x]; if(vst[y]==2)vst[y]=1,q.push(y); if(find(x)==x)p[x]=w; if(find(y)==y)p[y]=w; x=pre[y]; } } int aug(int s, int n){ if((ans+1)*2>n) return 0; for(int i=1; i<=n; ++i) { p[i]=i; vst[i]=pre[i]=0; } while(!q.empty()) q.pop(); q.push(s); vst[s]=1; while(!q.empty()){ int u=q.front(); q.pop(); for(int i=0, v, w; i<G[u].size(); ++i){ if(find(u)==find(v=G[u][i])||vst[v]==2)continue; if(!vst[v]){ vst[v]=2; pre[v]=u; if(!match[v]){ for(int x=v, lst; x; x=lst) { lst=match[pre[x]]; match[x]=pre[x]; match[pre[x]]=x; } return 1; } vst[match[v]]=1; q.push(match[v]); }else{ w=lca(u,v); blossom(u,v,w); blossom(v,u,w); } } } return 0; } int CommonGraphMatch(int n){ for(int i=n; i; --i) if(!match[i]) ans+=aug(i,n); return ans; } }; int CGM::cnt=0, CGM::ans=0; int n, m; int o[PN]; CGM cgm; int main(){ while(~scanf("%d%d", &n, &m)){ int tot=n; cgm.init(n+m<<1); for(int i=1, x; i<=n; ++i){ scanf("%d", &x); o[i]=x==2?++tot:0; } for(int i=0, u, v, t; i<m; ++i){ scanf("%d%d", &u, &v); cgm.add(u,tot+1); cgm.add(v,tot+2); cgm.add(tot+1,tot+2); if(o[u]) cgm.add(o[u],tot+1); if(o[v]) cgm.add(o[v],tot+2); tot+=2; } // int ans=cgm.CommonGraphMatch(tot); // for(int i=1; i<=tot; ++i) printf("%d%c", cgm.match[i], " \n"[i==tot]); // printf("tot=%d ans=%d\n", tot, ans); // puts((tot%2==0 && ans==tot/2)?"Yes":"No"); puts((tot%2==0 && cgm.CommonGraphMatch(tot)==tot/2)?"Yes":"No"); } return 0; }
结语
这就是目前我搞懂的所有题目,其他的由于智商和时间问题仍不会做,扔给队友吧。
这场比赛,我们队就做出了2道,真是辣鸡得不行~
说到做不出来的原因,A题最主要是不知道后缀数组这个东西,其次是不知道那个结论,或是没有得出结论的那种思维;B题最主要是不知道虚树,其次是没有分析出答案的计算方法;H题主要是不会网络流;I题主要是没有把问题转换成二分图的能力,其次是不知道一般图匹配;J题主要是数学不好。
总结就是:1.没有分析问题的思维;2.不会相应模板;3.数学不好。
再总结:菜鸡一个
身为菜鸡,我很抱歉 ╮(╯▽╰)╭
2020.7.31