Codeforces 做题日记Round 957 (Div. 3)-G.Ultra-Meow

CF偶遇Ultra-Meow,阶乘爆炸强如怪物,拼尽全力无法战胜

看完答案之后豁然开朗,但是答案的代码比较抽象,我试着用形象的说法来讲解一下思路

刻下的话语,浮现心头:将我的思路,弃置于此

问题描述:

将题目翻译一下,意思大致是:定义一个运算MEX(),再给你一个数,比如4,对应的集合就是{1,2,3,4},5就是1~5,以此类推,要求出这个集合所有子集的MEX,我试着用比较形象的方式来描述一下个MEX运算:

假设这个表格是全集1~10:X表示该位置对应的数字存在集合中

XXXXXXXXXX

那么给的集合A是这样的:元素个数为N

XXXX

进行的MEX运算就是,再给你N+1个O来从左往右填剩下的表格,最后能到达的位置就是你的输出:

可以看到,O最后到达了第九个格子,对应的数字就是9

XOXOXXOOO

所以MEX(A)的输出就是9

特别注意:最后输出的是O所能到达的位置,跟X最远在哪里无关;X是不能超出给定集合的范围的(因为X是集合子集的元素),但是O可以,因为那是给你拿去填的东西;同样,X和X之间可以有空隙,但是O和X,O和O之间不能有空隙

这种情况是允许的:

XOOOXOOXX

这样是不可以的:

XOOXOOXOXO

最简单的描述就是:给你N个固定的X和N+1个O,O在不留空的情况下最远能填到多少

如果还是理解不了,可以继续往下看,有具体例子应该会更好看懂

这样就很好理解了,那么如何解决这个问题呢:

首先,输入的是一个数字N,代表集合S的大小,集合元素规定是从1,2,3,4....N,但我们要求的不是MEX(S),而是S所有子集的MEX的和,所以我们要从如何枚举所有S的子集入手

但是我们并不需要知道每个子集都是什么,原因如下:

继续按照我们填格子的思路,对集合S={1,2,3,4,5,6,7,8,9,10}的子集A={1,3,4,5,7,}和子集B={2,3,5,7,8}来说情况是一样的,其实和最简单的五个元素的子集C={1,2,3,4,5}都是一样的

XOXXXOXOOOO
OXXOXOXXOOO
XXXXXOOOOOO

可以看到,我们最后都到达了11,这就引申出两种情况了 :即子集的大小是否达到了给定集合大小的一半,如果达到了一半及以上,那么很容易得到:X不管在格子里如何排列,O都能填上它们之间的空隙,并再往后面填,所以,O最远能到达的位置一定是N+N+1,举个例子:输入为10时,对它所有大小在5及以上的子集来说,比如大小为5的子集,都有5个X和6个O能填,那么不管X在前十个格子里如何排列,O一定都能把十个格子填满再往后填一个,所以对于所有大小在5以上的子集来说,MEX的输出都是11

那么一共有多少大小在5以上的子集呢?

这里就要用到组合数,C(N,I),即在N个数里选I个一共有几种方式,那么易得大小为5的子集数量是:C(10,5)                 (具体的计算代码在代码部分)

基于这样的思路,很容易想到要从子集大小入手来遍历所有的子集,那么大小大于输入一半的子集很好计算,小于输入一半的子集就有点复杂了:我们来看这几种情况

比如,输入为10时,大小为3的子集

XOOXOOX

XOOOOXX

OOOOXXX

XXXOOOO

这四个例子中,MEX的输出分别为:6,5,4,7(要记得,我们需要的是O最后到达的位置,而不是X)这里就来到整个算法最绕的地方了,只要能理解到,整个代码的难度就只剩下对于组合数计算时阶乘太大的问题了

我们可以先忘记X和O的区别,我们只看X和O的数量,可知总数量对于每个集合来说都是确定的2*N+1,我们最后要的是一个由X和O串起来的链子(大家可以想象一下),且链子的最后一个一定是O,我们要的就是这个链子的长度(不一定每个X都被串在了链子里

最简单,也是最理想的情况

XXXOOOO

同样,这个情况和上面是完全一致的:

XOXOXOO

也就是说,不关链子内部被如何打乱,只要串进去的X数目一样,最后的结果都是一样的(下面同理)(在链子末端的X视为没有串进去,因为链子的结尾必须是O)

O到达了最远的7

那如果有一个X跑到了7之外呢(也就是没有被串在链子里,在这种情况里因为元素太少,X还有空位可以待,如果是上面讨论的大于一半情况那么X无论待在哪里都会被串进去)

XXOOOOX

我们的O最远就只能到6了

以此类推如果有两个X跑到了外面。。。

XOOOOXX

最后的输出就只有5

所以对于这个部分我们可以这样来计算,我们假设最后串起来的链子长度为K,而子集的大小为I,输入的全集大小为N

那么我们要遍历有没有一个X在外面的情况,一个X在外面的情况,两个X在外面的情况.....所有X都在外面的情况,那么用for循环就可以达成K从I+1到2*I+1的遍历(K=I+1对应所有X都在外面的情况,K=2*I+1对应所有X都被串进去的情况)

那么,胜利就在眼前了,对于一个大小为I的子集,链子的长度为K(也就是我们要的MEX输出),那么怎么计算一共有几种排列方式呢,从给定的数据可以看出,串进链子的X个数应该是链子长度-O的个数,即K-(I+1);在链子外面的X个数应该是子集大小-链子中X的个数,即2*I-K-1,那么我们就是要在链子中选K-(I+1)个X,在链子外选2*I-K-1个X,即C(K-1,K-(I+1))和C(N-I,2*I+1-K)

特别注意第一个C(K-1,K-(I+1)),应该是K-1而不是K的原因是在链子中选X的时候不可以选最后一个位置,因为那里必须是O,所以其实是在K-1个位置里选(我自己写代码的时候就忽略了这一点所以一直无法通过测试,看了一个上午才发现这里的错误)

至此,整个算法的大体思路就结束了,下面是代码实现和一些细节上的东西

1.组合数计算时涉及阶乘的运算,在测试例4999上会超出int的范围

其实题目的下面有一行字来提示我们要取模运算,但我做题的时候忽略了,加上我之前也没有了解过取模运算这种方式

答案中给出了取模运算以及之后进行阶乘,组合数运算的代码,还是相当好理解的

const int mod = 1e9 + 7;

int add(int a, int b) {
    if (a + b >= mod)
        return a + b - mod;
    return a + b;
}

int sub(int a, int b) {
    if (a < b)
        return a + mod - b;
    return a - b;
}

int mul(int a, int b) {
    return (int)((1ll * a * b) % mod);
}

int binpow(int a, int pw) {
    int b = 1;
    while (pw) {
        if (pw & 1)
            b = mul(b, a);
        a = mul(a, a);
        pw >>= 1;
    }
    return b;
}

int inv(int a) {
    return binpow(a, mod - 2);
}

const int N = 15000;
int f[N], r[N];

void precalc() {
    f[0] = 1;
    for (int i = 1; i < N; i++)
        f[i] = mul(f[i - 1], i);
    r[N - 1] = inv(f[N - 1]);
    for (int i = N - 2; i > -1; i--)
        r[i] = mul(r[i + 1], i + 1);
}

int C(int n, int k) {
    if (n < 0 || k < 0 || n < k)
        return 0;
    return mul(f[n], mul(r[k], r[n - k]));
}

取模运算可以在保留数字的一些数学特性的前提下,让数据不超过某一个特定的范围,在本题的计算中主要用于计算阶乘和组合数,并不会影响最后计算得到的数值的绝对大小(因为我们需要的只是排列的个数,阶乘的数值经过取模之后不会影响我们计算得到的组合数大小)(这一块我也不是很了解,甚至可能现在说的也不一定对,数学功底不太好是这样的)

答案中的代码还运用了预处理的方式来减少运行时间,不然每次都重新进行阶乘的计算会相当费时

下面附上我写的代码(其实跟照抄标准答案没什么区别了,自己写的时候没写取模运算的部分,直接复制的标准答案的取模代码,后面的内容也都大同小异)

#include<iostream>

using namespace std;

const int mod = 1e9 + 7;

int mul(int a, int b) {
	return (int)((1ll * a * b) % mod);
}

int binpow(int a, int pw) {
	int b = 1;
	while (pw) {
		if (pw & 1)
			b = mul(b, a);
		a = mul(a, a);
		pw >>= 1;
	}
	return b;
}

int inv(int a) {
	return binpow(a, mod - 2);
}


const int N = 15000;
int f[N], r[N];

void precalc() {
	f[0] = 1;
	for (int i = 1; i < N; i++)
		f[i] = mul(f[i - 1], i);
	r[N - 1] = inv(f[N - 1]);
	for (int i = N - 2; i > -1; i--)
		r[i] = mul(r[i + 1], i + 1);
}

int C(int n, int k) {
	if (n < 0 || k < 0 || n < k)
		return 0;
	return mul(f[n], mul(r[k], r[n - k]));
}

int add(int a, int b) {
	if (a + b >= mod)
		return a + b - mod;
	return a + b;
}

int MEOW(int n)
{
	int sum = 1;
	for (int i = 1; i <= n; i++)
	{
		if (2 * i >= n)
		{
			sum=add(sum,mul((2 * i + 1), C(n, i)));
		}
		else
		{
			for (int k = i+1; k <= 2*i+1; k++)
			{
				sum =add(sum, mul(k , mul(C(k-1,k-i-1), C(n-k, 2*i+1-k))));
			}
		}
	}

	return sum;
}

int main()
{
	precalc();
	int m = 0;
	cin >> m;
	for (int i = 0; i < m; i++)
	{
		int n = 0;
		cin >> n;

		cout << MEOW(n) << endl;
	}

	return 0;
}

  • 27
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
在Educational Codeforces Round 146 (Rated for Div. 2)比赛中,有关于一个节点数量为n的问。根据引用的结论,无论节点数量是多少,都可以将其分成若干个大小为2或3的区间使其错排,从而减少花费。根据引用的推导,我们可以通过组合数和差量来表示剩余的花费。而引用进一步推广了这个结论,可以使得任意长度的一段错排花费代价为边权和的两倍,并且通过贪心算法使得更多的边不被使用。以此来解决与节点数量相关的问。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [Educational Codeforces Round 146 (Rated for Div. 2)(B,E详解)](https://blog.csdn.net/TT6899911/article/details/130044099)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* [Educational Codeforces Round 83 (Rated for Div. 2) D](https://download.csdn.net/download/weixin_38562130/14878888)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值