csp信奥赛C++之反素数

csp信奥赛C++之反素数

原题说明:洛谷P1463 反素数

题目描述

对于任何正整数 x x x,其约数的个数记作 g ( x ) g(x) g(x)。例如 g ( 1 ) = 1 g(1)=1 g(1)=1, g ( 6 ) = 4 g(6)=4 g(6)=4。

如果某个正整数 x x x 满足: ∀ 0 < i < x \forall 0 \lt i \lt x ∀0<i<x,都有 g ( x ) > g ( i ) g(x) \gt g(i) g(x)>g(i),则称 x x x 为反素数 。例如, 1 , 2 , 4 , 6 , 12 , 24 1,2,4,6,12,24 1,2,4,6,12,24 等都是反素数。

现在给定一个正整数 N N N,你能求出不超过 N N N 的最大的反素数么?

输入格式

仅一行一个正整数 N N N。

输出格式

仅一行一个正整数,代表不超过 N N N 的最大的反素数。

输入输出样例 1
输入 1
复制代码
1000
输出 1
复制代码
840
说明/提示

对于所有数据,有 1 ≤ N ≤ 2 × 10 9 1 \leq N \leq 2 \times 10^9 1≤N≤2×109。

暴力思路

  1. 问题理解

    反素数定义为:对于一个正整数 (x),如果所有比 (x) 小的正整数 (i) 的约数个数都小于 (x) 的约数个数,那么 (x) 就是反素数。

    题目要求:在 1 ∼ N 1 \sim N 1∼N范围内,找出最大的反素数。

  2. 暴力思路

    • 从 1到 N 逐个枚举每个数 x。
    • 对于每个 (x),计算它的约数个数 g(x)。
    • 维护一个变量 mx 记录目前为止遇到的最大 g(x),以及对应的数 ans
    • 如果当前 g(x) > mx,说明 (x) 比之前所有数都有更多的约数,因此它是反素数,更新 mxans
    • 遍历结束后,ans 即为不超过 N 的最大反素数。
  3. 计算约数个数的技巧

    • 一个数的约数成对出现(除非是完全平方数,其中一对相等)。因此只需枚举到 x \sqrt{x} x 。
    • 对于每个能整除 x 的 i,我们同时得到两个约数:i 和 x/i,所以计数器加 2。
    • 如果 i × i = x i \times i = x i×i=x(即 x 是完全平方数),那么刚才加 2 时把同一个约数加了两次,需要减 1 修正。
  4. 复杂度

    最坏情况下需计算每个数的约数个数,时间复杂度约为 O ( N N ) O(N \sqrt{N}) O(NN ),对于 N ≤ 2 × 10 9 N \le 2\times 10^9 N≤2×109 必然超时。

暴力代码(50分)

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

int n;

int main() {
    cin >> n;                     // 输入上限 N

    int mx = 0;                   // 记录当前找到的最大约数个数
    int ans = 1;                  // 记录当前找到的反素数(初始为1)

    // 枚举 1 到 N 的所有正整数 x
    for (int x = 1; x <= n; x++) {
        int cnt = 0;              // 用于统计 x 的约数个数
        int root = sqrt(x);       // x 的平方根,只需枚举到 root

        // 枚举 1 到 sqrt(x) 的所有数 i
        for (int i = 1; i <= root; i++) {
            if (x % i == 0) {     // 如果 i 是 x 的约数
                cnt += 2;         // 则 i 和 x/i 都是约数,所以加 2
                if (i * i == x)   // 如果 i 恰好等于 x/i(即 x 是完全平方数)
                    cnt--;        // 则刚才加多了 1 个,需要减去
            }
        }

        // 如果当前 x 的约数个数严格大于之前所有数的约数个数
        if (cnt > mx) {
            mx = cnt;             // 更新最大约数个数
            ans = x;              // 更新答案为当前 x
        }
    }

    cout << ans << endl;          // 输出不超过 N 的最大的反素数
    return 0;
}

优化思路分析

1. 反素数的定义与性质
  • 定义 :一个正整数 x x x 是反素数,当且仅当对于任意 i < x i < x i<x,都有 g ( x ) > g ( i ) g(x) > g(i) g(x)>g(i)( g ( x ) g(x) g(x) 表示 x x x 的约数个数)。
  • 直观理解 :反素数就是在所有不超过它的数中,拥有最多约数个数的那个数。因此,反素数一定是"高度合成数",但还必须保证前面没有数能与之持平或超过。
2. 约数个数的计算公式

若 x x x 的质因数分解为:
x = p 1 a 1 p 2 a 2 ⋯ p k a k x = p_1^{a_1} p_2^{a_2} \cdots p_k^{a_k} x=p1a1p2a2⋯pkak

则其约数个数为:
g ( x ) = ( a 1 + 1 ) ( a 2 + 1 ) ⋯ ( a k + 1 ) g(x) = (a_1+1)(a_2+1)\cdots(a_k+1) g(x)=(a1+1)(a2+1)⋯(ak+1)

3. 反素数必须满足的两个重要性质
性质一:质因子必须是连续的最小质数
  • 假设 x x x 是一个反素数,如果它的质因子集合不是从 2 开始连续的前 k k k 个质数,比如缺少质数 p j p_j pj 却包含了更大的质数 p t p_t pt,那么我们可以用 p j p_j pj 替换 p t p_t pt(保持指数不变),得到一个新的数 x ′ < x x' < x x′<x,但 g ( x ′ ) = g ( x ) g(x') = g(x) g(x′)=g(x)。这与反素数的定义矛盾(因为存在更小的数有相同的约数个数)。
  • 因此,反素数的质因子一定是从 2 开始的连续质数: 2 , 3 , 5 , 7 , ... 2,3,5,7,\dots 2,3,5,7,...。
性质二:指数单调不增(非递增)
  • 假设 x = 2 a 1 3 a 2 ⋯ p k a k x = 2^{a_1}3^{a_2}\cdots p_k^{a_k} x=2a13a2⋯pkak 是反素数,若存在 a i < a i + 1 a_i < a_{i+1} ai<ai+1(即指数递增),那么交换这两个指数,得到 x ′ = 2 a 1 ⋯ p i a i + 1 p i + 1 a i ⋯ x' = 2^{a_1}\cdots p_i^{a_{i+1}}p_{i+1}^{a_i}\cdots x′=2a1⋯piai+1pi+1ai⋯。由于 p i < p i + 1 p_i < p_{i+1} pi<pi+1,交换后 x ′ < x x' < x x′<x,但约数个数不变(因为指数集合相同)。同样存在更小的数有相同约数个数,矛盾。
  • 因此,指数必须满足 a 1 ≥ a 2 ≥ ⋯ ≥ a k ≥ 1 a_1 \ge a_2 \ge \cdots \ge a_k \ge 1 a1≥a2≥⋯≥ak≥1。
4. 基于性质的搜索算法

利用上述两条性质,所有可能的反素数候选都可以通过**深度优先搜索(DFS)**枚举指数组合得到,并且可以大幅剪枝。

搜索框架
  • 质数表 :由于 N ≤ 2 × 10 9 N \le 2\times10^9 N≤2×109,我们只需考虑前几个质数。 2 × 3 × 5 × 7 × 11 × 13 × 17 × 19 × 23 × 29 ≈ 6.13 × 10 9 2\times3\times5\times7\times11\times13\times17\times19\times23\times29\approx 6.13\times10^9 2×3×5×7×11×13×17×19×23×29≈6.13×109,已经超过 N N N,所以前 10 个质数足够。
  • DFS 状态
    • cur:当前构造的数值。
    • cnt:当前约数个数(根据公式累乘)。
    • last:上一个质数使用的指数,当前质数的指数不能超过它(满足性质二)。
    • pos:当前考虑第几个质数(从 0 开始)。
  • 搜索过程
    1. 每到达一个状态,首先检查是否需要更新答案:
      • cnt > mx(历史最大约数个数),则更新答案和最大值。
      • cnt == mxcur < ans(数值更小),也更新答案(因为反素数要求数值本身最小)。
    2. 然后尝试为下一个质数(p[pos])分配指数 i i i,从 1 到 last 枚举(注意指数至少为 1)。
      • 累乘:new_cur = cur * p[pos]^i,但实际采用迭代乘法,防止幂运算开销。
      • 如果 new_cur > N,则停止枚举更大的指数(剪枝)。
      • 递归调用 dfs(new_cur, cnt*(i+1), i, pos+1)
  • 初始调用dfs(1, 1, 31, 0)
    • cur=1cnt=1( g ( 1 ) = 1 g(1)=1 g(1)=1)。
    • last 初始设为一个很大的数(31),因为第一个质数 2 的指数理论上限可达 ⌊ log ⁡ 2 N ⌋ \lfloor \log_2 N \rfloor ⌊log2N⌋,而 2 31 > 2 × 10 9 2^{31}>2\times10^9 231>2×109,所以 31 足够大,相当于"无限"。
    • pos=0 表示从质数 2 开始。
5. 算法的正确性
  • 完备性:所有可能的指数组合(满足指数非递增)都会被搜索到,因为递归树遍历了所有合法的指数分配。由于质数连续且指数单调,每个反素数必然对应一条搜索路径。
  • 剪枝的正确性 :当当前乘积超过 N N N 时,后续更大指数或更大质数的乘积必定更大,因此可以直接剪枝,不会遗漏任何不超过 N N N 的候选数。
  • 最优性 :在搜索过程中,我们时刻用全局变量 ansmx 记录当前最优值。由于搜索顺序(指数从大到小枚举)保证了先遇到的可能是较小数值,但最终通过比较更新,可以确保得到的是不超过 N N N 的最大反素数(即约数个数最多且数值最小)。
6. 时间复杂度分析
  • 状态总数非常有限。对于 N = 2 × 10 9 N=2\times10^9 N=2×109,指数组合的数目大约在几千量级(实际测试中递归次数不超过 5000),因此算法可以在毫秒级内完成。

AC代码

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

// 质数表:前10个质数,因为2*3*5*7*11*13*17*19*23 ≈ 2.2e8,再乘29 ≈ 6.3e9 > 2e9
const int p[] = {2,3,5,7,11,13,17,19,23,29};
int n;          // 输入上限
ll ans = 1;     // 当前答案,初始为1
int mx = 1;     // 当前最大约数个数,g(1)=1

// 深搜:cur 当前乘积,cnt 当前约数个数,last 上一质数的指数,pos 当前质数下标
void dfs(ll cur, int cnt, int last, int pos) {
    // 更新答案:约数个数更大 或 相等但数值更小
    if (cnt > mx || (cnt == mx && cur < ans)) {
        mx = cnt;
        ans = cur;
    }
    // 没有更多质数可选
    if (pos >= 10) return;
    ll tmp = cur;           // 用于累乘
    // 枚举当前质数 p[pos] 的指数 i,从1到last,并保证乘积不超过n
    for (int i = 1; i <= last; ++i) {
        tmp *= p[pos];
        if (tmp > n) break; // 超出范围,停止枚举更大的指数
        dfs(tmp, cnt * (i + 1), i, pos + 1);
    }
}

int main() {
    cin >> n;
    dfs(1, 1, 31, 0);   // last初始设为一个很大的数(31足以覆盖2^31>2e9)
    cout << ans << endl;
    return 0;
}

功能分析

以 (N=10) 为例,我们详细分析 DFS 算法的执行过程。算法利用反素数的性质(质因子连续且指数非递增)枚举所有可能的数,并动态更新最优解(约数个数最多且数值最小)。

初始状态
  • 质数表:p[] = {2,3,5,7,11,13,17,19,23,29}(只需前几个)
  • 全局变量:ans = 1(当前答案),mx = 1(当前最大约数个数)
  • 初始调用:dfs(1, 1, 31, 0)
    参数说明:当前乘积 cur=1,约数个数 cnt=1,上一指数 last=31(足够大),当前质数下标 pos=0(对应质数2)
递归树展开
第一层:质数 2

dfs(1,1,31,0) 中:

  • 更新答案:cnt=1mx=1 相等且 cur=1 等于 ans,不更新。
  • 枚举指数 i 从 1 开始,累乘 cur 并确保不超过 N=10
  1. i=1cur = 1*2 = 2,调用 dfs(2, 1*(1+1)=2, last=1, pos=1)
  2. i=2cur = 2*2 = 4,调用 dfs(4, 1*(2+1)=3, last=2, pos=1)
  3. i=3cur = 4*2 = 8,调用 dfs(8, 1*(3+1)=4, last=3, pos=1)
  4. i=4cur = 8*2 = 16 > 10,停止。
第二层:处理各个分支
分支 A:dfs(2,2,1,1)(当前质数 3)
  • 更新答案:cnt=2 > mx=1 → 更新 ans=2, mx=2
  • 枚举质数 3 的指数 i 从 1 到 last=1
    • i=1cur = 2*3 = 6,调用 dfs(6, 2*(1+1)=4, last=1, pos=2)
分支 A1:dfs(6,4,1,2)(当前质数 5)
  • 更新答案:cnt=4 > mx=2 → 更新 ans=6, mx=4
  • 枚举质数 5 的指数 i 从 1 到 last=1
    • i=1cur = 6*5 = 30 > 10,停止。
  • 返回
分支 B:dfs(4,3,2,1)(当前质数 3)
  • 更新答案:cnt=3 < mx=4,不更新
  • 枚举质数 3 的指数 i 从 1 到 last=2
    • i=1cur = 4*3 = 12 > 10,停止(后续更大指数更不可能)
  • 返回
分支 C:dfs(8,4,3,1)(当前质数 3)
  • 更新答案:cnt=4 == mx=4,但 cur=8 > ans=6,不更新
  • 枚举质数 3 的指数 i 从 1 到 last=3
    • i=1cur = 8*3 = 24 > 10,停止
  • 返回
最终结果

所有递归结束后,全局变量 ans=6mx=4,输出 6

正确性验证

不超过 10 的正整数中,约数个数最多为 4,对应的数有 6、8、10。其中 6 是最小的,且所有小于 6 的数的约数个数均小于 4,因此 6 是反素数。算法通过 DFS 遍历了所有可能的候选(满足质数连续且指数非递增),并正确选出了最优解。

算法要点
  • 每次进入 DFS 即检查更新,保证任何新数都被考虑。
  • 利用质数连续和指数非递增性质大幅剪枝,避免无效枚举。
  • 当乘积超过 N 时立即停止,确保只搜索合法范围。

【文末福利:一等奖秘籍汇总】(完整csp信奥赛C++学习资料):

1、csp/信奥赛C++,完整信奥赛系列课程(永久学习):

https://edu.csdn.net/lecturer/7901 点击跳转

2、CSP信奥赛C++竞赛拿奖视频课:

https://edu.csdn.net/course/detail/40437 点击跳转

3、csp信奥赛高频考点知识详解及案例实践:

CSP信奥赛C++动态规划:
https://blog.csdn.net/weixin_66461496/category_13096895.html点击跳转

CSP信奥赛C++标准模板库STL:
https://blog.csdn.net/weixin_66461496/category_13108077.html 点击跳转

信奥赛C++提高组csp-s知识详解及案例实践:
https://blog.csdn.net/weixin_66461496/category_13113932.html 点击跳转

4、csp信奥赛冲刺一等奖有效刷题题解:

CSP信奥赛C++初赛及复赛高频考点真题解析(持续更新): https://blog.csdn.net/weixin_66461496/category_12808781.html 点击跳转

信奥赛C++提高组csp-s初赛&复赛真题题解(持续更新):
https://blog.csdn.net/weixin_66461496/category_13125089.html 点击跳转

5、GESP C++考级真题题解:

GESP(C++ 一级+二级+三级)真题题解(持续更新):https://blog.csdn.net/weixin_66461496/category_12858102.html 点击跳转

GESP(C++ 四级+五级+六级)真题题解(持续更新):https://blog.csdn.net/weixin_66461496/category_12869848.html 点击跳转

GESP(C++ 七级+八级)真题题解(持续更新):
https://blog.csdn.net/weixin_66461496/category_13117178.html 点击跳转

· 文末祝福 ·

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
int main(){
	cout<<"跟着王老师一起学习信奥赛C++";
	cout<<"    成就更好的自己!       ";
	cout<<"  csp信奥赛一等奖属于你!   ";
	return 0;
}
相关推荐
Renhao-Wan1 小时前
Java 算法实践(七):动态规划
java·算法·动态规划
pursuit_csdn2 小时前
LeetCode 1461. Check If a String Contains All Binary Codes of Size K
算法·leetcode·职场和发展
Crazy________3 小时前
力扣113个mysql简单题解析(包含plus题目)
mysql·算法·leetcode·职场和发展
生成论实验室3 小时前
即事经智能:一种基于生成易算的通用智能新范式(书)
人工智能·神经网络·算法·架构·信息与通信
YxVoyager3 小时前
基于 X-Macro 宏的手动 RTTI 实现模式
c++
清风20223 小时前
vllm 采样调研
人工智能·算法·机器学习
初次攀爬者3 小时前
力扣解题-无重复字符的最长子串
后端·算法·leetcode
MekoLi293 小时前
生成式推荐系统:从“判别式匹配”到“生成式创造”的范式革命
后端·算法
SoulruiA3 小时前
超容易理解+模版套路解决LeetCode 前序+中序、中序+后序、前序+后序遍历构造树问题
java·算法·力扣