AcWing 198. 反素数
题目传送门
吐槽
这\(NM\)难成这个样子,需要挖掘三个数学性质,还说是简单,能不能不欺负人啊!
一、题目描述
0、前置知识
设\(N\)的唯一分解式:
\[N=P_1^{c_1}P_2^{c_2}...P_k^{c_k} \]
知识总结
- 约数个数公式
\(d(N)=(c_1+1)\times (c_2+1)\times ... \times (c_k+1)\)
举个栗子:
\(180=2^2∗3^2∗5\)
约数个数=\((1+2)∗(1+2)∗(1+1)=18\)
证明:
组合数学知识,每个质数因子可以选择\(0 \sim r_k\)个,即\(1+r_k\)个,根据乘法原理,总的个数就是上式,证毕。
- 约数和公式
\(\sigma(n)=({p_1}^{0}+{p_1}^{1}+{p_1}^{2}+...+{p_1}^{c_1})({p_2}^{0}+{p_2}^{1}+{p_2}^{2}+...+{p_2}^{c_2})...({p_k}^{0}+{p_k}^{1}+{p_k}^{2}+...+{p_k}^{c_k})\)
Sigma(大写Σ,小写σ),是第十八个希腊字母。
举个栗子:
\(180= 2^2 * 3^2 * 5\)
约数和\(=(1+2+4) * (1+3+9 ) * (1+5)=546\)
证明:
和上面证明个数一样的思路,每个质数因子可以选择\(0\sim c_k\)个,以上面\(180\)为例,第一项质数因子\(2\),可以不要,也可以要\(1\)或\(2\)个,每种选择情况,因为还要和后面的式子进行乘积,不要的话,理解为\(2^0\);\(1\)个的话理解为\(2^1\);\(2\)个的话理解为\(2^2\),每项都按这个规则拆开,就是上面的公式了,证毕。
1、反素数定义
对于任何正整数\(x\),其约数的个数记作\(g(x)\),例如\(g(1)=1\)、\(g(6)=4\)。
如果某个正整数\(x\)满足:对于任意的小于\(x\)的正整数 \(i\),都有\(g(x)>g(i)\) ,则称\(x\)为反素数。
说白了,素数之所以称为素数,就是因为约数少,只有\(1\)和自己。
反素数,也就可以理解为:约数多,而且每个数的约数个数,都比小于自己的数字约数个数多。
2、求解
现在给定一个数\(N\),请求出不超过\(N\)的最大的反素数
二、挖掘反素数的性质
性质1
\(1\sim N\)中最大的反素数,就是 \(1\sim N\)中约数个数最多的数中最小的一个。
- \(g(x)\)=约数个数,求最大的反素数,不就是在求哪个数的约数个数最多嘛~
- 如果不是最小的那一个,必然会出现
g(x)=g(i)
,与反素数的定义就矛盾了~
性质2
联想约数个数公式:
\(d(N)=(c_1+1)\times (c_2+1)\times ... \times (c_k+1)\)
一个数的约数个数,不与具体的质数因子相关,只与质数因子的幂次相关!
换言之,由于我们追求最小的数字,那么根据 贪心思想 ,我们知道:质因子越小,幂次越大,才可以保障最终的数字最小!!!
即\(c_1>=c_2>=c_3>=...>=c_k\)
举个栗子
假设\(t=2^{c_1}×3^5×5^4×7^{c_4}…\)
则\(c_1>=5>=4>=c_4\),交换\(3\)和\(5\)的次数即变成\(t=2^{c_1}×3^4×5^5×7^{c_4}…\),这样交换约数个数相同,可\(3^4∗5^5 > 3^5∗5^4\),交换后该数变大了
性质3
- \(1\sim N\)中任何数的不同质因子都不会超过\(9\)个
因为\(2 \times 3\times 5\times 7\times 11\times 13\times 17\times 19\times 23\times 29 >2\times 10^9\):
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
int primes[] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29};
int main() {
cout << INT_MAX << endl;
LL s = 1;
for (int i = 0; i <= 8; i++) s *= primes[i];
cout << s << endl;
s = 1;
for (int i = 0; i <= 9; i++) s *= primes[i];
cout << s << endl;
return 0;
}
输出结果:
2147483647
223092870
6469693230
- 所有质因数的指数总和最大不超过\(30\)
因为\(2^{31}>2\times 10^9\).
根据上面一系列性质,我们得出了最简洁的思路,使用\(dfs\),尝试确定前九个质数的指数,然后满足指数单调递减,总乘积不超过\(N\),且同时记录约数的个数。
三、实现代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
int primes[] = {2, 3, 5, 7, 11, 13, 17, 19, 23}; //前9位质数数组
int n; //讨论n以内的最大反素数
int MaxNumber; //在n以内,最大的反素数(约数个数最多)
int MaxCnt; //取到的最大反素数MaxNumber,它有多少个约数
/**
* @param step 枚举到第几个质数
* @param last 次数最大是多少,也可以理解为上一次的最次数,因为每次的幂次不能大于上一次的幂次
* @param number 当前路线已经取得的最大反素数
* @param cnt 当前路线已经取得的最大反素数,它有多少个约数
*/
void dfs(int step, int last, int number, int cnt) {
//(1)当前约数个数是大于最大约数个数
//(2)等于最大约数个数并且 当前取得的最大反素数 小于 最大反素数
if (cnt > MaxCnt || cnt == MaxCnt && number < MaxNumber)
MaxCnt = cnt, MaxNumber = number; //更新
if (step == 9) return; //边界
for (int i = 1; i <= last; i++) { //枚举每一个可行的幂次值(需要幂次小于等于上一次的幂次)
if ((LL)number * primes[step] > n) break; //超过了肯定不行
number *= primes[step]; //多乘一个质因子primes[step]
//注意这里需要在单独的一行写 number*=primes[step],因为这其实是一个累乘的原因,不能简单的去掉直接将乘法算式写入dfs函数中!
dfs(step + 1, i, number, cnt * (i + 1)); //约数个数公式
}
}
int main() {
scanf("%d", &n);
dfs(0, 30, 1, 1);
cout << MaxNumber << endl;
return 0;
}
四、一点疑问
\(Q\):为什么在\(dfs\)中循环是从小到大的,而不是从大到小的,从大到小不行吗?
\(A\):也可以,但复杂的多,不推荐:
#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long LL;
int ps[] = {2, 3, 5, 7, 11, 13, 17, 19, 23};
int n;
int MaxNumber; //在n以内,最大的反素数(约数个数最多)
int MaxCnt; //取到的最大反素数MaxNumber,它有多少个约数
//快速幂模板
LL qmi(LL a, LL b) {
LL res = 1;
while (b) {
if (b & 1) res *= a;
a *= a;
b >>= 1;
}
return res;
}
void dfs(int step, int last, LL number, int cnt) {
if (step == 9) return;
if (cnt > MaxCnt || (number < MaxNumber && cnt == MaxCnt))
MaxNumber = number, MaxCnt = cnt;
//如果真的要逆天而行,从大到小,那么首先需要计算ps[step]的last次方,这玩意不用快速幂就不好算
//而且可能很大,会爆long long,需要使用ULL
number *= qmi(ps[step], last);
for (int i = last; i >= 1; i--) { //如你所愿意,倒着从大到小
if (number <= n) dfs(step + 1, i, number, cnt * (i + 1)); //没法剪枝,只能判断不派发新任务
number /= ps[step]; //由乘变除
}
}
int main() {
scanf("%d", &n);
dfs(0, 30, 1, 1);
printf("%d\n", MaxNumber);
return 0;
}
由小到大:
(1)简单乘积即可,不用初始化一个极大的可能爆long long 的幂次结果.
(2)方便剪枝,不行了就不再计算。
由大到小:
(1)需要初始化一个极大的幂次结果,不管有用没用,得先算出来,等着挨除。
(2)无法剪枝,剪枝了后面就没法算了,只能是判断不新派发任务。
五、经验总结
在INT_MAX
范围内,最大的反素数拥有的约数个数是\(1600\)个 ,这个是整数范围内约数个数的极限值, 需要记住这个值,后面的做题当中可以当做常数使用!
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
int primes[] = {2, 3, 5, 7, 11, 13, 17, 19, 23}; //前9位质数数组
int n; //讨论n以内的最大反素数
int MaxNumber; //在n以内,最大的反素数(约数个数最多)
int MaxCnt; //取到的最大反素数MaxNumber,它有多少个约数
/**
* @param step 枚举到第几个质数
* @param last 次数最大是多少,也可以理解为上一次的最次数,因为每次的幂次不能大于上一次的幂次
* @param number 当前路线已经取得的最大反素数
* @param cnt 当前路线已经取得的最大反素数,它有多少个约数
*/
void dfs(int step, int last, int number, int cnt) {
if (step == 9) return; //边界
//(1)当前约数个数是大于最大约数个数
//(2)等于最大约数个数并且 当前取得的最大反素数 小于 最大反素数
if (cnt > MaxCnt || cnt == MaxCnt && number < MaxNumber)
MaxCnt = cnt, MaxNumber = number; //更新
for (int i = 1; i <= last; i++) { //枚举每一个可行的幂次值(需要幂次小于等于上一次的幂次)
if ((LL)number * primes[step] > n) break; //超过了肯定不行
number *= primes[step]; //多乘一个质因子primes[step]
//注意这里需要在单独的一行写 number*=primes[step],因为这其实是一个累乘的原因,不能简单的去掉直接将乘法算式写入dfs函数中!
dfs(step + 1, i, number, cnt * (i + 1)); //约数个数公式
}
}
int main() {
scanf("%d", &n);
dfs(0, 30, 1, 1);
cout << MaxNumber << endl;
cout<<MaxCnt<<endl;
return 0;
}
输入\(2e9\),输出\(1536\)。
输入INT_MAX
,即\(2147483647\),输出\(1600\)。