爆零考试后一场

爆零考试后一场T1wait

题目描述

题目描述
有一n*m的棋盘,每次随机染黑一个位置(可能染到已经黑了的),当某一行或者一列全为黑色时停止,求期望染色次数(mod 998244353)
输入
一行两个正整数n,m
输出
期望结果
数据范围
对于20%的数据n,m<=5
对于100%的数据n,m<=1000

思路

不知道为什么每次考试看到数学期望值总是1分都得不到我们看到这道题,我们可以先确定一点,我们先设答案为ans(不是期望答案),则 a n s = m i n ( m i n ( a i ) ( i ∈ [ 1 , n ] ) , m i n ( b i ) ( i ∈ [ 1 , m ] ) ) ans=min(min(a_i)(i\in[1,n]),min(b_i)(i\in[1,m])) ans=min(min(ai)(i[1,n]),min(bi)(i[1,m])),其中 a i b i {a_i}{b_i} aibi表示第i行,第i列的最小步数,我们这里又有一个公式 a n s = ∑ S ∈ U ( − 1 ) ∣ S ∣ + 1 m a x S ans=\sum_{S \in U}(-1)^{|S|+1}max{S} ans=SU(1)S+1maxS,这是基于容斥原理推出来的,至于为什么我之后再补上来吧因为我也不知道然后,我们求ans的数学期望值,我们可以设为 E ( a n s ) E(ans) E(ans),我们实际上就是求 ∑ S ∈ U ( − 1 ) ∣ S ∣ + 1 E ( m a x S ) \sum_{S \in U}(-1)^{|S|+1}E(max{S}) SU(1)S+1E(maxS),U就是所有 a i , b i a_i,b_i ai,bi所组成的一个集合,我们考虑如何求出这个值,我们想一下假设当前取出i行,j列,则我们可以用容斥算出取了多少个,即 i ∗ m + j ∗ n − i ∗ j i*m+j*n-i*j im+jnij,这个肯定没有问题吧?除非是个脑残,我们把它带到 S S S里面,就可以知道它的值就为 ( n i ) ∗ ( m j ) ∗ ∑ i = 0 i ∗ m + j ∗ n − i ∗ j n ∗ m i {n \choose i} * {m \choose j} * \sum_{i=0}^{i*m+j*n-i*j} \frac{n*m}i (in)(jm)i=0im+jnijinm,我觉得可能后面那个 ∑ \sum 可能比较难以理解,实际上我们可以通过观察一下,假设从n行里选1行,m列里选1列我可能行列不分,我们就选了1个数,就是从 n ∗ m n*m nm中选出一个数,就是 1 n ∗ m \frac{1}{n*m} nm1,数学期望步数从理论上来说就是 n ∗ m 1 \frac{n*m}{1} 1nm次就可以填满,然后选2个数的话也一样,但由于是求数学期望,就是加上一个 n ∗ m 2 \frac{n*m}2 2nm了,而不是乘,我们就可以理解到那个 ∑ \sum 的含义了,然后我们再具体求值的时候,本来理论上应该是 2 x 2^x 2x,其中x由n和m决定,但我们实际上可以通过枚举i和j使我们的时间复杂度由一个指数级别降到 O ( n 2 ) O(n^2) O(n2),然后逆元我们可以通过线性递推求出来,然后这道题就这样了,也没有什么好说的了吧

代码

#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;

#define Int register int
#define int long long
#define MAXN 1005
#define mod 998244353

int n,m;
int fac[MAXN],f[MAXN * MAXN],inv[MAXN * MAXN],invfac[MAXN * MAXN];

int C(int n,int m)
{
	return fac[n] * invfac [m] % mod * invfac [n - m] % mod;
}

void init()
{
	f[0] = 1,f[1] = n * m,inv[1] = 1;
	for (Int i = 2;i <= n * m;++ i)
	{
		inv[i] = (int)(mod - mod / i) * inv[mod % i] % mod;
		f[i] = (f[i - 1] + n * m * inv [i] % mod) % mod;
	}
	fac[0] = 1;invfac[0] = 1;
	for (Int i = 1;i <= max(n,m);++ i) 
	{
		fac[i] = fac[i - 1] * i % mod;
		invfac[i] = invfac[i - 1] * inv[i] % mod;
	}
} 

void read (int &x)
{
	x = 0;char c = getchar();int f = 1;
	while (c < '0' || c > '9'){if (c == '-') f = -f;c = getchar();}
	while (c >= '0' && c <= '9'){x = (x << 3) + (x << 1) + c - '0';c = getchar();}
	x *= f;return ;
}

void write (int x)
{
	if (x < 0){x = -x;putchar ('-');}
	if (x > 9) write (x / 10);
	putchar (x % 10 + '0');
}

signed main()
{
	//freopen ("wait.in","r",stdin);
	//freopen ("wait.out","w",stdout);
	read (n),read (m);
	init();
	int tot = 0;
	for (Int i = 0;i <= n;++ i)
		for (Int j = 0;j <= m;++ j)
		{
			if (!i && !j) continue;
			int a = i * m + j * n - i * j;
			int now = C (n,i) % mod * C (m,j) % mod * f[a] % mod;
			if ((i + j + 1) % 2 == 0) tot = (tot + now) % mod;
			else tot = (tot % mod + mod - now % mod) % mod;
 		}
 	write ((tot % mod + mod) % mod),putchar ('\n');
 	return 0;
}

爆零考试后一场T2game

题目描述

题目描述:
双方进行游戏,有两个正整数a,b
若a<=b
双方轮流进行选择下列两种操作之一
1)将b对a取模,即b变为b%a
2)从b中减去a的幂,不能减成负数,即b变为b-ak(b-ak>=0且k为正整数)
若a>b,类似
若a,b中之一为0,则无法进行,无法进行者败
输入初始a,b,判断先后手谁有必胜策略

思路

我们先来想一下30分的做法,我们可以用一个递归程序,去判断对于每个 ( a , b ) (a,b) (a,b)操作它的人是否能赢,设f数组为是否可行,则 f [ i ] [ j ] = m a x ( f [ i ] [ j m o d &ThinSpace;&ThinSpace;   i ] , ∑ k = 1 i k &lt; = j d p [ i ] [ j − i k ] ) f[i][j]=max(f[i][j\mod\ i],\sum_{k=1}^{i^k&lt;=j}dp[i][j-i^k]) f[i][j]=max(f[i][jmod i],k=1ik<=jdp[i][jik]),实际上我们这个东西用递归实现之后我们加上记忆化递归,就可以实现 O ( n ) O(n) O(n)的算法时间复杂度,但是对于我们题目中 a , b &lt; = 1 0 18 a,b&lt;=10^{18} a,b<=1018还远远不能满足,我们可以设 j = j m o d &ThinSpace;&ThinSpace; i + k ∗ i j=j\mod i + k*i j=jmodi+ki,这里的k与题目描述中的不是同一个东西,根据WK巨佬所发现的规律,我们发现当k模2余1和k跟i相等的时候 f [ i ] [ j ] f[i][j] f[i][j]是可行的,为什么呢?大家可以输出一下,找一下规律,就可以发现了。我们还是略微理论证明一下吧,我们考虑a是奇数的时候,我们选了之后给对方,对方再一搞,自己的奇偶性没变,所以不论自己怎么搞,都是对方先嗝屁掉。我们再来证明为什么 d p [ i ] [ j − i k ] dp[i][j - i^k] dp[i][jik] i + 1 i+1 i+1一个循环,我们考虑不正面刚因为刚不出来用侧面证明——数学归纳法,我们可以先假设在已知的段数都满足,那么我们可以尝试证明 d p [ i ] [ j − i p ] dp[i][j-i^{p}] dp[i][jip]可以推到 d p [ i ] [ j − i p − ( i + 1 ) ] dp[i][j-i^{p-(i+1)}] dp[i][jip(i+1)],然后读者们可以尝试自己证明一下因为本蒟蒻实在不会了,然后我们还可以发现当 k = i k=i k=i的时候 d p [ i ] [ j ] dp[i][j] dp[i][j]也是可行的,但我实在不知道怎么证明了,就你们去证明吧,相信你们!

代码

#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;

#define Int register int
#define int long long

bool pd (int a,int b)
{
	if (!a) return 0;
	if (!pd (b % a,a)) return 1;
	int k = ((int)b / a - 1) % (a + 1);
	if (k % 2 || k == a) return 1;
	else return 0;
}

void read (int &x)
{
	x = 0;char c = getchar();int f = 1;
	while (c < '0' || c > '9'){if (c == '-') f = -f;c = getchar();}
	while (c >= '0' && c <= '9'){x = (x << 3) + (x << 1) + c - '0';c = getchar();}
	x *= f;return ;
}

void write (int x)
{
	if (x < 0){x = -x;putchar ('-');}
	if (x > 9) write (x / 10);
	putchar (x % 10 + '0');
}

signed main()
{
	//freopen ("game.in","r",stdin);
	//freopen ("game.out","w",stdout);
	int times;
	read (times);
	while (times --)
	{
		int a,b;
		read (a),read (b);
		if (a > b) swap (a,b);
		if (pd (a,b)) puts ("First");
		else puts ("Second");
	}
 	return 0;
}

爆零考试后一场T3R

题目描述

题目描述
有两个1~n的排列A,B,序列C一开始为空,每次可以选择进行以下两种操作之一
1)若A不为空,则可取出A的开头元素放在序列C的末尾
2)若B不为空,则可取出B的开头元素放在序列C的末尾
这样当A,B皆为空时,C称为排列A,B的合并,其长度为2*n
记F(A,B)为A,B的所有可能合并的总数
求对于所有可能的1~n的排列A,B,F(A,B)的和,mod 998244353

思路

还是先讲一下20分的做法吧,我们可以打一个全排,然后 O ( n ! 2 ) O(n!^2) O(n!2)暴力统计,20分。考虑40分接近40分。我们实际上有一个循环是可以省略的,我们可以固定一个数组,因为都是一样的。考虑满分做法,我们设 d p [ i ] [ j ] [ k ] dp[i][j][k] dp[i][j][k]表示现在A数组到了i位,B数组到了j位,前面一共有k个重复的数出现,这个状态储存的是情况总和,我们可以非常轻松地列出dp式: d p [ i ] [ j ] [ k ] = d p [ i − 1 ] [ j ] [ k − 1 ] ∗ ( j − k + 1 ) + d p [ i ] [ j − 1 ] [ k − 1 ] ∗ ( i − k + 1 ) + d p [ i − 1 ] [ j ] [ k ] ∗ ( n − ( i + j − k − 1 ) ) + d p [ i ] [ j − 1 ] [ k ] ∗ ( n − ( i + j − k − 1 ) ) − ∑ i = 1 k d p [ i − d ] [ j − d ] [ k − d ] ∗ ( n − ( i + j − k − d ) d ) ∗ g ( d ) ∗ d ! dp[i][j][k]=dp[i-1][j][k-1]*(j-k+1)+dp[i][j-1][k-1]*(i-k+1)+dp[i-1][j][k]*(n-(i+j-k-1))+dp[i][j-1][k]*(n-(i+j-k-1))-\sum_{i=1}^{k}dp[i-d][j-d][k-d]*{{n-(i+j-k-d)} \choose d} * g(d) * d! dp[i][j][k]=dp[i1][j][k1](jk+1)+dp[i][j1][k1](ik+1)+dp[i1][j][k](n(i+jk1))+dp[i][j1][k](n(i+jk1))i=1kdp[id][jd][kd](dn(i+jkd))g(d)d!,这个式子表示A数组选了i-1个数,B数组选了j个,有k-1个重复的,你现在从B数组中原本不重复的数中选一个数放到i位,就是 d p [ i − 1 ] [ j ] [ k − 1 ] ∗ ( j − k + 1 ) dp[i-1][j][k-1]*(j-k+1) dp[i1][j][k1](jk+1), j − k + 1 j-k+1 jk+1是不重复的个数,然后后一个转移也差不多,然后再后面一个转移就是说A数组中i-1个数,B数组中j个,有k个重复的,你就只能选一个已经重复出现的,就是有 n − ( i + j − k − 1 ) n-(i+j-k-1) n(i+jk1)个已经重复的,然后再后面一个转移也差不多,然后再后面一个 ∑ \sum 就是说从k个重复的数中我们选d个出来,为什么呢?因为我们前面在算的时候是把它算重了,它有 d p [ i − d ] [ j − d ] [ k − d ] dp[i-d][j-d][k-d] dp[id][jd][kd]的原本资本,就是说把i、j、k中的d都不看了,我们知道这d个数无论怎么排,效果都是一样的,所以还要乘上一个 d ! d! d!,我们除了这d个,还有 ( i − d ) + ( j − d ) − ( k − d ) (i-d)+(j-d)-(k-d) (id)+(jd)(kd)个数互不相同,我们从n中减去它们所组成的集合中中选出d个数,所以这就是那个组合数的含义。那一个关键问题来了,我们怎么算g(x)呢?g(x)又是什么意思呢?g(x)表示的就是现在有一个长度为2*x的串有多少个"好数",那"好数"又是什么呢?我们定义一个数组为好数,当且仅当它每个数出现了两次,且它的任意前缀不能是一个好数,不然的话我们就会算重,然后就需要用容斥原理这种又慢又烦人的算法,知道了它的定义那又怎么算呢?我们实际上可以用卡塔兰数,我们可以想象存在一个二维地图,你要从左下走到右上,不能超过中间的斜线的走法有多少种,我们把选a中的数想象成往右走,把选b中的数想象成往上走,然后不能选一个超过两个,否则就会重复,你会发现这似乎与卡塔兰数的百度中的图很像,实际上我们就可以这么算,但是要注意我们完全不能碰到,所以卡塔兰数的话你的自变量需要减一再带进去。然后这道题我能说的就到这里了,其余如果你有疑问也不在我的能力范围以内,所以请自行理解,如有不便请拨打你妈妈的电话寻求心理安慰,对此我不感任何抱歉

代码

#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;

#define Int register int
#define int long long
#define mod 998244353
#define MAXN 105

int n;
int fac[MAXN << 1],invfac[MAXN << 1],dp[MAXN][MAXN][MAXN];

int quike_pow (int a,int b,int c)
{
	int res = 1;
	while (b)
	{
		if (b & 1)
			res = res * a % c;
		a = a * a % c;
		b >>= 1;
	}
	return res % c;
}

void init()
{
	fac[0] = 1;invfac[0] = 1;
	for (Int i = 1;i <= (n * 2);++ i)
	{ 
		fac[i] = fac[i - 1] * i % mod; 
		invfac[i] = quike_pow (fac[i],mod - 2,mod);
	} 
}

int C(int n,int m)
{
	return fac[n] * invfac[m] % mod * invfac[n - m] % mod;
}

int Catalan (int x)
{
	if (x == 0) return 1;
	else return C (x << 1,x) - C (x << 1,x + 1);
}

int g(int x)
{
	return Catalan (x - 1);
}

void read (int &x)
{
	x = 0;char c = getchar();int f = 1;
	while (c < '0' || c > '9'){if (c == '-') f = -f;c = getchar();}
	while (c >= '0' && c <= '9'){x = (x << 3) + (x << 1) + c - '0';c = getchar();}
	x *= f;return ;
}

void write (int x)
{
	if (x < 0){x = -x;putchar ('-');}
	if (x > 9) write (x / 10);
	putchar (x % 10 + '0');
}

signed main()
{
	//freopen ("R.in","r",stdin);
	//freopen ("R.out","w",stdout);
	read (n);
	init();
	dp[0][0][0] = 1;
	for (Int i = 0;i <= n;++ i)
		for (Int j = 0;j <= n;++ j)
			if (i || j)
			{
				for (Int k = max(0ll,i + j - n);k <= i && k <= j;++ k)
				{
					int &s = dp[i][j][k];
					if (i && k) s = (s + dp[i - 1][j][k - 1] * (j - k + 1) % mod) % mod;
					if (j && k) s = (s + dp[i][j - 1][k - 1] * (i - k + 1) % mod) % mod;
					if (i) s = (s + dp[i - 1][j][k] * (n - (i + j - k - 1)) % mod) % mod;
					if (j) s = (s + dp[i][j - 1][k] * (n - (i + j - k - 1)) % mod) % mod;
					for (Int d = 1;d <= k;++ d)
						s = (s + mod - dp[i - d][j - d][k - d] * C (n - (i + j - k - d),d) % mod * g(d) % mod * fac[d] % mod) % mod;
				}	
			}
	write (dp[n][n][n]),putchar ('\n');
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Go语言(也称为Golang)是由Google开发的一种静态强类型、编译型的编程语言。它旨在成为一门简单、高效、安全和并发的编程语言,特别适用于构建高性能的服务器和分布式系统。以下是Go语言的一些主要特点和优势: 简洁性:Go语言的语法简单直观,易于学习和使用。它避免了复杂的语法特性,如继承、重载等,转而采用组合和接口来实现代码的复用和扩展。 高性能:Go语言具有出色的性能,可以媲美C和C++。它使用静态类型系统和编译型语言的优势,能够生成高效的机器码。 并发性:Go语言内置了对并发的支持,通过轻量级的goroutine和channel机制,可以轻松实现并发编程。这使得Go语言在构建高性能的服务器和分布式系统时具有天然的优势。 安全性:Go语言具有强大的类型系统和内存管理机制,能够减少运行时错误和内存泄漏等问题。它还支持编译时检查,可以在编译阶段就发现潜在的问题。 标准库:Go语言的标准库非常丰富,包含了大量的实用功能和工具,如网络编程、文件操作、加密解密等。这使得开发者可以更加专注于业务逻辑的实现,而无需花费太多时间在底层功能的实现上。 跨平台:Go语言支持多种操作系统和平台,包括Windows、Linux、macOS等。它使用统一的构建系统(如Go Modules),可以轻松地跨平台编译和运行代码。 开源和社区支持:Go语言是开源的,具有庞大的社区支持和丰富的资源。开发者可以通过社区获取帮助、分享经验和学习资料。 总之,Go语言是一种简单、高效、安全、并发的编程语言,特别适用于构建高性能的服务器和分布式系统。如果你正在寻找一种易于学习和使用的编程语言,并且需要处理大量的并发请求和数据,那么Go语言可能是一个不错的选择。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值