5.4 考试总结

60+50+10+21=141pts,rk6。

A.Lesson 5!

容易想到使用传奇拓扑排序对所有点进行拓扑排序,然后我们考虑实用数据结构(multiset/可删堆/权值线段树)对答案进行维护。我们设以 \(x\) 为终点的最长路长度为 \(f_x\),以 \(x\) 为起点的最长路长度为 \(g_x\)。具体方式如下:

  1. 最开始所有点都在 \(B\) 集合,将所有 \(g_x\) 加入数据结构中。
  2. 重复下列操作,直到 \(B\) 集合为空集:
    1. 将目前 \(B\) 集合中拓扑序最大的点 \(x\) 取出 \(B\) 集合。此时在 \(B\) 集合中没有点连向 \(x\)
    2. \(g_x,f_c+g_x+1\) 从数据结构中删去,其中 \(c\) 满足有一条从 \(c\) 连向 \(x\) 的边。
    3. \(f_x,f_x+g_c+1\) 加入数据结构中,其中 \(c\) 满足有一条从 \(x\) 连向 \(c\) 的边。
    4. 此时数据结构中最大的数 \(num\) 即为删除 \(x\) 点时的最长路径。

时间复杂度 \(O(n\log n)\),建议使用码量短且速度快的可删堆。

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int t,n,m,f[N],g[N],st[N];
int id[N],rd[N],cd[N],tp;
struct que{
	priority_queue<int>a,b;
	int top(){
		while(b.size()&&a.top()==b.top())
			a.pop(),b.pop();return a.top();
	}void clear(){
		while(a.size()) a.pop();
		while(b.size()) b.pop();
	}void push(int x){a.push(x);}
	void pop(int x){b.push(x);}
}q;vector<int>vg[N],ve[N];
void solve(){
	memset(f,0,sizeof(f));
	memset(g,0,sizeof(g));
	for(int i=1;i<=n;i++)
		ve[i].clear(),vg[i].clear();
	cin>>n>>m;int c=0,ai,as=1e9;
	if(n==1&&m==0) return cout<<"1 0\n",void();
	for(int i=1,x,y;i<=m;i++){
		cin>>x>>y,rd[y]++,cd[x]++;
		vg[x].push_back(y),ve[y].push_back(x);
	}for(int i=1;i<=n;i++)
		if(!rd[i]) st[++tp]=i;
	while(tp){
		int x=st[tp--];
		for(auto y:vg[x]){
			rd[y]--,f[y]=max(f[y],f[x]+1);
			if(!rd[y]) st[++tp]=y;
		}id[++c]=x;
	}for(int i=1;i<=n;i++)
		if(!cd[i]) st[++tp]=i;
	while(tp){
		int x=st[tp--];
		for(auto y:ve[x]){
			cd[y]--,g[y]=max(g[y],g[x]+1);
			if(!cd[y]) st[++tp]=y;
		}q.push(g[x]);
	}for(int i=1;i<=n;i++){
		q.pop(g[id[i]]);
		for(auto x:ve[id[i]])
			q.pop(f[x]+g[id[i]]+1);
		if(q.top()<as) ai=id[i],as=q.top();
		else if(q.top()==as) ai=min(ai,id[i]); 
		q.push(f[id[i]]);
		for(auto x:vg[id[i]])
			q.push(f[id[i]]+g[x]+1);
	}cout<<ai<<" "<<as<<"\n",q.clear();
}int main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>t;
	while(t--) solve();
	return 0;
}//Italy is AC Milan!

B.贝尔数

首先,你需要知道 \(95041567=31\times 37\times 41\times 43\times 47\)

这样我们就可以考虑使用中国剩余定理,分别解决每个质因数,然后通过中国剩余定理将之合并。

根据性质一,我们可以推出前 \(47\) 个贝尔数。

根据性质二,我们可以想到用矩阵快速幂优化。具体的,若模数为 \(p\),我们先建立一个 \(1\times p\) 的矩阵,记录 \(B_0\)\(B_{p-1}\),然后建立一个 \(p\times p\) 的转移矩阵,使得我们可以转移到一个记录 \(B_p\)\(B_{2p-1}\)\(1\times p\) 的矩阵。

时间复杂度 \(O(t\sum p_i^3\log\frac n{p_i}+47^2)\),其中 \(p=\{31,37,41,43,47\}\)

#include<bits/stdc++.h>
using namespace std;
int t,n,C[1005][1005],B[1005],a[5];
int md[5]={31,37,41,43,47},p=95041567;
struct matrix{int z[50][50];};
void clear(matrix &mt){
	for(int i=1;i<=p;i++)
		for(int j=1;j<=p;j++) mt.z[i][j]=0;
}matrix operator*(matrix x,matrix y){
	matrix z;clear(z);
	for(int i=1;i<=p;i++) for(int j=1;j<=p;j++)
		for(int k=1;k<=p;k++) z.z[i][j]=(z.z[i][j]+x.z[i][k]*y.z[k][j])%p;
	return z;
}int solve(){
	matrix ans,turn;int cc=n/p;
	clear(ans),clear(turn);
	for(int i=1;i<=p;i++)
		ans.z[1][i]=B[i-1]%p;
	for(int i=1;i<p;i++)
		turn.z[i][i]=turn.z[i+1][i]=1;
	turn.z[1][p]=turn.z[2][p]=turn.z[p][p]=1;
	while(cc){
		if(cc&1) ans=ans*turn;
		turn=turn*turn,cc>>=1;
	}return ans.z[1][n%p+1];
}int qpow(int x,int y,int pr){
	int re=1;x%=pr;
	while(y){
		if(y&1) re=re*x%pr;
		x=x*x%pr,y>>=1;
	}return re;
}int main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>t,C[0][0]=B[0]=B[1]=1;
	for(int i=1;i<1000;i++){
		C[i][0]=1;
		for(int j=1;j<=i;j++)
			C[i][j]=(C[i-1][j]+C[i-1][j-1])%p;
		for(int j=0;j<=i;j++)
			B[i+1]=(B[i+1]+1ll*C[i][j]*B[j])%p;
	}while(t--){
		cin>>n;int ans=0;
		if(n<=1000){cout<<B[n]<<"\n";continue;}
		for(int i=0;i<5;i++) p=md[i],a[i]=solve();
		for(int i=0,m=1;i<5;i++,m=1){
			for(int j=0;j<5;j++) if(i!=j) m*=md[j];
			ans+=1ll*a[i]*qpow(m,md[i]-2,md[i])*m%95041567;
		}cout<<ans%95041567<<"\n";
	}return 0;
}//Italy is AC Milan!

C. 穿越广场

考场有大常数做法,但是挂分了,痛失 70pts。

相当于你有 \(n+m\) 个字符,其中有 \(n\)\(R\)\(m\)\(D\),问你有多少种排列方式使得排列后形成的字符串包含两个给出的字符串。

容易想到设 \(dp_{i,j,k,l,r}\) 表示目前填了 \(i\) 个字符,其中有 \(r\)\(R\),状态为 \(j\)(0 指没有包含任何一个给定字符串,1 指包含第一个给定字符串,2 指包含第二个给定字符串,3 指两个都包含),当前对应第一个给定字符串的第 \(k\) 位,和第二个给定字符串的第 \(l\) 位。

使用刷表法转移,转移方程为:

\[s_{k+1}=t_{l+1}=R\begin{cases} f_{i,j,k,l,r}\to f_{i+1,j|[k+1=len_s]|(2\times[l+1=len_t]),k+1,l+1,r+1}\\ f_{i,j,k,l,r}\to f_{i+1,j,ns_{k+1},nt_{l+1},r} \end{cases}\\ s_{k+1}=D,t_{l+1}=R\begin{cases} f_{i,j,k,l,r}\to f_{i+1,j|(2\times[l+1=len_t]),ns_{k+1},l+1,r+1}\\ f_{i,j,k,l,r}\to f_{i+1,j|[k+1=len_s],k+1,nt_{l+1},r} \end{cases}\\ s_{k+1}=R,t_{l+1}=D\begin{cases} f_{i,j,k,l,r}\to f_{i+1,j|[k+1=len_s],k+1,nt_{l+1},r+1}\\ f_{i,j,k,l,r}\to f_{i+1,j|(2\times[l+1=len_t]),ns_{k+1},l+1,r} \end{cases}\\ s_{k+1}=t_{l+1}=D\begin{cases} f_{i,j,k,l,r}\to f_{i+1,j,ns_{k+1},nt_{l+1},r+1}\\ f_{i,j,k,l,r}\to f_{i+1,j|[k+1=len_s]|(2\times[l+1=len_t]),k+1,l+1,r} \end{cases}\]

其中 \(s,t\) 指代第一、二个给定字符串,\(len_s,len_t\) 表示两个给定字符串的长度,\(ns_i\) 表示最大的 \(j\),满足 \(s_{1-(j-1)}=s_{(i-j+1)-(i-1)}\)\(s_j\ne s_i\)\(nt_i\) 同理。

时空复杂度都比较大,所以在空间上滚动数组,在时间上使用记忆化 \(bfs\) 的手法进行 \(dp\)。同时考虑到假如 \(k=3\),那么这个状态 \(c\) 的贡献一定是 \(\binom{n+m-i-1}{n-r}c\),可以直接加上。时间复杂度 \(O(6tn^4)\)

#include<bits/stdc++.h>
using namespace std;
const int N=105,p=1e9+7;
int n,m,ls,lt,ns[N],nt[N],C[N*2][N*2];
int tc,f[2][3][N][N][N];string s,t;
struct node{int i,j,k,l,r;};
int md(int x){return x-(x>=p)*p;}
void solve(){
	memset(f,0,sizeof(f));queue<node>q;
	cin>>n>>m>>s>>t,f[0][0][0][0][0]=1;int ans=0;
	ls=s.size(),s=" "+s,lt=t.size(),t=" "+t;
	for(int i=1;i<=ls+1;ns[++i]=0)
		for(int j=i-1;j;j--){
			int flag=(s[j]!=s[i]);
			if(!flag) continue;
			for(int l=1,r=i-j+1;l<j;l++,r++)
				flag&=(s[l]==s[r]);
			if(flag){ns[i]=j;break;}
		}
	for(int i=1;i<=lt+1;nt[++i]=0)
		for(int j=i-1;j;j--){
			int flag=(t[j]!=t[i]);
			if(!flag) continue;
			for(int l=1,r=i-j+1;l<j;l++,r++)
				flag&=(t[l]==t[r]);
			if(flag){nt[i]=j;break;}
		}
	q.push({0,0,0,0,0});
	for(int i=0,e=1;i<n+m;i++,e^=1)
		while(q.front().i==i){
			node cc=q.front();q.pop();
			int j=cc.j,k=cc.k,l=cc.l,r=cc.r;
			int jn=j,kn=k+1,ln=l+1;
			int wzq=f[e^1][j][k][l][r];
			f[e^1][j][k][l][r]=0;
			if(r<n){
				kn=(s[kn]=='R'?kn:ns[kn]);
				ln=(t[ln]=='R'?ln:nt[ln]);
				jn|=(kn==ls)|(2*(ln==lt));
				if(jn<3){
					if(!f[e][jn][kn][ln][r+1]) q.push({i+1,jn,kn,ln,r+1});
					f[e][jn][kn][ln][r+1]=md(f[e][jn][kn][ln][r+1]+wzq);
				}else ans=(ans+1ll*wzq*C[n+m-i-1][n-r-1])%p;
			}if(i-r==m) continue;
			kn=(s[k+1]=='D'?k+1:ns[k+1]);
			ln=(t[l+1]=='D'?l+1:nt[l+1]),jn=j;
			jn|=(kn==ls)|(2*(ln==lt));
			if(jn<3){
				if(!f[e][jn][kn][ln][r]) q.push({i+1,jn,kn,ln,r});
				f[e][jn][kn][ln][r]=md(f[e][jn][kn][ln][r]+wzq);
			}else ans=(ans+1ll*wzq*C[n+m-i-1][n-r])%p;
		}
	cout<<ans<<"\n";
}int main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>tc,C[0][0]=1;
	for(int i=1;i<=200;i++){
		C[i][0]=1;
		for(int j=1;j<=i;j++)
			C[i][j]=(C[i-1][j-1]+C[i-1][j])%p;
	}while(tc--) solve();
	return 0;
}//Italy is AC Milan!

D. 欢乐豆

今天第一天,送您 3000 豆!

考虑实际需要考虑的只有 \(2m+1\) 个点,因为所有没有出现在特殊边中的点贡献都是 \((n-1)a_i\),所以只需要特别考虑 \(2m\) 个点。由于可能会出现走一个只经过原来的边的中转站,所以再加入其他点中 \(a\) 值最小的点。

由于没有特殊出边的点贡献和没有特殊边的点的贡献相同,所以我们只需要计算从有特殊出边的点出发到所有要考虑的点的距离。想到直接跑单源最短路 \(dij\)。但是普通堆优化的 \(dij\) 时间复杂度为 \(O(E\log E)\),其中 \(E\) 在这个问题中表示完全图中的边数,所以根本不可行。考虑使用线段树优化 \(dij\)

实际上,\(dij\) 的优化本质就是找到还没有明确 \(dis\) 值中距离源点最近的点,所以也可以使用线段树优化。不同的是,对于本题相同边很多的情况,我们可以进行区间修改。这样我们就可以做到时间复杂度 \(O(e\log e)\)。与上文 \(E\) 不同的是,这里的 \(e\) 只指特殊边。

这样,我们就可以做到时间复杂度 \(O(n+m^2\log m)\)。需要卡常。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5,M=6005;
struct edge{int to,w;};vector<edge>g[N];
int n,m,dis[M],ans,vs[M],idx[M],vis[N];
int a[N],tot,id[N],mn[M*4],idm[M*4],tag[M*4];
int cmp(edge x,edge y){return x.to<y.to;}
void push_up(int x){
	if(vs[idm[x*2]]) mn[x]=mn[x*2+1],idm[x]=idm[x*2+1];
	else if(vs[idm[x*2+1]]) mn[x]=mn[x*2],idm[x]=idm[x*2];
	else if(mn[x*2]<mn[x*2+1]) mn[x]=mn[x*2],idm[x]=idm[x*2];
	else mn[x]=mn[x*2+1],idm[x]=idm[x*2+1];
}void down(int x,int c){
	mn[x]=min(mn[x],c),tag[x]=min(tag[x],c);
}void push_down(int x){
	down(x*2,tag[x]),down(x*2+1,tag[x]),tag[x]=1e18;
}void build(int x,int l,int r){
	mn[x]=tag[x]=1e18,idm[x]=l;
	if(l==r) return;int mid=(l+r)/2;
	build(x*2,l,mid),build(x*2+1,mid+1,r);
}void minn(int x,int l,int r,int L,int R,int c){
	if(L>R) return;
	if(L<=l&&r<=R) return down(x,c);
	int mid=(l+r)/2;push_down(x);
	if(L<=mid) minn(x*2,l,mid,L,R,c);
	if(R>mid) minn(x*2+1,mid+1,r,L,R,c);
	push_up(x);
}void change(int x,int l,int r,int k,int c){
	if(l==r) return mn[x]=c,void();
	int mid=(l+r)/2;push_down(x);
	if(k<=mid) change(x*2,l,mid,k,c);
	else change(x*2+1,mid+1,r,k,c);
	push_up(x);
}void dij(int s){
	memset(vs,0,sizeof(vs));
	memset(dis,0x3f,sizeof(dis));
	build(1,1,tot),change(1,1,tot,s,0);
	for(int num=1,x;num<=tot;num++){
		dis[x=idm[1]]=mn[1],vs[x]=1;
		change(1,1,tot,x,1e18);int lst=1;
		for(int i=0;i<g[x].size();i++){
			int y=g[x][i].to,w=g[x][i].w;
			minn(1,1,tot,lst,y-1,dis[x]+a[idx[x]]);
			minn(1,1,tot,y,y,dis[x]+w),lst=y+1;
		}minn(1,1,tot,lst,tot,dis[x]+a[idx[x]]);
	}for(int i=1;i<=tot;i++) ans+=dis[i];
}signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0),cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>a[i];
	for(int i=1,x,y,z;i<=m;i++){
		cin>>x>>y>>z,vis[x]=1;
		if(!id[y]) id[y]=++tot,idx[tot]=y;
		if(!id[x]) id[x]=++tot,idx[tot]=x;
		g[id[x]].push_back({id[y],z});
	}int mx=0;a[0]=1e18;
	for(int i=1;i<=n;i++)
		if(a[i]<a[mx]&&!id[i]) mx=i;
	if(mx) id[mx]=++tot,idx[tot]=mx;
	for(int i=1;i<=tot;i++)
		sort(g[i].begin(),g[i].end(),cmp);
	for(int i=1;i<=n;i++){
		if(vis[i]){
			dij(id[i]);int mni=1e18;
			for(int j=1;j<=tot;j++)
				mni=min(mni,dis[j]+a[idx[j]]);
			ans+=mni*(n-tot);
		}else ans+=(n-1)*a[i];
	}return cout<<ans,0;
}//Italy is AC Milan!
posted @ 2025-05-05 09:12  长安一片月_22  阅读(17)  评论(0)    收藏  举报