SDNU 2022.10.23 月赛热身赛

本文回顾了快速幂和逆元在ProblemC中的应用,并详细讲解了如何使用线性逆元解决ProblemD中的组合数问题。作者通过实例和代码分享了解题思路,包括求逆元的方法、组合数的正确计算方式,以及如何处理大数据和时间复杂度。最后,还提到了在不同场景下的逆元和组合数计算技巧。
摘要由CSDN通过智能技术生成

目录

Problem C:Find the inverse element

Problem D:Number of combinations

回顾

思路

Problem E:写数字

总结


热身赛,个人觉得像周测,主要是快速幂和逆元,复习一下这周的小数论捏。

A,B签到题略,下面都是没a的。重点是D题。补题辣都A辣!!

因为写着写着突然想起来可能会t所以cin/scanf乱用,见谅。

Problem C:Find the inverse element

纯纯逆元捏,当时没看懂d是个什么东西把他跳了, 我是笨蛋。

#include <bits/stdc++.h>
using namespace std;
long long power(long long a, long long b, long long k) {
	long long ans = 1;
	a = a % k;
	while (b) {
		if (b % 2 == 1)
			ans = (ans * a) % k;
		b /= 2;
		a = (a * a) % k;
	}
	return ans;
}
//快速幂
int main() {
	long long k = 998244353, result, a, b, y;
	int t;
	cin >> t;
	while (t--) {
		cin >> a >> b;
		y = power(b, k - 2, k);//求b的逆元即b^(k-2)
		result = (a % k * y) % k;
		cout << result << "\n";
	}
}

Problem D:Number of combinations

注意:这题题目的a,b要求反了。 

回顾

笑死,本来想来个一血,然后t了一发wa了5发。当时想的太简单了, 前两天刚做了一个单纯求组合数的,两次循环会t,正解是边乘边除,这题直接套了。

思路

这题问题还挺多的,跟他杠了两天才A。总结一下大问题:

1.当被除数被取模后就不可以再除了。因为‘/’是整除,被除数取模后,可能出现被除数小于除数而除后得0的情况,所以我们应该对除数求逆元。

2.这题会卡时间,模拟会t,多次调用快速幂也会t,应该是开两个数组,一个存阶乘,一个存阶乘的逆元。阶乘好说,逆元不能用快速幂了,师哥说要用线性求逆元,开始自学。细说一下。

线性逆元:设要求的是k的逆元,p=ak+b,则ak+b|p,同除bk,得到k的逆元mod p余(-a/b),即求(-a/b),由初始式知a=p整除k,b=p取余k,由于线性,我们已知小于k的所有逆元(这里有一点点递归的意思),即b的逆元已知,为防止出现负数,我们加一个p,于是可得:

for (int i = 2; i <= MAX; ++i) {
		v[i] = (p - (p / i) * v[p % i] % p) % p;
	}    //v[]为逆元被存的数组

线性阶乘逆元:一开始想直接把阶乘数组放到上式里但是数据太大了不行,查了一下求法,很简单的想法捏。要知道逆元,说人话就是谁分之一,我们用一次快速幂求出最大数的逆元,然后依次倒推可得:

	v[MAX] = qpow(f[MAX], k - 2, k);

	for (int i = MAX - 1; i >= 0; --i) {
		v[i] = (v[i + 1] * (i + 1) ) % k;
	}    //1/i=(1/i+1)!*(i+1)    记得取模

 到这里就差不多了捏,最后一点注意,ans连乘的时候每乘一次都要取余一下防止爆ll,笨比本人在这个问题上卡了个把小时。emmm我基本上是把这题所有坑都踩了一遍。

#include <bits/stdc++.h>
using namespace std;
long long MAX = 1000000;
long long f[1000005] = {1};    //阶乘数组
long long v[1000005];          //逆元数组
long long k = 998244353;

long long int qpow(long long int A, long long int B, long long int K ) {
	long long int ans = 1;
	A = A % K;
	while (B) {
		if (B % 2 == 1)
			ans = (ans * A) % K;
		B /= 2;
		A = (A * A) % K;
	}
	return ans;
}
//快速幂

int main() {
	long long int n;
	scanf("%lld", &n);
	for (int i = 1; i <= MAX; ++i) {    //求阶乘
		f[i] = (f[i - 1] * i ) % k;
	}
	v[MAX] = qpow(f[MAX], k - 2, k);    //求MAX的阶乘逆元
	for (int i = MAX - 1; i >= 0; --i) {
		v[i] = (v[i + 1] * (i + 1) ) % k;
	}                                   //倒推出逆元阶乘数组
	v[0] = 0;   
	v[1] = 1;
	while (n--) {
		long long a, b;
		long long ans = 1;
		scanf("%lld %lld", &b, &a);
		if (b == 0 || a == b)            //判断,题目有提示,不加判断会输出0
			ans = 1;
		else
			ans = (f[a] % k * v[b] % k * v[a - b] % k) % k;
                //注意:每乘一次都取余一下。
		cout << ans << '\n';
	}
	return 0;
}

>>2022.11.16补充:

在oj上做题做到了类似的,本想直接cv结果出问题了。SDNUOJ1311,取模数变了变成了10000003,但是这样做出来的结果是错的,为什么?

我们注意到快速幂求逆元的原理是费马小定理,而费马小定理的成立条件是p为素数,即当且仅当取模数为素数的时候我们可以用快速幂求逆元,而10000003并非素数。以下两个解决方案 :

1. 杨辉三角求组合数

递推公式为C(n,m)=C(n-1,m)+C(n-1,m-1)

#include <bits/stdc++.h>    //1311_AC代码
using namespace std;
#define ll long long
const int n = 1000;
const ll mod = 10000003;
ll a[1005][1005];

ll Combination() {
	int i, j;
	a[0][0] = 1;
	for (i = 0; i <= n; i++) {
		a[i][0] = a[i][i] = 1;
		for (j = 1; j < i; j++) {
			a[i][j] = (a[i - 1][j - 1] + a[i - 1][j]) % mod;
		}
	}
	return 0;
}
//预处理

int main() {
	int t;
	int c, k;
	Combination();
	scanf("%d", &t);
	while (t--) {
		scanf("%d%d", &c, &k);
		cout << a[c][k] << '\n';
	}
	return 0;
}

2. 完全线性求逆元,不用快速幂

说来很惭愧,当初师哥给我发了一份他的代码但我没太重视,觉得我这样写也可以,果然是出问题了,师哥还是师哥 

#include<bits/stdc++.h>    //师哥的D题代码
using namespace std;
const int mod=998244353;
const int maxn=1e6+10;
const int maxx=1e6;
int read(){
	int ans=0;bool f=0;char ch=getchar();
	while(ch<'0' or ch>'9'){if(ch=='-')f=1;ch=getchar();}
	while(ch>='0' and ch<='9'){ans=(ans<<1)+(ans<<3)+(ch^48);ch=getchar();}
	return f?~ans+1:ans;
}//快读

long long t,n,m,fac[maxn],inv[maxn],ans;

long long C(int a,int b){
    if(a<b or a<0 or b<0)
    return 0;
    return 1ll*fac[a]*inv[b]%mod*inv[a-b]%mod;
}

int main(){
	fac[0]=fac[1]=inv[0]=inv[1]=1;
	for(int i=2;i<=maxx;++i){
        fac[i]=1ll*fac[i-1]*i%mod;
        inv[i]=1ll*inv[mod%i]*(mod-mod/i)%mod;    //先求出逆元
    }
    for(int i=2;i<=maxx;++i){
        inv[i]=1ll*inv[i]*inv[i-1]%mod;    //再求逆元的阶乘
    }
	t=read();
	while(t){
		t--;
		n=read(),m=read();
		printf("%lld\n",C(m,n));
	}
    return 0;
}

特别地,当数据小但重复性高的时候,可以考虑先线性逆元做预处理防T且线性逆元没有局限性,比快速幂适用范围广时间复杂度还第,虽然一般不会有题卡这个但是多留个心。

这里特别感谢yjgg,每次问他题都能直击重点还会给我拓展一点他的经验, 受益匪浅。(谁会不爱yjgg呜呜QAQ!)

Problem E:写数字

 

第一眼看可能有点懵,但是挺简单的这题。

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

int main() {
	long long int n, num;
	cin >> n >> num;
	long double a = sqrt(n);
	int ge;
	if (a * a != n)
//double并不是精确值我不知道能不能这样写,但是样例+我试的几个完全平方数都过了
//试过了这样写A了
		ge = (long long int)a + 1;
	else
		ge = a;
	long long int hang, lie;
	if (num % ge == 0) {
		hang = num / ge;
		lie = ge;       //行末
	} else {
		hang = num / ge + 1;
		lie = num % ge;
	}
	cout << hang << " " << lie << '\n';
	return 0;
}

总结

因为这篇都是补题,可能有的地方是错的或者有更好的方法,欢迎纠错。

还是对逆元的经典模型和适用范围不够熟练,果然对我来说只是理解远远不够,笨比泪目。

等我再补一补就写周赛的!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值