2021牛客暑期多校训练营10
比赛链接:https://ac.nowcoder.com/acm/contest/11261
F,H,12。
A
题意:
给一系列字符串,对每个字符串,要找到一些前缀,使得当前字符串以及它之前的所有字符串都至少有一个前缀在这些前缀中,而它之后的所有字符串都没有前缀在这些前缀中。对每个字符串,输出找到的前缀的最小个数。空间限制\(32768KB\),\(n \leq 10^5\),每个字符串长度不超过\(100\)。
分析:
首先不考虑空间限制,看这个题怎么做。
涉及到前缀,考虑一下字典树。由于题目说没有字符串是其他字符串的前缀,也没有空字符串,所以每个字符串都终止在字典树的一个叶子上。
从\(1\)到\(n\)枚举当前字符串\(i\),那么所有字符串被分成了两类,第一类是需要有前缀“占领”,第二类是不能被前缀“占领”的。
取一个前缀,就是在字典树上找一个节点。题目的要求实际上就是找的这些节点的子树包含的字符串只能是第一类,不能有第二类,求这些节点可能的最小数目。
为了直观,我们把当前的第一类字符串所对应的叶子成为“白点”,第二类字符串所对应的叶子称为“黑点”,找的前缀对应的点成为“红点”。一开始所有叶子都是黑点。红点的子树里不能有黑点。
我们可以对字典树上每个点记录一个值\(sizb\),表示这个点的子树中有多少黑点;再记录一个值\(sizr\),表示子树中有多少红点。
每次枚举到下一个字符串,有一个黑点变成了白点。相应地,它和它祖先的\(sizb\)都会减少\(1\)。这下会出现一些新的点\(sizb=0\),也就是出现一些新的点可以变成红点。
为了让红点个数最少,我们要让每个红点覆盖到尽量多的白点,也就是让它的深度尽量浅。而一个红点的子树内不再需要别的红点了。
所以我们在被更新的那条祖先链上找深度最浅的一个点,让它变成红点,以前它子树中所有的红点都不要了。这里需要用到\(sizr\),也要再更新一番\(sizr\)。值得注意的是,这个红点的子树内没有黑点了,也就是我们以后再也不会用到这个子树。所以不用再费力修改子树内点的\(sizr\)了。
这样做的时间复杂度是\(O(总字符串长度)\)的,可以。
但是这样我们需要建一个总字符串长度规模的字典树,还要记录这个规模的\(sizb,sizr,fa\),空间太大了。
实际上,字典树还可以压缩。字典树上那些没有旁支的链都可以省略。所以我们递归建树,当一个点需要分叉时再建新点。为了快速判断是否分叉,我们可以把字符串排序以后再建树,这样分到同一个叉里的字符串是一个连续的区间。当只有一个字符串时,结束递归。
这里要注意多个字符串共有的部分也需要新建一个点,因为前缀可以在这里取到!
这样字典树的规模就是\(2*n\)的,因为每增加一个字符串,最多增加两个点(与别人共有的点和自己的叶子点)。空间复杂度也没问题了。
代码如下:
#include<iostream> #include<cstring> #include<algorithm> #include<vector> #define pb push_back using namespace std; int const N=1e5+5,M=(N<<1); int n,cnt,buk[70],sizb[M],sizr[M],pos[N],fa[M]; struct Nd{ string s; int id; }a[N]; vector<int>son[M]; bool cmp(Nd x,Nd y){return x.s<y.s;} int chg(char c) { if(c>='a'&&c<='z')return c-'a'+1; if(c>='A'&&c<='Z')return c-'A'+27; if(c=='.')return 53; if(c=='/')return 54; else return c-'0'+55; } void build(int u,int l,int r,int dep) { //printf("u=%d l=%d r=%d dep=%d fa[u]=%d\n",u,l,r,dep,fa[u]); if(l==r){pos[a[l].id]=u; sizb[u]=1; return;} int pdep=dep; while(1) { /* for(int i=1;i<=65;i++)buk[i]=0; int num=0; for(int i=l;i<=r;i++) { int x=chg(a[i].s[dep]); if(!buk[x])num++; buk[x]++; } if(num==1)dep++; else break; */ if(a[l].s[dep]==a[r].s[dep])dep++; else break; } if(dep>pdep)son[u].pb(++cnt),fa[cnt]=u,u=cnt;//相同部分新建一个点! int L=l,R; while(L<=r) { R=L+1; while(R<=r&&a[R].s[dep]==a[L].s[dep])R++; son[u].pb(++cnt); fa[cnt]=u; build(cnt,L,R-1,dep+1); L=R; } } void dfs(int u) { //if(!son[u].size()){sizb[u]=1; return;} for(int v:son[u]) dfs(v),sizb[u]+=sizb[v]; //printf("sizb[%d]=%d\n",u,sizb[u]); } bool cmp2(Nd x,Nd y){return x.id<y.id;} void work(int u) { int p=u; while(p!=-1)sizb[p]--,p=fa[p]; while(1) { if(fa[u]==0||sizb[fa[u]])break;//红点不能是根节点 u=fa[u]; } int pre=sizr[u]; sizr[u]=1; u=fa[u];//此点以下不再用到了 while(u!=-1)//更新祖先sizr { sizr[u]-=pre; sizr[u]++; u=fa[u]; } } int main() { scanf("%d",&n); for(int i=1;i<=n;i++) cin>>a[i].s,a[i].id=i; sort(a+1,a+n+1,cmp); fa[0]=-1; build(0,1,n,0); dfs(0); sort(a+1,a+n+1,cmp2); for(int i=1;i<=n;i++) { work(pos[i]); printf("%d\n",sizr[0]); } return 0; }
D
题意:
求\(n\)个点的无根有标号树的各种形态的直径和。
分析:
巧妙的DP。
首先,我们要给出一种求树的直径的方法。对一棵树,可以一轮一轮不断删掉它所有叶子节点;删除\(x\)轮后会剩下一个或两个点。如果剩下一个点,那么直径是\(2x\);如果剩下两个点,那么直径是\(2x+1\)。因为我们没删除一轮,直径会减少\(2\)。
这启发了我们如何在构建一棵树的时候维护它的直径。我们采用一轮一轮添加叶子的方法建树,每次原来的叶子至少要连接一个新叶子。那么新树的直径\(=\)旧树的直径\(+2\)。
设\(g[i][j][k]\)表示给一个原来有\(i\)个点、\(j\)个叶子的树添加叶子,使其变成\(i+k\)个点、\(k\)个叶子的树的方案数。
\(g[i][j][k]\)可以等价于一个长度为\(k\)的序列,每个位置有\(i\)种选择可以填,在总序列中有\(j\)个值必须出现,的方案数。这个可以用DP转移,分类讨论第\(k\)个位置填的是不是一个全新的\(j\)中的值:
\(g[i][j][k] = g[i][j][k-1]*i + g[i-1][j-1][k-1]*j \)
得到\(g\)以后,我们设\(f[i][j]\)表示构建\(i\)个点、有\(j\)个叶子的树的方案数。那么
\(f[i+k][k] += f[i][j] * g[i][j][k] * C^{i}_{i+k} \)
这里出现组合数是因为题目要求的树是有标号的,所以每个点彼此不同,我们要分配一下\(i+k\)个点中哪些是原来树的,那些是新叶子的。
现在我们考虑如何算直径和。设\(h[i][j]\)表示\(i\)个点、\(j\)个叶子的所有树的直径和。和上面计算\(f\)的过程一样,我们考虑\(h[i][j]\)对\(h[i+k][k]\)的贡献:
旧树是\(i\)点\(j\)叶,新树是\(i+k\)点\(k\)叶。新树的直径\(=\)旧树的直径\(+2\)。
所以新树的直径分成了两部分:旧树的直径\(*\)旧树变新树的方案数 \(+\) \(2*\)新树的方案数,即
\(h[i+k][k] += h[i][j] * g[i][j][k] * C_{i+k}^{i}\)
\(h[i+k][k] += 2*f[i+k][k]\)
注意\(f[i+k][k]\)是从很多个\(f[i][j]\)更新而来的,所以上面两部分要分开加。
然后就是各种边界、初始化……颇麻烦。
代码如下:
#include<iostream> #define ll long long using namespace std; int const N=505; int n,p; ll g[N][N][N],f[N][N],h[N][N],jc[N],jcn[N]; ll pw(ll x,int y) { ll ret=1,nx=x; while(y) { if(y&1)ret=ret*nx%p; nx=nx*nx%p; y>>=1; } return ret; } void init() { jc[0]=1; for(int i=1;i<=n;i++)jc[i]=jc[i-1]*i%p; jcn[n]=pw(jc[n],p-2); for(int i=n-1;i>=0;i--)jcn[i]=jcn[i+1]*(i+1)%p; } ll C(int x,int y){return jc[x]*jcn[y]%p*jcn[x-y]%p;} int main() { scanf("%d%d",&n,&p); init(); // for(int i=1;i<=n;i++)g[i][0][1]=i,g[i][1][1]=1; g[0][0][0]=1; for(int i=1;i<=n;i++) { g[i][0][0]=1; for(int k=1;k<=n;k++) g[i][0][k]=(g[i][0][k-1]*i)%p; } for(int k=1;k<=n;k++) for(int i=1;i<=n;i++) for(int j=1;j<=min(i,k);j++) g[i][j][k]=(g[i][j][k-1]*i%p+g[i-1][j-1][k-1]*j%p)%p;//,printf("g[%d][%d][%d]=%lld\n",i,j,k,g[i][j][k]); // for(int k=1;k<=n;k++) // printf("g[%d][%d][%d]=%d\n",n,5,k,g[n][5][k]); f[1][1]=1; f[2][2]=1; for(int i=1;i<=n;i++) for(int j=min(2,i);j<=i;j++) for(int k=max(2,j);i+k<=n;k++) f[i+k][k]=(f[i+k][k]+f[i][j]*g[i][j][k]%p*C(i+k,i)%p)%p; // for(int i=1;i<=n;i++) // for(int j=1;j<=i;j++)printf("f[%d][%d]=%lld\n",i,j,f[i][j]); h[1][1]=0; h[2][2]=1; for(int i=1;i<=n;i++) for(int k=2;i+k<=n;k++) { for(int j=min(2,i);j<=min(i,k);j++) { h[i+k][k]=(h[i+k][k]+h[i][j]*g[i][j][k]%p*C(i+k,i)%p)%p;//+2*f[i+k][k]%p)%p; /*if(i+k==4&&k==2)printf("h[4][2]=%d h[%d][%d]=%d g[%d][%d][%d]=%d f[%d][%d]=%d\n", h[4][2],i,j,h[i][j],i,j,k,g[i][j][k],i+k,k,f[i+k][k]);*/ } h[i+k][k]=(h[i+k][k]+2*f[i+k][k]%p)%p; } // for(int i=1;i<=n;i++) // for(int j=1;j<=i;j++)printf("h[%d][%d]=%lld\n",i,j,h[i][j]); ll ans=0; for(int j=min(n,2);j<=n;j++)ans=(ans+h[n][j])%p; printf("%lld\n",ans); return 0; }
F
分析:
可以把火车序列当作一棵树来看,括号序列就是树的dfs序。所以这个问题实际上是要使得树上每个点的直接孩子颜色彼此不同,过程中贪心地取个数最多的几种颜色即可。
(之所以把此题摆在这,是因为比赛时我做出了此题!值得纪念。)
G
分析:
设\(f(S)\)表示集合\(S\)中的人恰好被击中的概率。由于相同人数的集合都是等价的,故我们直接用人数来代替集合,只需再乘一个组合数。
最终有\(k\)人被击中的概率就是\(F(k) = C_{n}^{k} * f(k) \),这里\(f(k)\)表示一个大小为\(k\)的集合恰好被击中的概率。
要求“恰好”的问题,我们通常想到用“至少/多”来容斥。这里设\(g(k)\)表示一个大小为\(k\)的集合被击中的概率,恰好被击中的是它的子集。
那么\(g(k) = (1-p+p*\frac{k-1}{n-1})^k * (1-p+p*\frac{k}{n-1})^{n-k} \)。这是从开枪者的角度来考虑的:集合内的\(k\)个人要么没打中,要么打中了集合内的其他\(k-1\)个人;集合外的\(n-k\)个人要么没打中,要么打中了集合内的\(k\)个人。
然后根据容斥:\(f(k) = \sum_{i=0}^{k}(-1)^{k-i}*g(i)*C_k^i \)
综上:
\(F(k) = C_{n}^{k} * ( \sum_{i=0}^{k}(-1)^{k-i}*g(i)*C_k^i ) \)
\( = \frac{n!}{(n-k)!} * \sum_{i=0}^{k} \frac{g(i)}{i!} * \frac{(-1)^{k-i}}{(k-i)!} \)
于是可以通过NTT求出。
花了一点时间重拾了一下FFT和NTT。看了看以前的博客。
注意数组大小,是\(2n\)往上的\(2\)的幂次,所以开四倍。
代码如下:
#include<iostream> #define ll long long using namespace std; int const N=(1<<20)+5,md=998244353; int n,lim,rev[N]; ll p,a[N],b[N],jc[N],jcn[N]; ll pw(ll x,int y) { ll ret=1,nx=x; while(y) { if(y&1)ret=ret*nx%md; nx=nx*nx%md; y>>=1; } return ret; } void init() { jc[0]=1; for(int i=1;i<=n;i++)jc[i]=jc[i-1]*i%md; jcn[n]=pw(jc[n],md-2); for(int i=n-1;i>=0;i--)jcn[i]=jcn[i+1]*(i+1)%md; } ll mo(ll x){x%=md; if(x<0)x+=md; return x;} ll calg(int i){return pw(mo(1-p+p*(i-1)%md*pw(n-1,md-2)%md),i)*pw(mo(1-p+p*i%md*pw(n-1,md-2)%md),n-i)%md;} void ntt(ll *a,int tp) { for(int i=0;i<lim;i++) if(i<rev[i])swap(a[i],a[rev[i]]); for(int mid=1;mid<lim;mid<<=1) { int len=(mid<<1),wn=pw(3,tp==1?(md-1)/len:(md-1)-(md-1)/len); for(int j=0;j<lim;j+=len) for(int k=0,w=1;k<mid;k++,w=(ll)w*wn%md) { int x=a[j+k],y=(ll)w*a[j+mid+k]%md; a[j+k]=mo(x+y); a[j+mid+k]=mo(x-y); } } if(tp==1)return; int inv=pw(lim,md-2); for(int i=0;i<lim;i++)a[i]=a[i]*inv%md; } int main() { int aa,bb; scanf("%d%d%d",&n,&aa,&bb); p=(ll)aa*pw(bb,md-2)%md; init(); lim=1; int l=0; while(lim<=n+n)lim<<=1,l++; for(int i=0;i<lim;i++)rev[i]=((rev[i>>1]>>1)|((i&1)<<(l-1))); for(int i=0,f=1;i<=n;i++,f=-f) a[i]=calg(i)*jcn[i]%md,b[i]=mo(f*jcn[i]); ntt(a,1); ntt(b,1); for(int i=0;i<lim;i++)a[i]=a[i]*b[i]%md; ntt(a,-1); for(int i=n;i>=0;i--) printf("%lld\n",a[i]*jc[n]%md*jcn[n-i]%md); return 0; }
K
分析:
首先,两个维度彼此互不影响。所以可以分开算,最后组合在一起是\(t\)步就行了。
对于一个维度,我们要求的是走\(i\)步始终合法的方案数,记作\(K[i]\)。
有个很显然的DP做法:设\(f[i][j]\)表示始终合法地走了\(i\)步、最后在\(j\)位置的方案数,转移显然。复杂度是\(O(tn)\)的,当二者比较小的时候可做。
直接考虑\(K[i]\),发现\(K[i] = 2*K[i-1] - f[i-1][1] - f[i-1][ed] \)(这里\(ed\)是\(n\)或\(m\)),所以我们单独求\(f[i-1][1]\)和\(f[i-1][ed]\)就好了;
设\(g[i][j]\)表示不管合法与否地走了\(i\)步、最后在\(j\)位置的方案数,那么\(g[i][j]\)直接组合可以算出来。
从任意方案求合法方案,我们考虑容斥。
对于起点是数轴上的\(x\),任意走\(i\)步到达\(1\)的方案数\(g[x][i][1]\),有些路线是合法的,有些路线不合法。不合法的路线中,有的走到了\(1\)的左边,有的走到了\(ed\)的右边;当然也有两边都走了的,这也是容斥所在。
考虑走到\(1\)左边的那些不合法路线:它们一定经过了点\(0\),然后从\(0\)又走来走去,最后走到了\(1\)。一个经典的想法是,把\(x\)到\(0\)的这段路线对称到\(0\)的左边,也就是变成了从\(-x\)到\(0\)的路线(这样做是因为因为从\(-x\)走到\(1\)一定要穿越点\(0\);可能穿过很多次,我们取最后那次)。之后的路线不对称,还是原样。据此,我们需要减去的是\(g[-x][i][1]\)。右边界同理,我们要减去的是\(g[2*ed-x][i][1]\)。
但是显然会重复减掉一些路线,比如那些既超过了左边界又超过了右边界的路线。所以我们要容斥,再加回来;加的路线里又有重复,需要再减去……
换句话说,令\(L(x)=-x\)表示左边界对称点,\(R(x)=2*ed-x\)表示右边界对称点,那么
\( f[i][1] = g[x][i][1] - g[L(x)][i][1] - g[R(x)][i][1] + g[L(R(x))][i][1] + g[R(L(x))][i][1] - g[L(R(L(x)))][i][1] - g[R(L(R(x)))][i][1] + ...... \)
每次作对称(除了第一次),对称点到\(1\)(或\(ed\))的距离会至少增加\(ed\)。所以求\(f[i][1]\)的复杂度是\(O(i/ed)\)。
(说实话这个容斥还是不太好想呃……我们再详细一点:从后往前比较好想,每次减去的东西已经被容斥过了;也就是上面式子写成
\( f[i][1] = g[x][i][1] - (g[L(x)][i][1] - (g[R(L(x))][i][1] - (g[L(R(L(x)))][i][1] - ... ))) \)
\( - (g[R(x)][i][1] - (g[L(R(x))][i][1] - (g[R(L(R(x)))][i][1] - ...))) \)
初始\(g[x][i][1]\)要减去\(g[L(x)][i][1]\)和\(g[R(x)][i][1]\),此时的含义应该是减去最后一次穿过的边界是右/左边界的不合法路线。
就\(g[L(x)][i][1]\)来说,它代表的应该是从\(x\)出发,最后一次穿过的边界是左边界,的不合法路线。所以它需要减去穿过左边界后又穿过右边界再回到\(1\)的路线,即从\(L(x)\)出发,最后一次穿过的是右边界的不合法路线(这里“不合法”是针对\(L(x)\)为起点说的:从里面(\(1—ed\))穿过边界到外面,即“不合法”)。再用上面的对称思想,可知这个值是\(g[R(L(x))][i][1]\)。
注意这里是倒着减的,就是说我们在减这一步的时候\(g[R(L(x))][i][1]\)已经处理好了,它的含义就是从\(L(x)\)出发,最后一次穿过的是右边界的不合法路线(用上面的对称思想,就是把\(L(x)\)对称到右边去(即\(R(L(x))\))以后走到\(1\)的路线;当然这需要进一步处理,在这里是处理好的)。
那么考虑\(g[R(L(x))][i][1]\)是怎么处理的:从\(R(L(x))\)出发的所有路线,减去最后一次穿过的是左边界的不合法路线。所以它这里需要被减去的是已经处理好的\(g[L(R(L(x)))][i][1]\)。
到这里我们应该能看出来了,每次的操作实际上是一样的,而且左右边界交替进行。这就形成了上面的式子。
至于终点,我们也看到了,这样对称来对称去,距离越来越大,总有某一时刻距离大于步数,走不到了,方案数是\(0\)。这代表什么呢?这说明某个\(g\)需要减的数是\(0\),也就是无限制算出来的那个\(g\)就是满足我们设定含义的\(g\)。有了这第一个满足含义的\(g\),我们就可以接着做了。)
这个做法整体的复杂度是\(O(\frac{t^2}{n})\)。当\(n\)比较大的时候可做。
那么,当\(n < \sqrt{5*10^5} \)的时候我们用DP做法,否则用第二种做法。时间复杂度是\(O(t\sqrt{t})\),完美。
另:
过程中某次运行发现VSCode出现了大段复杂奇妙报错,最后一句是“collect2.exe: ld returned 1 exit status”。大意是main函数写错了?或者已经在运行程序??后来发现其他代码还能运行。后来发现是数组开爆了。应该用滚动数组。记住这个报错。
然后因为不慎把一个\(>\)写成了\(<\),调了几个小时。竟一直没看出来囧。
代码如下:
#include<iostream> #define ll long long using namespace std; int const N=5e5+5,md=998244353; int t,n,m,a,b; ll f[2][2][N],K[2][N],fc[N],inv[N]; ll pw(ll x,ll y) { ll ret=1,na=x; while(y) { if(y&1)ret=(ret*na)%md; na=(na*na)%md; y>>=1; } return ret; } void init() { fc[0]=1; for(int i=1;i<=t;i++)fc[i]=((ll)fc[i-1]*i)%md; inv[t]=pw(fc[t],md-2); for(int i=t-1;i>=0;i--)inv[i]=((ll)inv[i+1]*(i+1))%md; } ll C(int x,int y){return (ll)fc[x]*inv[y]%md*inv[x-y]%md;} void work(int u,int x) { f[u][0][x]=1; K[u][0]=1; int p=1,ed=(u)?m:n; for(int i=1;i<=t;i++) { for(int j=2;j<ed;j++)f[u][p][j]=(f[u][p^1][j-1]+f[u][p^1][j+1])%md; f[u][p][1]=f[u][p^1][2]; f[u][p][ed]=f[u][p^1][ed-1]; for(int j=1;j<=ed;j++)K[u][i]=(K[u][i]+f[u][p][j])%md; p^=1; } } int L(int x){return -x;} int R(int x,int u){return (u)?(2*(m+1)-x):(2*(n+1)-x);} ll ab(ll x){return (x<0)?-x:x;} ll calg(int x,int y,int nt) { int d=ab(x-y); if(nt<d||(nt-d)%2)return 0; return C(nt,(nt-d)/2); } ll mo(ll x){x%=md; if(x<0)x+=md; return x;} ll calf(int u,int i,int x,int y) { //if(!u&&i==2)printf("x=%d y=%d\n",x,y); if(ab(x-y)>i)return 0;///< ll ret=calg(x,y,i),fl=-1,lstl=x,lstr=x; while(1) { //if(!u&&i==2)printf("x=%d y=%d ret=%d lstl=%d lstr=%d\n",x,y,ret,lstl,lstr); int pl=L(lstr),pr=R(lstl,u); if(ab(y-pl)>i&&ab(y-pr)>i)break; ret=mo((ll)ret+fl*calg(pl,y,i)); ret=mo((ll)ret+fl*calg(pr,y,i)); fl=-fl; lstl=pl; lstr=pr; } return ret; } void work2(int u,int x) { K[u][0]=1; int ed=(u)?m:n; for(int i=0;i<t;i++) { ll f1=calf(u,i,x,1),f2=calf(u,i,x,ed); K[u][i+1]=(((2*K[u][i]%md-f1)%md-f2)%md+md)%md; } } int main() { scanf("%d%d%d%d%d",&t,&n,&m,&a,&b); init(); if(n<=1000)work(0,a); else work2(0,a); if(m<=1000)work(1,b); else work2(1,b); // work2(0,a); work2(1,b); // for(int p=0;p<=1;p++) // for(int i=0;i<=t;i++) // printf("K[%d][%d]=%lld\n",p,i,K[p][i]); ll ans=0; for(int i=0;i<=t;i++) ans=((ll)ans+(ll)K[0][i]*K[1][t-i]%md*C(t,i)%md)%md; printf("%lld\n",ans); return 0; }