2022牛客多校联赛第十场 题解

比赛传送门
作者: fn


签到题

H题 Wheel of Fortune / 命运之轮

题目大意
炉石背景
给定双方英雄血量,随机释放炎爆(打10)直到一名玩家死亡,求获胜概率。

考察内容
数学

分析
分类讨论,设对手的血量最多抗 n u m x numx numx 次,自己最多抗 n u m y numy numy 次,
获胜的概率即对手抗了 n u m x numx numx 次炎爆且自己恰好抗了 0 0 0 次炎爆, 1 1 1 次炎爆,…, n u m y − 1 numy-1 numy1 次炎爆的概率之和。最后一发炎爆留给对手。

计算每种情况的概率并求和即可。

#pragma GCC optimize(3)
#include<bits/stdc++.h>
#define ll long long
#define cer(x) cerr<<(#x)<<" = "<<(x)<<'\n'
#define endl '\n'
using namespace std;
const int N=1e6+10; // 抗炎爆的次数<=1e6
const ll mod=998244353;

ll power(ll a,ll b){ // a^b%mod
	ll c=1;
	for(;b;b>>=1){
		if(b&1)c=c*a%mod;
		a=a*a%mod;
	}
	return c;
}

ll n,m,x,y,a[10],b[10];
ll f[N];
ll po2[N*2];

int main(){ 
	ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
	cin>>x;
	for(int i=1;i<=7;i++){
		cin>>a[i];
	}
	cin>>y;
	for(int i=1;i<=7;i++){
		cin>>b[i];
	}
	
	swap(x,y); // 
	
	ll numx=(x+10-1)/10; // 可以抗炎爆的次数 
	ll numy=(y+10-1)/10;

	ll ans=0;
	
	po2[0]=1;
	for(int i=1;i<=numx+numy;i++){ // 预处理2的幂次 
		po2[i]=po2[i-1]*2%mod; 
	}
	
	ll sumx=1; // 1*2*...*(numx-1) 
	for(int i=2;i<=numx-1;i++){
		sumx=sumx*i%mod;
	}
	ll invsumx=power(sumx,mod-2); // sumx的逆元 
	
	ll sum=sumx;
	for(int i=0;i<=numy-1;i++){ // 枚举y被打中的次数 
		ll cnt=sum*invsumx%mod;  // C(i+numx-1,numx-1)%mod;
		sum=sum*(i+1+numx-1)%mod;
		sum=sum*power(i+1,mod-2)%mod; // 除以(i+1)
		
		cnt=cnt*power(po2[numx-1+i],mod-2)%mod;
		
		ans+=cnt;
		ans%=mod;
	}
	
	cout<<ans%mod*499122177%mod<<endl; // *(1/2)
	return 0;
}
/*
30 0 0 0 0 0 0 0
30 0 0 0 0 0 0 0

20 0 0 0 0 0 0 0
30 0 0 0 0 0 0 0

*/ 

基本题

F题 Shannon Switching Game? / 香农切换游戏?

题目大意
给定一个可以包含重边的无向图,初始在点 s s s ,两个玩家轮流行动,先手每次可以移除一条和所在位置相邻的边, 后手每次可以沿着一条未删除边移动,如果可以移动到 t t t 则后手获胜,否则先手获胜。

求双方最优策略下的胜者。

考察内容
博弈论,bfs

分析
终点是一个必胜态。
如果当前点 x x x 到终点 t t t 有两条边,则无论切掉哪一条都能走到终点 t t t ,所以 x x x 也是必胜态,加入必胜的点的集合。

如果当前点 y y y 到必胜的点有两条边,则 y y y 也是必胜的点,加入必胜的点的集合。

从终点 t t t 开始跑bfs,找到所有必胜的点的集合,剩下的就是必败的点。判断 s s s 是否在集合中即可。

#pragma GCC optimize(3)
#include<bits/stdc++.h>
#define ll long long
#define cer(x) cerr<<(#x)<<" = "<<(x)<<'\n'
#define endl '\n'
using namespace std;
const int N=105;
ll n,m,s,t;
vector<ll> g[N];

int vis[N]; // 是否在取胜集合中 
ll num[N]; // 取胜的路径数量 

queue<ll> q;

void bfs(){
	vis[t]=1; // 进入取胜集合 
	q.push(t);
	while(!q.empty()){
		ll t1=q.front();
		q.pop();
		
		for(auto a1:g[t1]){
			if(vis[a1])continue;
			
			num[a1]++;
			if(num[a1]>=2){ // 路径数量>=2 
				q.push(a1);
				vis[a1]=1;
			} 
		} 
	}	
}

void init(ll n){ // 初始化 
	memset(vis,0,sizeof(vis[0])*(n+1));
	memset(num,0,sizeof(num[0])*(n+1));
	
	for(int i=1;i<=n;i++){
		g[i].clear();
	}
}

int main(){ // bfs
	ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
	int t0; cin>>t0;
	while(t0--){
		cin>>n>>m>>s>>t;
		
		init(n);
		
		for(int i=1;i<=m;i++){
			ll u,v;
			cin>>u>>v;
			g[u].push_back(v);
			g[v].push_back(u);
		}
		
		bfs();
		
		if(!vis[s])cout<<"Cut Player"<<endl; // 不在取胜集合中,先手获胜 
		else cout<<"Join Player"<<endl; // 后手获胜 
	}
	return 0;
}
/*
1
4 6 1 4
1 2
1 3
2 4
2 4
3 4
3 4

*/ 

进阶题

I题 Yet Another FFT Problem? / 还有另一个FFT问题?

题目大意
给定两个序列 a , b a,b a,b ,找出下标 i , j , k , l i,j,k,l i,j,k,l ,使得 a [ i ] + b [ l ] = a [ j ] + b [ k ] a[i]+b[l]=a[j]+b[k] a[i]+b[l]=a[j]+b[k] i ≠ j , k ≠ l i≠j,k≠l i=j,k=l 。若不存在则输出-1。
0 ≤ a [ i ] , b [ i ] ≤ 1 0 7 0 \leq a[i],b[i] \leq 10^7 0a[i],b[i]107

考察内容
鸽笼原理,枚举

分析
亲测直接FFT或NTT等复杂度带log的做法过不了。。。(又被标题骗了。。。)

假设 a , b a,b a,b 中均无重复元素。
由鸽笼原理, 因为 a [ i ] + b [ l ] ≤ V ≤ 2 ∗ 1 0 7 a[i]+b[l] \leq V \leq 2*10^7 a[i]+b[l]V2107 ,所以遍历不同的元素 O ( V ) O(V) O(V) 次就一定可以找到一组答案。

a 、 b a、b ab 去重之后进行枚举即可。注意考虑两对相同元素的情况。
时间复杂度 O ( V ) O(V) O(V)

#pragma GCC optimize(3)
#include<bits/stdc++.h>
#define ll long long
#define cer(x) cerr<<(#x)<<" = "<<(x)<<'\n'
#define endl '\n'
using namespace std;
const int N=1e6+5;
const int MM=2e7+5;

int n,a[N],b[N],m;
int n1,A[N]; // A为去重后的a数组的下标 
int nx[MM],ny[MM]; 
int vis[MM>>1]; // vis[i]记录i在a/b中的下标 
int ggx=0,ggy; // 记录a中一对相同元素的下标 
bool ok=0;

int main(){ // 鸽笼原理。复杂度O(V)。AC
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) {
		scanf("%d",&a[i]);
		
		if(vis[a[i]]){ // a[i]之前已经出现过
			if(ggx==0){ // 以前没发现对子 
				ggx=vis[a[i]]; // 上一个a[i]的下标 
				ggy=i; // 这个a[i]的下标 
			}
		} 
		else{ // a[i]第一次出现 
			A[++n1]=i; // 记录去重后的a数组的下标
		} 
		vis[a[i]]=i; // 记录a[i]出现的下标 
	}
	for(int i=1;i<=m;i++) { 
		scanf("%d",&b[i]); // 读入b[i]
	}
	

	memset(vis,0,sizeof vis); // 清空vis,接下来vis[i]用来记录i在b中的下标
	for(int i=1;i<=m;i++) { // 遍历b[i] 
		if(vis[b[i]]) { // b[i]出现过 
			if(ggx) { // a中存在"对子"
				printf("%d %d %d %d\n",ggx,ggy,vis[b[i]],i); // 输出两对相同元素
				ok=1; // 找到了 
				break; 
			}
			continue;
		}
		
		// vis[b[i]]==0,b[i]第一次出现 
		vis[b[i]]=i;
		for(int j=1;j<=n1;j++) { // 枚举a中的元素(已通过A[i]去重) 
			int u=1e7; // 偏移量 
			int o=b[i]-a[A[j]]+u; // 计算nx的下标 
			if(nx[o]){ // 找到一组"碰撞" 
				printf("%d %d %d %d\n",nx[o],A[j],ny[o],i);
				ok=1;
				break;
			}
	 	
			nx[o]=A[j];
			ny[o]=i;
		}
		
		if(ok) break; // 已经找到,提前退出 
	}
	
	if(!ok) puts("-1"); // 没找到,输出-1 
}

E题 Reviewer Assignment / 审稿人分配

题目大意
n n n 个审稿人和 m m m 篇论文,给出每个审稿人能审论文的集合,给每个审稿人安排一篇论文。
f ( i ) f(i) f(i) 表示被至少 i i i 个审稿人审过的论文数量,要求求出一种分配方案,使得 ( f ( 1 ) , f ( 2 ) , . . . , f ( n ) ) (f(1),f(2),...,f(n)) (f(1),f(2),...,f(n)) 字典序最大。

1 ≤ n , m ≤ 400 1≤n,m≤400 1n,m400

考察内容
二分图匹配,匈牙利算法,最大流,费用流

分析

解法一(最小费用最大流):
建最小费用流图:
源点向每个审稿人连接容量为 1 1 1 ,花费为 0 0 0 的边;
每个审稿人向能审的论文连接容量为 1 1 1 ,花费为 0 0 0 的边;
每篇论文向汇点连接 n n n 条容量为 1 1 1 ,花费为 1 , 2 , . . . , n 1,2,...,n 1,2,...,n 的边。
然后计算最小费用最大流。

解法二(二分图匹配):
注意到要求 f ( 1 ) f(1) f(1) 最大就是求二分图最大匹配,可以用匈牙利算法或最大流解决。
对于 i = 1 , 2 , . . . , n i=1,2,...,n i=1,2,...,n ,在不改变 f ( 1 ) , f ( 2 ) , . . . f ( i − 1 ) f(1),f(2),...f(i-1) f(1),f(2),...f(i1) 的情况下最大化 f ( i ) f(i) f(i)

解法二代码:

#include<bits/stdc++.h>
using namespace std;
bool Mark[405];
bool Vis[405];
int Fa[405],Cnt[405];
vector<int> Edge[405];

bool Hungary(int u) { // 匈牙利算法(增广路算法) 
	for(int i=0; i<Edge[u].size(); i++) {
		int v=Edge[u][i];
		if(!Mark[v]) {
			Mark[v]=1;
			if(Fa[v]==-1 || Hungary(Fa[v])) {
				Fa[v]=u;
				return true;
			}
		}
	}
	return false;
}

int n,m;
string S;
int main() { // 匈牙利算法求二分图匹配 
	memset(Fa,-1,sizeof(Fa));
	scanf("%d%d",&n,&m);
	bool flag=1;
	for(int i=1; i<=n; i++) {
		cin>>S;
		bool f=0;
		for(int j=0; j<m; j++){
			if(S[j]=='1'){ // 可以看这篇论文 
				Edge[j+1].push_back(i); // 连边 
				f=1;
			}
		}	
		flag&=f;
	}
	if(!flag) {
		puts("-1");
		return 0;
	}

	int mx=0; // 记录cnt的最小值 
	bool f=1;
	while(f) {
		f=0;
		for(int i=1; i<=m; i++) { // 枚举每一篇论文 
			if(Cnt[i]!=mx)continue;
			
			memset(Mark,0,sizeof(Mark));
			if(Hungary(i)){
				Cnt[i]++;
				f=1;
			}
		}
		mx++;
	}
	
	for(int i=1; i<=n; i++)
		printf("%d ",Fa[i]);
	puts("");
}

2022暑期多校联赛完结撒花!

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值