NOIP2018提高组金牌训练营——数论专题
地址
https://www.51nod.com/live/liveDescription.html#!liveId=23
1187 寻找分数
给出 a,b,c,d, 找一个分数p/q,使得a/b < p/q < c/d,并且q最小。例如:1/3同1/2之间,符合条件且分母最小的分数是2/5。(如果q相同,输出p最小的)
Input
第1行:一个数T,表示后面用作输入测试的数的数量。(1 <= T <= 10000) 第2 - T + 1行:每行4个数,a,b,c,d,中间用空格分隔。(1 <= a,b,c,d <= 10^9)
Output
输出共T行,对应符合条件的分数。
Input示例
4 1 3 1 2 2 1 3 1 2 1 4 1 1000 1001 1001 1002
Output示例
2/5 5/2 3/1 2001/2003
一开始想到二分q,因为q越大显然选择越多(其实是错的)
有了q之后设p=1
可以用lcm把三个式子的分母通分,得到分子
然后可以找在新的分子中存不存在a<p<c
写完后发现过不了样例
然后发现有极少极少的数不满足单调性,大部分都是满足的
但是这样就不能二分了
怎么办??
我直接枚举,非常暴力。
我想着应该会超时
我一交上去
我靠25个点过了22个点
3个点没过是因为WA,最后一个点900多毫秒,卡过去了
其实考试这样的话我就满意了
然后我觉得没过是因为取lcm的时候会爆long long
我就改成了unsigned long long
多过了一个点。
写高精度??
不行,这样就会超时。
所以这就是我这种做法的极限了,25个点能过23个点
#include<bits/stdc++.h> #define REP(i, a, b) for(register int i = (a); i < (b); i++) #define _for(i, a, b) for(register int i = (a); i <= (b); i++) using namespace std; typedef unsigned long long ull; ull a, b, c, d; ull ansp, ansq; ull gcd(ull a, ull b) { return !b ? a : gcd(b, a % b); } ull lcm(ull a, ull b) { return a / gcd(a, b) * b; } bool check(ull q) { ull t = lcm(lcm(b, q), d); ull p = t / q, ta = t / b * a, tc = t / d * c; t = ta / p + 1; if(ta < t * p && t * p < tc) { ansp = t; ansq = q; return true; } return false; } int main() { int T; scanf("%d", &T); while(T--) { scanf("%llu%llu%llu%llu", &a, &b, &c, &d); ull t = gcd(a, b); a /= t; b /= t; t = gcd(c, d); c /= t; d /= t; for(register ull ans = 1; ; ans++) if(check(ans)) { printf("%llu/%llu\n", ansp, ansq); break; } } return 0; }
正解用到了一个叫做类欧几里得算法的东西
很骚……第一次写
形式和欧几里得很像。
对于这道题,大概就是不断的减去整数部分
直到a/b和c/d都不是整数部分,就取倒数
#include<bits/stdc++.h> #define REP(i, a, b) for(register int i = (a); i < (b); i++) #define _for(i, a, b) for(register int i = (a); i <= (b); i++) using namespace std; typedef long long ll; ll p, q; void sim_gcd(ll a, ll b, ll c, ll d) { if(!a) { p = 1; q = d / c + 1; return; } else if(a >= b) { sim_gcd(a % b, b, c - d * (a / b), d); p += q * (a / b); return; } else if(c > d) { p = q = 1; return; } sim_gcd(d, c, b, a); swap(p, q); } int main() { int T; scanf("%d", &T); while(T--) { ll a, b, c, d; scanf("%lld%lld%lld%lld", &a, &b, &c, &d); sim_gcd(a, b, c, d); printf("%lld/%lld\n", p, q); } return 0; }
1060 最复杂的数
把一个数的约数个数定义为该数的复杂程度,给出一个n,求1-n中复杂程度最高的那个数。
例如:12的约数为:1 2 3 4 6 12,共6个数,所以12的复杂程度是6。如果有多个数复杂度相等,输出最小的。
Input
第1行:一个数T,表示后面用作输入测试的数的数量。(1 <= T <= 100) 第2 - T + 1行:T个数,表示需要计算的n。(1 <= n <= 10^18)
Output
共T行,每行2个数用空格分开,第1个数是答案,第2个数是约数的数量。
Input示例
5 1 10 100 1000 10000
Output示例
1 1 6 4 60 12 840 32 7560 64
反素数,之前写过,这次当作复习了
注意剪枝
#include<bits/stdc++.h> #define REP(i, a, b) for(register int i = (a); i < (b); i++) #define _for(i, a, b) for(register int i = (a); i <= (b); i++) using namespace std; typedef long long ll; ll p[] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59}; // 17 ll ans_num, ans, n; void dfs(ll now, ll i, ll time, ll num) { if(i >= 17 || now > n) return; if(num > ans_num) { ans_num = num, ans = now; } else if(num == ans_num && now < ans) ans = now; ll t = p[i]; _for(j, 1, time) { if(now > n / t + 1) return; //这个减枝非常重要,不加就TLE,同时乘法变除 dfs(now * t, i + 1, j, num * (j + 1)); t *= p[i]; } } int main() { int T; scanf("%d", &T); while(T--) { scanf("%lld", &n); ans = n; ans_num = 0; dfs(1, 0, 60, 1); printf("%lld %lld\n", ans, ans_num); } return 0; }
1179 最大的最大公约数
给出N个正整数,找出N个数两两之间最大公约数的最大值。例如:N = 4,4个数为:9 15 25 16,两两之间最大公约数的最大值是15同25的最大公约数5。
Input
第1行:一个数N,表示输入正整数的数量。(2 <= N <= 50000) 第2 - N + 1行:每行1个数,对应输入的正整数.(1 <= S[i] <= 1000000)
Output
输出两两之间最大公约数的最大值。
Input示例
4 9 15 25 16
Output示例
5
这道题没有做出来,是看题解的。
题解有个非常关键的转换的地方
这道题就是求公约数里面的最大值
不要局限在两两个数之间
所以我们就枚举所有公约数,然后取最大。
非常暴力,但是复杂度是nlogn的
怎么枚举呢
对于当前备选答案d
看d,2d,3d,4d中有没有一个kd是两个数的公约数
输入的时候把约数建立一个桶就好了
#include<bits/stdc++.h> #define REP(i, a, b) for(register int i = (a); i < (b); i++) #define _for(i, a, b) for(register int i = (a); i <= (b); i++) using namespace std; const int MAXN = 1e6 + 10; int s[MAXN], n; int main() { scanf("%d", &n); _for(i, 1, n) { int x; scanf("%d", &x); for(register int i = 1; (long long)i * i <= x; i++) if(x % i == 0) { s[i]++; if((long long)i * i != x) s[x / i]++; } } int ans = 0; REP(i, 1, MAXN) { int t = i, cnt = 0; for(register int j = 1; t * j < MAXN; j++) if(s[t * j] > 1) { ans = max(ans, t); break; } } printf("%d\n", ans); return 0; }
1434 区间LCM
一个整数序列S的LCM(最小公倍数)是指最小的正整数X使得它是序列S中所有元素的倍数,那么LCM(S)=X。
例如,LCM(2)=2,LCM(4,6)=12,LCM(1,2,3,4,5)=60。
现在给定一个整数N(1<=N<=1000000),需要找到一个整数M,满足M>N,同时LCM(1,2,3,4,...,N-1,N) 整除 LCM(N+1,N+2,....,M-1,M),即LCM(N+1,N+2,....,M-1,M)是LCM(1,2,3,4,...,N-1,N) 的倍数.求最小的M值。
Input
多组测试数据,第一行一个整数T,表示测试数据数量,1<=T<=5 每组测试数据有相同的结构构成: 每组数据一行一个整数N,1<=N<=1000000。
Output
每组数据一行输出,即M的最小值。
Input示例
3 1 2 3
Output示例
2 4 6
这道题依然没做出来……
怎么这么弱……
我思考这道题的时候没有去想多个数的最大公因数和最小公倍数的本质
最小公倍数的本质是每个数的质因子取最大值,最大公因数是取最小值
可以用这个性质来做
对于每一个质数p
算出最大的幂k
然后求tp^k>n最小的tp^k
那么m一定大于等于这个值
#include<bits/stdc++.h> #define REP(i, a, b) for(register int i = (a); i < (b); i++) #define _for(i, a, b) for(register int i = (a); i <= (b); i++) using namespace std; typedef long long ll; const int MAXN = 1e6 + 10; bool is_prime[MAXN + 10]; vector<int> prime; ll n, m; void get_prime() { memset(is_prime, true, sizeof(is_prime)); is_prime[0] = is_prime[1] = false; REP(i, 2, MAXN) { if(is_prime[i]) prime.push_back(i); REP(j, 0, prime.size()) { if(i * prime[j] >= MAXN) break; is_prime[i * prime[j]] = false; if(i % prime[j] == 0) break; } } } int main() { get_prime(); int T; scanf("%d", &T); while(T--) { scanf("%lld", &n); m = 0; if(n == 1) { puts("2"); continue; } REP(j, 0, prime.size()) { int p = prime[j]; if(p > n) break; ll k = log(n) / log(p); ll num = pow(p, k); ll t = n / num + 1; m = max(m, t * num); } printf("%lld\n", m); } return 0; }
1040 最大公约数之和
给出一个n,求1-n这n个数,同n的最大公约数的和。比如:n = 6
1,2,3,4,5,6 同6的最大公约数分别为1,2,3,2,1,6,加在一起 = 15
Input
1个数N(N <= 10^9)
Output
公约数之和
Input示例
6
Output示例
15
这道题依然没做出来,想到了正解的一部分,没有深入去想。
首先gcd(n,i)肯定是n的约数
所以我们考虑n的约数d
这个约数的倍数是kd
我们看一下什么时候gcd(n, kd) = d
这个时候我们要把n变换一下
n = (n / d) * d
即gcd((n / d) * d, kd) = d
gcd(n / d, k) = 1
因为n / d >= k
所以就有phi(n/d)个
枚举就好了
#include<bits/stdc++.h> #define REP(i, a, b) for(register int i = (a); i < (b); i++) #define _for(i, a, b) for(register int i = (a); i <= (b); i++) using namespace std; typedef long long ll; ll euler(ll d) { ll res = d; for(ll i = 2; i * i <= d; i++) if(d % i == 0) { while(d % i == 0) d /= i; res = res * (i - 1) / i; if(d == 1) break; } if(d > 1) res = res * (d - 1) / d; return res; } int main() { ll n, sum = 0; scanf("%lld", &n); for(ll d = 1; d * d <= n; d++) if(n % d == 0) { sum += d * euler(n / d); if(d * d != n) sum += (n / d) * euler(d); } printf("%lld\n", sum); return 0; }
1225 余数之和
F(n) = (n % 1) + (n % 2) + (n % 3) + ...... (n % n)。其中%表示Mod,也就是余数。
例如F(6) = 6 % 1 + 6 % 2 + 6 % 3 + 6 % 4 + 6 % 5 + 6 % 6 = 0 + 0 + 0 + 2 + 1 + 0 = 3。
给出n,计算F(n), 由于结果很大,输出Mod 1000000007的结果即可。
Input
输入1个数N(2 <= N <= 10^12)。
Output
输出F(n) Mod 1000000007的结果。
Input示例
6
Output示例
3
很容易推出一个结论
当 i <= p <= n/(n/i)时
n/p的结果是一样的
用这个结果加上等差数列求和就好了
记得2要求逆元
最坑的一点是数据高达10^12
所以两个数一乘就炸
所以要打很多很多的%mod
在这里卡了好久
#include<bits/stdc++.h> #define REP(i, a, b) for(register int i = (a); i < (b); i++) #define _for(i, a, b) for(register int i = (a); i <= (b); i++) using namespace std; typedef long long ll; const int mod = 1000000007; ll n, sum; ll power(ll a, int b) { ll res = 1 % mod; a %= mod; for(; b; b >>= 1) { if(b & 1) res = res * a % mod; a = a * a % mod; } return res; } int main() { ll t = power((ll)2, mod - 2); scanf("%lld", &n); sum = (n % mod) * (n % mod) % mod; for(ll i = 1; i <= n;) { ll l = i, r = n / (n / i); sum = (sum - ((n / i) * ((l + r)%mod) % mod * ((r - l + 1)%mod) % mod * t) % mod + mod) % mod; i = r + 1; } printf("%lld\n", sum); return 0; }
1217 Minimum Modular
N个不同的数a[1],a[2]...a[n],你可以从中去掉K个数,并且找到一个正整数M,使得剩下的N - K个数,Mod M的结果各不相同,求M的最小值。
Input
第1行:2个数N, K,中间用空格分隔,N表示元素的数量,K为可以移除的数的数量(1 <= N <= 5000, 0 <= K <= 4, 1 <= a[i] <= 1000000)。
Output
输出符合条件的最小的M。
Input示例
5 1 1 2 10 11 12
Output示例
4
这道题依然没有做出来,因为一直没有什么思路
正解非常非常精彩!!!!
首先要知道一个结论
p ≡ q (mod M) 当且仅当 M | (p - q) p > q
我就是不知道这个结论(其实这是一个很显然的结论)
那么我们就可以预处理所有数的差值
然后枚举m和m的倍数
看有多少对冲突
如果大于k(k+1)/2,就直接舍去
这个优化很重要,不然会TLE
然后接下来就暴力算出需要删掉多少数
就ok了
其实说到底就是暴力,但是加了一个很牛逼的优化,就大大减少的时间
#include<bits/stdc++.h> #define REP(i, a, b) for(register int i = (a); i < (b); i++) #define _for(i, a, b) for(register int i = (a); i <= (b); i++) using namespace std; const int MAXN = 1e6 + 10; int a[MAXN], s[MAXN], b[MAXN]; int n, k; int main() { scanf("%d%d", &n, &k); _for(i, 1, n) scanf("%d", &a[i]); _for(i, 1, n) _for(j, i + 1, n) s[abs(a[i] - a[j])]++; for(int m = n - k; ; m++) { int cnt = 0; for(int i = m; i < MAXN; i += m) cnt += s[i]; if(cnt > k * (k + 1) / 2) continue; cnt = 0; _for(i, 1, n) { if(b[a[i] % m] == m) { if(++cnt > k) break; } else b[a[i] % m] = m; } if(cnt <= k) { printf("%d\n", m); break; } } return 0; }
1616 最小集合
A君有一个集合。
这个集合有个神奇的性质。
若X,Y属于该集合,那么X与Y的最大公因数也属于该集合。
但是他忘了这个集合中原先有哪些数字。
不过幸运的是,他记起了其中n个数字。
当然,或许会因为过度紧张,他记起来的数字可能会重复。
他想还原原先的集合。
他知道这是不可能的……
现在他想知道的是,原先这个集合中至少存在多少数。
样例解释:
该集合中一定存在的是{1,2,3,4,6}
Input
第一行一个数n(1<=n<=100000)。 第二行n个数,ai(1<=ai<=1000000,1<=i<=n)。表示A君记起来的数字。 输入的数字可能重复。
Output
输出一行表示至少存在多少种不同的数字。
Input示例
5 1 3 4 6 6
Output示例
5
可以处理出f(i)表示i是多少个数的因数
当f(i) == f(j)且i | j, i < k时
i不是gcd,删去
否则就是集合内的数
#include<bits/stdc++.h> #define REP(i, a, b) for(register int i = (a); i < (b); i++) #define _for(i, a, b) for(register int i = (a); i <= (b); i++) using namespace std; const int MAXN = 1e6 + 10; int a[MAXN], f[MAXN]; int n, k, maxt; int main() { scanf("%d", &n); _for(i, 1, n) { int x; scanf("%d", &x); f[x] = 1; maxt = max(maxt, x); } _for(i, 1, maxt) for(int j = i * 2; j <= maxt; j += i) if(f[j]) f[i]++; int ans = 0; _for(i, 1, maxt) if(f[i]) { bool ok = true; for(int j = i * 2; j <= maxt; j += i) if(f[j] == f[i]) { ok = false; break; } if(ok) ans++; } printf("%d\n", ans); return 0; }
总结:
数论最后几道题都没做出来,因为题做得少,思维能力不够和一些结论不知道
不过还是很有收获的。
不要怕打暴力,只要复杂度是对的就行。
很多解法非常暴力但我却直接pass掉了
就像好几道题都是直接暴力枚举n内的数i和i的所有倍数
复杂度是nlogn的
这个时候往往之前做过一些预处理(往往是一个桶)使得对于枚举的每个数可以O(1)的判断