《div3数论训练》题解

题单链接:div3数论训练 - Virtual Judge

1. 有理数取余

题目链接:

有理数取余 - 洛谷 P2613 - Virtual Judge

题意:

给定一个有理数 c=\frac{a}{b},需要求解 c \mod 19260817 的值。换句话说,求 bx \equiv a(\mathrm{mod} 19260817) 的解。如果无解,输出 "Angry!"。

解题思路:

本体为快速幂模板题。首先,将输入的有理数 a 和 b 以字符串形式读取,逐位转化为整数,并对 19260817 取模。对于非常大的数字,这样可以避免直接处理超大数带来的问题。接下来,判断 b 是否是 19260817 的倍数,如果是,则输出 "Angry!",因为在这种情况下无法找到解。若 b 不是倍数,则通过费马小定理计算 b 在模 19260817 下的模逆元,使用快速幂方法 ksm(b, MOD-2, MOD) 进行计算。最后,计算 a \cdot b^{-1} \mod 19260817,并输出结果。

时间复杂度:转换字符串为整数并对 19260817 取模的时间复杂度为 O(n),其中 n 为数字的位数。计算模逆元时使用快速幂,时间复杂度为 O(\log MOD),其中 MOD=19260817。因此,总体时间复杂度为 O(n + \log MOD).

AC代码:

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

#define si(x) ((int)(x).size())
#define MOD 19260817

ll ksm(ll a, ll b, ll p) {
	a = a % p;
	ll res = 1;
	while (b) {
		if (b & 1) {
			res = res * a % p;
		}
		a = a * a % p;
		b >>= 1;
	}
	return res;
}

void solve() {
	string sa, sb;
	cin >> sa >> sb;
	ll a = sa[0] - '0', b = sb[0] - '0';
	for (int i = 1; i < si(sa); i++) {
		a = (a * 10 + sa[i] - '0') % MOD;
	}
	for (int i = 1; i < si(sb); i++) {
		b = (b * 10 + sb[i] - '0') % MOD;
	}

	if (b % MOD == 0) {
		cout << "Angry!" << endl;
		return;
	} else {
		ll c = (a * ksm(b, MOD - 2, MOD)) % MOD;
		cout << c << endl;
		return;
	}
	return;
}

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

2. Minimal Coprime

题目链接:

Minimal Coprime - CodeForces 2063A - Virtual Judge

题意:

给定一个区间 [l,r],需要找出该区间内的所有最小互质子区间。一个区间 [l',r'] 被称为最小互质子区间,当且仅当它是互质的,并且区间内没有其他互质的子区间。两个数 ab 是互质的,当且仅当它们的最大公约数为1。

对于每个测试用例,给定一个区间 [l,r],要求输出该区间内最小互质子区间的数量。

解题思路:

首先,最小互质子区间必然形如 [1,1] 或 [x,x+1](x\geqslant 2),因此,对于区间 [l,r] 

  • l=r=1 ,则包含 [1,1] ,答案为 1 ;

  • 否则 r\geqslant 2。对于所有的 l\leqslant x< r ,恰存在1个使得 [x,*] 是最小互质子区间,并且包含在 [l,r] 中。

    • 换句话说,在这种情况下,答案是 (y-1)-x+1=y-x 。

时间复杂度:​​O(1).

AC代码:

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

void solve() {
	int l, r;
	cin >> l >> r;
	cout << (r - l + (l == 1 && r == 1)) << endl;
	return;
}

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

3. 素数密度

题目链接:

 素数密度 - 洛谷 P1835 - Virtual Judge

题意:

给定一个区间 [L,R],计算该区间内的素数个数。

解题思路:

首先,使用筛法计算所有小于等于 50000 的素数。因为在区间 [L,R] 中,每个大于 50000 的数最多只能被小于等于 50000 的素数整除(最大可能数据值开根后的向上估计值),所以只需要预先计算这些素数。然后,对于每个测试用例,给定一个区间 [L,R],并且如果 L=1,则将其调整为 L=2(因为1不是素数)。接着,创建一个标记数组 vis 来标记区间内哪些数是合数。对于每个素数,标记其在区间内的倍数为合数,确保每个区间中的合数被标记。最后,计算区间内没有被标记的数的数量,即为素数的个数。

处理标记数组的方法是将 [L,R] 整体平移至 [0,R-L],这种做法可以避免MLE。

时间复杂度:

  • 筛法计算小于 50000 的素数时间复杂度是 O(n \log \log n),其中 n=50000
  • 对于每个测试用例,标记区间内的合数时,需要对每个素数在区间 [L,R] 上的倍数进行标记,复杂度为 O(\frac{R - L}{p})(其中 p 是当前的素数)。综合考虑,标记所有素数倍数的时间复杂度是 O((R - L + 1) \log \log n),其中 n=50000
  • 总体时间复杂度大致为 O((R - L + 1) \cdot \log \log n).

AC代码:

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

#define si(x) ((int)(x).size())
#define int ll

int prime[50'001];
bool is_prime[50'001];
int sieve(int n) {
	int p = 0;
	for (int i = 0; i <= n; i++) {
		is_prime[i] = true;
	}
	is_prime[0] = is_prime[1] = false;
	for (int i = 2; i <= n; i++) {
		if (is_prime[i]) {
			prime[p++] = i;
			for (int j = i + i; j <= n; j += i) {
				is_prime[j] = false;
			}
		}
	}
	return p;
}

void solve() {
	int n = sieve(50'000);
	int l, r;
	cin >> l >> r;
	l += (l == 1);
	int flag = 1;
	ll ans = 0;
	vector<int> vis(r - l + 1, 0);
	for (int i = 0; i < n; i++) {
		ll p = prime[i], start = max(2LL, (l + p - 1) / p) * p;
		for (int j = start; j <= r; j += p) {
			vis[j - l] = 1;
		}
	}
	for (int i = 0; i <= r - l; i++) {
		if (!vis[i]) {
			ans++;
		}
	}
	cout << ans << endl;
	return;
}

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

4. 最大公约数和最小公倍数问题

题目链接:

最大公约数和最小公倍数问题 - 洛谷 P1029 - Virtual Judge

题意:

给定两个正整数 x_0​ 和 y_0,求同时满足 \gcd(P, Q) = x_0 和 \text{lcm}(P, Q) = y_0 的正整数对 (P,Q) 的个数。

解题思路:

如果 y_0 不是 x_0 的倍数,答案显然为0,否则,对于 \frac{y_0}{x_0} 的每个质因子,它的次幂只能唯一地放在P,Q 中的一个(相同的质因数只能全是 P 的因数或 Q 的因数),于是只需计算出 \frac{y_0}{x_0} 有多少个不同质因子,答案即为2的多少次幂。

时间复杂度:O(\sqrt{10^5}).

AC代码:

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

#define int long long

void solve() {
	int x, y;
	cin >> x >> y;
	if (y % x != 0) {
		cout << 0 << endl;
		return;
	}
	int d = y / x;
	int cnt = 0;
	for (int i = 2; i <= d; i++) {
		if (d % i == 0) {
			cnt++;
			while (d % i == 0) {
				d /= i;
			}
		}
	}
	cout << (1 << cnt) << endl;
	return;
}

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

5. Longest Subsequence

题目链接:

Longest Subsequence - CodeForces 632D - Virtual Judge

题意:

给定一个包含 n 个整数的数组 a 和一个整数 m,要求从数组中选出一个最长的子序列,使得该子序列的所有元素的最小公倍数(LCM)不超过 m。需要输出该子序列的最小公倍数 lll 和最长子序列的长度,以及该子序列中元素的位置。

解题思路:

通过统计每个数的倍数出现次数,然后找到倍数出现次数最多的数 l,并输出所有能够整除 l 的元素。最终,答案就是这些能够整除 l 的数。

时间复杂度:

  • 填充 f 数组的时间复杂度为 O(n).

  • 更新 f 数组的时间复杂度为 m+\frac{m}{2}+\frac{m}{3}+\cdots +\frac{m}{m}=O(m\log m).

  • 总体时间复杂度:O(n+m\log m).

AC代码:

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

#define all(v) v.begin(), v.end()

void solve() {
	int n, m;
	cin >> n >> m;
	vector<int> a(n), f(m + 1);
	for (int i = 0; i < n; i++) {
		cin >> a[i];
		if (a[i] <= m) {
			f[a[i]]++;
		}
	}

	for (int i = m; i >= 1; i--) {
		for (int j = 2 * i; j <= m; j += i) {
			f[j] += f[i];
		}
	}

	int l = max_element(all(f)) - f.begin();
	if (l == 0) {
		cout << 1 << " " << 0 << endl;
		return;
	}
	int k = f[l];
	cout << l << " " << k << endl;
	for (int i = 0; i < n; i++) {
		if (l % a[i] == 0) {
			cout << i + 1 << " ";
		}
	}
	cout << endl;
	return;
}

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

6. Common Generator

题目链接:

Common Generator - CodeForces 2029E - Virtual Judge

题意:

给定一个数组 a,找出一个整数 x\geqslant 2,使得对于数组中的每个元素 a_i​,都存在一个操作可以将 x 转换为 a_i(操作是选择一个因数 d\geqslant 2,将 x 增加 d)。如果这样的 x 存在,输出它;否则输出 -1。

解题思路:

  • 对所有数字进行筛选(最小质因子筛),生成最小质因子数组 minp

  • 查找第一个可能的生成数 x,并验证它是否能生成所有数组元素。

  • 若找不到满足条件的 x,输出 -1。

时间复杂度:​

  • 最小质因子筛:O(n \log \log n) 用于构建质因子数组。

  • 对每个测试用例的处理:检查每个元素是否能由 x 转换,最坏情况下是 O(n)

由于每个测试用例的处理是 O(n),所以对于多次测试,整体复杂度大约是 O(n \log \log n).

AC代码:

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

#define IPSB cout << -1 << endl; return

vector<int> minp, primes;

void sieve(int n) {
	minp.assign(n + 1, 0);
	primes.clear();

	for (int i = 2; i <= n; i++) {
		if (minp[i] == 0) {
			minp[i] = i;
			primes.push_back(i);
		}

		for (auto p : primes) {
			if (i * p > n) {
				break;
			}
			minp[i * p] = p;
			if (p == minp[i]) {
				break;
			}
		}
	}
}

bool check(int x, int a) {
	if (a % x == 0) {
		return true;
	}
	if (a < 2 * x) {
		return false;
	}
	if (a % 2 == 0) {
		return true;
	}
	return a - minp[a] >= 2 * x;
}

void solve() {
	int n;
	cin >> n;
	vector<int> a(n);
	for (int i = 0; i < n; i++) {
		cin >> a[i];
	}

	int cur = 0;
	while (cur < n && (minp[a[cur]] != a[cur] || a[cur] == 2)) {
		cur++;
	}

	if (cur == n) {
		cout << 2 << endl;
		return;
	}

	int x = a[cur];

	for (int i = 0; i < n; i++) {
		if (!check(x, a[i])) {
			IPSB;
			return;
		}
	}
	cout << x << endl;
}

signed main() {
	int ttest = 1;
	cin >> ttest;
	sieve(400'000);
	while (ttest--) {
		solve();
	}
	return 0;
}

学习总结:

在《div3数论训练》的学习过程中,我通过六个典型问题的解决,深刻理解了数论中的一些重要概念和技巧。这些问题不仅锻炼了我的算法思维,也让我对数论的应用有了更深的理解。

通过“最大公约数与最小公倍数问题”,我学到了如何利用数的约束条件来构造可能的解,并理解了如何通过枚举和数的因数分解来快速找到满足条件的 P,QP, QP,Q。在这个过程中,我进一步加深了对数的因数和倍数关系的理解,特别是在处理大数时,如何高效判断和处理这些关系。

“最长子序列问题”让我对最小公倍数(LCM)和最大公约数(GCD)有了更深入的理解。这个问题的核心是要求找到一个子序列,其元素的LCM小于等于给定值。在解决该问题时,我学会了如何优化LCM的计算,避免了暴力解法的低效,并且掌握了在大数据量下如何处理序列问题的技巧。

在“生成数问题”中,我通过质因数分解的应用,学会了如何在给定的范围内找到符合条件的生成数。通过这个问题,我了解到如何利用筛法计算最小质因子(minp)来加速问题的求解,避免了重复计算和冗余判断,从而提升了算法效率。

在“通用生成数问题”的求解过程中,我意识到,虽然理论上可以枚举所有可能的 x,但结合筛法和最小质因子,可以大大减少需要枚举的候选数。通过有效利用质因数分解来判断某个数是否能生成数组中的其他元素,我更加理解了如何在复杂问题中减少计算量,提升效率。

在整个学习过程中,筛法的应用给我带来了极大的帮助。通过在多个问题中使用筛法来计算最小质因子或求解LCM,我更加熟悉了如何高效处理质数相关的问题,尤其是在大规模数据情况下,如何保持算法的高效性。这一技巧让我在解决数论问题时,能够更加从容应对各种挑战。

此外,我在这些问题中逐渐提高了对边界条件和算法优化的敏感度。在处理问题时,我学会了如何合理地利用预处理,避免重复计算,提升了整体算法的执行效率。通过这些实践,我更加理解了如何设计高效的算法,并在实际应用中避免冗余的计算。

总的来说,这段学习经历不仅丰富了我的数论知识,也帮助我提升了分析问题、设计算法和优化代码的能力。这些问题让我在面对类似挑战时,能够更加高效地找到解决方案,也让我对数论的应用有了更深的理解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值