Bash's Big Day

OJ: CF

题目 ID: 757B

标签: 数论埃式筛

日期: 2025-12-13 11:26

题目说:如果选出的数字最大公约数为 1,它们就会打架。我们要选最多的人且不打架。

反过来想: 为了让它们不打架,选出的所有数字的最大公约数 gg 必须满足 g>1g > 1

这等价于:选出的所有数字都必须是某个整数 x(x>1)x (x > 1) 的倍数。

贪心策略我们不需要找出那个具体的最大公约数是多少,我们只需要找到一个因子 xx,使得输入数组中有最多的数是 xx 的倍数。

问题就变成了:在 22100000100000(题目中 sis_i 的最大值)之间,哪一个整数 xx 能整除输入数组中数量最多的元素?

算法流程

  1. 桶记录(Frequency Array):因为数字范围比较小(10510^5),我们可以先用一个数组 cnt[] 记录每个力量值出现的次数。比如 cnt[4] = 2 表示力量为 4 的小精灵有 2 只。
  2. 枚举因子(暴力枚举的优化):我们需要枚举所有可能的公共因子 ii(从 2 到 100000100000)。
  3. 统计倍数:对于每一个因子 ii,我们去统计它的倍数 i,2i,3i,i, 2i, 3i, \dots 在输入数组中一共出现了多少次。即:Sumi=cnt[i]+cnt[2i]+cnt[3i]+\text{Sum}_i = \text{cnt}[i] + \text{cnt}[2i] + \text{cnt}[3i] + \dots
  4. 取最大值:比较所有 ii 对应的总数,最大的那个就是答案。

时间复杂度分析(为什么这样做不会超时?)

你可能会担心:外层循环从 2 枚举到 10510^5,内层循环还要枚举倍数,会不会超时?

这里就要用到调和级数的知识了。 设 VV 为数字的最大值(10510^5)。

  • i=2i=2 时,内层循环跑 V/2V/2 次。
  • i=3i=3 时,内层循环跑 V/3V/3 次。
  • 总运算次数 = V/2+V/3++V/V=V×(1/2+1/3++1/V)V/2 + V/3 + \dots + V/V = V \times (1/2 + 1/3 + \dots + 1/V)

这是一个调和级数,其和近似为 lnV\ln V。 所以总复杂度是 O(VlnV)O(V \ln V)。 对于 V=105V=10^5VlnV105×11.51.15×106V \ln V \approx 10^5 \times 11.5 \approx 1.15 \times 10^6,这在计算机 1 秒钟能处理的 10810^8 次运算范围内,非常快

cpp 代码

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

const int maxn = 1e5+5;
int cnt[maxn];
int n;

void init(){
    std::cin >> n;
    for(int i = 1;i <= n ;++i ) // i: 1->n
    {
        int t;
        std::cin >> t;
        cnt[t]++;
    }
}


int main (int argc, char *argv[]) {
    init();
    int ans = 1;
    for(int g = 2;g < maxn ;++g ) // g: 1->maxn
    {
        int now_sum = 0;
        for(int i = g ; i < maxn ;i+=g) {
            now_sum += cnt[i];
        }
        ans = max(ans,now_sum);
    }
    std::cout << ans << "\n";

    return 0;
}

haskell 代码

haskell
import Data.Array

-- 定义最大值,题目中 s_i <= 100000
maxVal :: Int
maxVal = 100000

solve :: [Int] -> Int
solve xs = ans
  where
    -- 1. 构建频率数组 (桶)
    -- accumArray 的参数含义:
    -- (+) : 累积函数 (遇到相同的下标怎么处理?相加)
    -- 0   : 初始值
    -- (1, maxVal) : 数组下标范围
    -- [(x, 1) | x <- xs] : 数据源,把每个输入数字 x 变成 (下标, 增量) 的元组
    counts :: Array Int Int
    counts = accumArray (+) 0 (1, maxVal) [(x, 1) | x <- xs]

    -- 2. 定义计算函数:给定一个公因子 g,计算它的倍数出现次数之和
    -- [g, 2*g .. maxVal] 利用 Haskell 的语法糖生成公差为 g 的数列
    countMultiples g = sum [counts ! k | k <- [g, 2*g .. maxVal]]

    -- 3. 对 2 到 maxVal 之间的所有数计算结果,取最大值
    -- 注意列表推导式 [countMultiples g | g <- [2 .. maxVal]]
    allScores = [countMultiples g | g <- [2 .. maxVal]]
    
    -- 4. 处理边界情况
    -- 如果输入全是 1,allScores 里的值可能全是 0。
    -- 但题目允许至少带走 1 个(不能自己打自己),所以结果至少是 1。
    ans = max 1 (if null allScores then 0 else maximum allScores)

main :: IO ()
main = do
    -- 读取输入
    _ <- getLine -- 忽略第一行 n,因为 Haskell 处理列表不需要知道长度
    content <- getContents
    -- 将剩余输入转换为整数列表
    let nums = map read (words content) :: [Int]
    
    -- 计算并输出
    print (solve nums)