AtCoder - ARC 117 - C(思维+杨辉三角结论+卢卡斯定理/模拟)

C - Tricolor Pyramid

题意:

已知一个金字塔的底层颜色分布,蓝白红,上面格子的颜色取决于下面两个相邻格子的颜色。
如果相邻格子为同色,那么上面那个格子也是这个颜色;如果相邻格子为不同色,那么上面那个格子就是不同于下面两种颜色的第三种颜色。

给你金字塔最顶层的方块数为 N,和底层颜色分布,用字符 B 表示蓝色,字符 W 表示白色,字符R 表示红色。

问:金字塔顶端是什么颜色。

数据范围:

2 ≤ N ≤ 400000

思路:

先发一个官方题解的图:

根据官方的题解,我们将三种颜色看作0,1,2;发现颜色 x 和 y 上面的颜色是 -(x+y)%3。如: -3 mod 3=0,-2 mod 3 = 1,−1 mod 3 = 2,-4 mod 3= 6。

根据这个公式,对于这个金子塔,在顶层就会推出系数为二项式定理的一个式子,在mod 3 的意义下,那个数就是我们要找的答案。即(-1)^{n-1}*\sum _{i = 1}^{n}C_{n-1}^{i-1}*c_{i}

那么究其根本是如何推出 C_{n-1}^{i-1} 式子的呢——

对于金字塔,把从最底层到最顶端的变换看作是一条路线,在每一天路线上,对于每个方块位置,都只有向左或者向右两种走法。通过观察,题目所给的图中,走到顶端需要走 5 步,最底层第一个方块要走到顶端需要左走5次,右走0次;方块2左走1次右走4次;方块3左走2次右走3次;方块4左走3次右走2次;方块5左走4次右走1次;方块6左走5次右走0次。也就是说,第 i 个方块从总共需要走的 n - 1 = 5 步中,任意选 i - 1 步左走。所以 c_{i} 的系数是 C_{n-1}^{i-1}

如何求组合数 C_{n-1}^{i-1}——

我们知道,求组合数要么通过逆元,要么用卢卡斯定理
根据组合数公式 C_{n-1}^{i-1} = \frac{(n-1)!}{(i-1)!*(n-i)!} 。这里是不能直接预处理阶乘和逆元然后求解的。因为根据费马小定理,当a,p互质时,才会有\frac{1}{a} (mod p) ≡ a^{p-2},即 a (mod p) 的逆元为 a^{p-2}

法1(卢卡斯定理):

套板子^_^。

Code:

#include<bits/stdc++.h>
using namespace std;

#define fi first
#define se second
#define int long long
#define endl '\n'
const int dx[] = { 1,-1,0,0 }, dy[] = { 0,0,1,-1 };

const int N = 400010, M = 3010, INF = 0x3f3f3f3f, mod = 3;

typedef pair<int, int>PII;

int n,res;
string s;
int a[N];

//快速幂
int qmi(int a,int k)
{
	int res = 1;
	while(k)
	{
		if(k & 1)res = res * a % mod;
		a = a * a % mod;
		k >>= 1;
	}
	return res;
}

//从定义出发算C(a,b)=a*(a-1)*……*(a-b+1)/1*2*……*b (mod p)
int C(int a,int b)
{
	if(b > a) return 0;
	int res = 1;
	for(int i = 1,j = a; i <= b; i++,j--)
	{
		res = res * j % mod;					         //分子循环乘b次
		res = res * qmi(i,mod - 2) % mod;    			 //分母用快速幂转换为乘b次逆元再(mod p)
	}
	return res;
}

//卢卡斯定理
int lucas(int a,int b)
{
	if(a < mod && b < mod)return C(a,b);
	return C(a % mod, b % mod) * lucas(a / mod, b / mod) % mod;
}

void solve()
{
	cin >> n >> s;
	//将字符用数字0,1,2表示
	for(int i = 0; i < n; i++)
	{
		if(s[i] == 'W') a[i] = 0;
		else if(s[i] == 'R') a[i] = 1;
		else a[i] = 2;
	}
	
	for(int i = 0; i < n; i++)
		res = (res + a[i] * lucas(n - 1, i)) % mod;    //套公式(-1)^(n-1)c[i]*C(n-1,i-1),i从1到n
	
	if(n % 2 == 0)res = -res;                          //如果n为偶数,加负号
	if(res < 0)res += 3;
	
	cout << "WRB"[res] << endl;
}

signed main()
{
    //int t;
    int t = 1;
    //cin >> t;
    while (t--)
    {
        solve();
    }
    return 0;
}

思路2(模拟):

因为分母可能与 3 不互质,所以我们把组合数 C_{n-1}^{i-1} = \frac{(n-1)!}{(i-1)!*(n-i)!} 的分子和分母都处理成 3^{k}*p 的形式。因为组合数一定是整数,所以分子一定大于等于分母。

我们把分子记为 3^{k1}*p1,分母记为 3^{k2}*p2 ,预处理得到的 k1 与 k2 只有 k1 > k2 或者 k1 = k2 两种情况,p1 和p2 只可能是 0,1 或 2 。
1. 如果 k1 > k2 ,说明分子大于分母且值为 3 的倍数,所以该值 mod 3 后结果是 0
2. 如果 k1 = k2 ,分子和分母的 3 全部约去后,值为 p1/p2 ,这时再求该数 mod 3 后的值就可以用费马小定理求逆元了,因为 p1 和 p2 值为 1 或 2 ,根据费马小定理,在 mod 3 时,1 和 2 的逆元为本身(1^(3-2)=1,2^(3-2)=2),结果为 p1*p2%3

根据最后结果输出对应的字母。

实现:
1. 先预处理所有阶乘转换后的 3^{k}*p 的形式中的 k,p 。用 pair 类型的数组 fact 来存,fact[i]={k,p}。k 即 i 最多能除以 3 的个数,p 即 i % 3 。因为是边处理阶乘边求,所以 fact[i] = {fact[i-1].first+cnt,fact[i-1].second*j%3};

2. 预处理 C_{n-1}^{i-1}。根据分子和分母的 k,p 关系,k1 = fact[n].first,k2 = fact[n-k].first + fact[k].first,p1 = fact[n].second,p2 = fact[n-k].second * fact[k].second。
如果 k1 > k2 ,返回 0,如果 k1 = k2 ,返回 p1*p2%3 。

3. 先遍历最后一行算出结果后,再考虑 n 的奇偶性判断是否加负号。(根据官方的表可以看出 n 为偶数时是带负号的,不过在下面的代码中因为 n--,所以是 n 为奇数时带负号。)

4. 代码与公式的求法有些许不同,因为考虑到 C_{n-1}^{i-1} 都有 -1,所以我们可以将输入的 n-- ,输入的字符从标从 0 开始,直接算 c[i] * C_{n}^{i} 即可,原理是一样的。 

Code:

#include<bits/stdc++.h>
using namespace std;

#define fi first
#define se second
#define int long long
const int dx[] = { 1,-1,0,0 }, dy[] = { 0,0,1,-1 };

const int N = 1e6 + 10, M = 3010, INF = 0x3f3f3f3f, mod = 998244353;

typedef pair<int, int>PII;

int n;
char c[N];
PII fact[N];

//预处理i的阶乘转换成的3^k*p的形式中的k,p,分别存入fact[i]中
void init()
{
	fact[0] = {0,1};                                             //0的阶乘等于1,k=0,p=1
	for(int i = 1; i <= 400010; i++)                             //预处理N范围内的阶乘
	{
		int j = i;
		int cnt = 0;
		while(j % 3 == 0)                                        //单说i的话,k=cnt累计得到i最多能整除的3的个数;p等于i%3
		{
			cnt++;
			j/=3;
		}
		fact[i] = {fact[i-1].first+cnt,fact[i-1].second*j%3};    //因为是预处理阶乘的,所以前后有关系的,注意k是累加,p是累乘。
	}
}
//原理是计算C(n-1,i-1)%3的值。代码有所不同(执行n--,且i从0开始),这里的公式是C(n,i)%3
int C(int k)
{
	if(fact[n].first > fact[n-k].first+fact[k].first)            //如果分子的k1大于分母的k2,说明C(n,i)为3的倍数,%3后为0
		return 0;
	return fact[n].second*fact[n-k].second*fact[k].second % 3;   //k1=k2,C(n,i)%3=p1/p2(mod 3) = p1*p2%3
}
void solve()
{
	init();
	
	cin >> n >> c;
	n--;                                          //这里的n--是考虑公式C(n-1,i-1)都有-1,所以预处理下n--,且i从0开始
	int sum = 0;
	map<char, int> mp = {                         //将颜色为数字0,1,2表示(这种写法没见过,拿小本本记下来)
		{'B', 0}, 
		{'W', 1}, 
		{'R', 2}
	};
	for(int i = 0; i <= n; i++)                    //注意因为前面执行n--,所以i从0开始时,到n结束
	{
		sum = (sum + mp[c[i]] * C(i)) % 3;         //套公式c[i]*C(n-1,i-1),这里是c[i]*C(n,i)。
	}
	if(n & 1) sum = -sum;                          //根据n的奇偶性,判断最后是否需要加负号
	if(sum < 0) sum += 3;						   //对sum为负数时再取余,即+3变为正数
	cout << "BWR"[sum] << endl;					   //根据最后的值输出对应字母
}

signed main()
{
	//int t;
	int t = 1;
	//cin >> t;
	while (t--)
	{
		solve();
	}


	return 0;
}

------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
吐槽:很考验思维的题目,也可以说是结论题,注意求组合数的不同方法的适用条件,考虑题不能太死板,不能照搬照对地看选哪一种方法,而是要知其所以然。像法二的思路就是跳出求组合数的模板,遇到哪一点问题就想办法解决这一点问题,一步步得出答案。

题目注意点:
1.首先这个结论,嗯,我没见过,强调一下。

2.注意费马小定理的使用条件,逆元不是任何情况都存在的。

3. "BWR"[sum] 和 map 那边将字符对应为数字的写法可以使得代码更加简洁,我要学习学习。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值