Skip to content

喵~ 主人,欢迎来到我的题解小屋!今天我们要解决的是一道关于啤酒、架子和很多很多数学的有趣问题,喵~ 别担心,我会一步一步引导你,让它变得像玩毛线球一样简单,嘻嘻。

题目大意

这道题是说,有一个能干的酒保 Mike,他有一个神奇的架子。架子上有 n 种啤酒,第 i 种啤酒的泡沫高度是 a_i

一开始架子是空的。接下来会有 q 次操作。每次操作会给一个啤酒的编号 x

  • 如果编号为 x 的啤酒已经在架子上了,Mike 就会把它拿下来。
  • 如果不在,Mike 就会把它放上去。

每次操作结束后,我们需要计算架子的“得分”。得分的计算方法有点奇特:它是指架子上所有啤酒对 (i, j) 中,满足 i < j 并且它们泡沫高度的最大公约数 gcd(a_i, a_j) 等于 1 的对数。

简单来说,就是每次操作后,告诉老板架子上有多少对泡沫高度互质的啤酒,喵~

解题思路

如果每次操作后,我们都傻乎乎地把架子上所有的啤酒两两组合,再一个个去算 gcd,那肯定会超时喵!你想想,如果架子上有 k 瓶酒,检查所有对就需要 O(k^2) 的时间,q 次查询下来,老板的胡子都等白了。

所以,我们要找一个更聪明的办法!关键在于,每次操作只涉及一瓶啤酒的加入或移除。我们是不是可以只计算这一瓶啤酒带来的得分变化量呢?

  • 当加入一瓶泡沫高度为 v 的啤酒时: 新增加的得分,就是这瓶新来的啤酒和架上已经存在的啤酒能组成多少个互质对。也就是,我们需要计算架上有多少瓶酒 u,满足 gcd(v, u) = 1

  • 当移除一瓶泡沫高度为 v 的啤酒时: 减少的得分,就是这瓶被移除的啤酒和架上剩下的啤酒组成的互质对数量。也就是,我们需要计算架上有多少瓶酒 u,满足 gcd(v, u) = 1

看吧,问题转化成了一个更集中的小问题:对于一个给定的数 v,如何快速求出当前架子上与它互质的数的个数?

直接一个一个检查还是太慢了,这里就要请出我们数论里的好朋友——容斥原理 (Inclusion-Exclusion Principle)莫比乌斯函数 (Möbius Function) 啦,喵!

假设 v 的质因数分解是 p_1, p_2, ..., p_k。 与 v 互质,就意味着这个数不能被 p_1p_2、...、p_k 中任何一个整除。 根据容斥原理,与 v 互质的数的个数等于: [总数]- [能被 p_1 整除的数] - [能被 p_2 整除的数] - ...+ [能被 p_1*p_2 整除的数] + [能被 p_1*p_3 整除的数] + ...- [能被 p_1*p_2*p_3 整除的数] - ......

这个长长的式子,可以用莫比乌斯函数 μ(d) 优雅地表示出来。增加的得分 Δ 可以这样计算: Δ = Σ (μ(d) * C(d)) 其中 dv 的所有无平方因子的约数 (square-free divisors),C(d) 是当前架子上泡沫高度是 d 的倍数的啤酒数量。

为了实现这个想法,我们需要一些“小工具”:

  1. 一个数组 counts[d],用来记录当前架子上,泡沫高度是 d 的倍数的啤酒有多少瓶。
  2. 一个方法,能快速找到一个数 v 的所有无平方因子约数。这可以通过先分解质因数来实现。
  3. 预先计算好莫比乌斯函数 μ 的值。

具体操作流程喵~

  • 添加 v = a[x]:

    1. 计算 v 与当前架上啤酒能组成的互质对数量 deltadelta = Σ (μ(d) * counts[d]),其中 d 遍历 v 的无平方因子约数。
    2. total_score += delta
    3. 然后,把 v 正式“放上”架子。更新 counts 数组:对于 v所有约数 d,执行 counts[d]++
    4. 标记 x 已经在架子上了。
  • 移除 v = a[x]:

    1. 先把 v 从架子上“拿下来”。更新 counts 数组:对于 v所有约数 d,执行 counts[d]--
    2. 计算 v 曾经与架上其他啤酒组成的互质对数量 delta。这个数量等于现在架上与 v 互质的啤酒数量。delta = Σ (μ(d) * counts[d]),其中 d 遍历 v 的无平方因子约数。
    3. total_score -= delta
    4. 标记 x 已经不在架子上了。

这个顺序非常重要哦!添加时先计算贡献再更新状态,移除时先更新状态再计算贡献,这样逻辑最清晰,喵~

为了让查询过程跑得飞快,我们需要预处理一些东西:

  1. 线性筛O(N) 时间内筛出 1max(a_i) 的莫比乌斯函数 μ 和每个数的最小质因子 spf
  2. 预处理出 1max(a_i) 每个数的所有约数,方便更新 counts 数组。

这样,每次查询的复杂度就只和 a[x] 的约数个数和质因子个数有关了,对于 5*10^5 以内的数据,这是非常快的!

题解代码

这是我为主人的小窝准备好的 C++ 代码,加了一些注释方便理解,喵~

cpp
#include <iostream>
#include <vector>
#include <numeric>
#include <algorithm>

// 喵~ 加速输入输出,让程序跑得更快!
void fast_io() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);
}

const int MAXA = 500005; // 泡沫高度的最大值
const int MAXN = 200005; // 啤酒种类的最大值

// --- 问题数据 ---
int n, q;
int a[MAXN]; // a[i] 是第 i 种啤酒的泡沫高度
bool on_shelf[MAXN]; // on_shelf[i] 标记第 i 种啤酒是否在架子上
long long counts[MAXA]; // counts[d] 记录架子上泡沫高度是 d 的倍数的啤酒数量
long long total_score = 0; // 当前总分

// --- 预处理数据 ---
int spf[MAXA];  // spf[i] 存储 i 的最小质因子 (Smallest Prime Factor)
int mu[MAXA];   // mu[i] 存储 i 的莫比乌斯函数值
std::vector<int> all_divisors[MAXA]; // all_divisors[i] 存储 i 的所有约数

// --- 查询时用的临时变量,避免重复分配内存 ---
std::vector<int> p_factors_cache; // 存质因子
std::vector<int> sq_free_divs_cache; // 存无平方因子约数

// --- 预处理函数 ---

// 线性筛,用来计算 spf 和 mu
void linear_sieve() {
    std::vector<int> primes;
    std::iota(spf, spf + MAXA, 0); // 初始化 spf[i] = i
    mu[1] = 1;
    for (int i = 2; i < MAXA; ++i) {
        if (spf[i] == i) { // i 是质数
            mu[i] = -1;
            primes.push_back(i);
        }
        for (int p : primes) {
            if (p > spf[i] || 1LL * i * p >= MAXA) break;
            spf[i * p] = p;
            if (p == spf[i]) { // p 是 i 的最小质因子
                mu[i * p] = 0;
                break;
            } else { // p 比 i 的最小质因子还小
                mu[i * p] = -mu[i];
            }
        }
    }
}

// 预处理每个数的所有约数
void precompute_all_divisors() {
    for (int i = 1; i < MAXA; ++i) {
        for (int j = i; j < MAXA; j += i) {
            all_divisors[j].push_back(i);
        }
    }
}

// --- 查询辅助函数 ---

// 根据一个数 v,找出它的所有无平方因子约数
void get_sq_free_divs(int v) {
    p_factors_cache.clear();
    int temp = v;
    // 1. 找出所有质因子
    while (temp > 1) {
        int p = spf[temp];
        p_factors_cache.push_back(p);
        while (temp % p == 0) {
            temp /= p;
        }
    }

    // 2. 用质因子组合出所有无平方因子约数
    sq_free_divs_cache.clear();
    int k = p_factors_cache.size();
    sq_free_divs_cache.reserve(1 << k); // 预留空间
    for (int i = 0; i < (1 << k); ++i) {
        int d = 1;
        for (int j = 0; j < k; ++j) {
            if ((i >> j) & 1) { // 用二进制位来决定是否乘上这个质因子
                d *= p_factors_cache[j];
            }
        }
        sq_free_divs_cache.push_back(d);
    }
}

int main() {
    fast_io();

    // 预处理,喵~
    linear_sieve();
    precompute_all_divisors();

    std::cin >> n >> q;
    for (int i = 1; i <= n; ++i) {
        std::cin >> a[i];
    }

    for (int k = 0; k < q; ++k) {
        int x;
        std::cin >> x;
        int v = a[x];

        get_sq_free_divs(v); // 找到 v 的无平方因子约数
        long long delta = 0;

        if (on_shelf[x]) {
            // --- 移除啤酒 ---
            // 1. 先更新状态:把 v 的贡献从 counts 中移除
            for (int d : all_divisors[v]) {
                counts[d]--;
            }
            on_shelf[x] = false;

            // 2. 计算 v 曾经的贡献值 delta
            for (int d : sq_free_divs_cache) {
                delta += mu[d] * counts[d];
            }
            // 3. 从总分中减去
            total_score -= delta;

        } else {
            // --- 添加啤酒 ---
            // 1. 先计算 v 能带来的新贡献 delta
            for (int d : sq_free_divs_cache) {
                delta += mu[d] * counts[d];
            }
            // 2. 加到总分里
            total_score += delta;

            // 3. 再更新状态:把 v 的信息加入 counts
            for (int d : all_divisors[v]) {
                counts[d]++;
            }
            on_shelf[x] = true;
        }
        std::cout << total_score << "\n";
    }

    return 0;
}

相关知识点介绍

为了让主人彻底明白,我再来啰嗦几句相关的知识点,喵~

1. 莫比乌斯函数 (Möbius Function)

莫比乌斯函数 μ(n) 是数论中一个非常重要的函数,它的定义是这样的:

  • 如果 n=1μ(1) = 1
  • 如果 n 有一个平方因子(即能被某个 p^2 整除,p是质数),那么 μ(n) = 0
  • 如果 nk 个不同质数的乘积(即无平方因子),那么 μ(n) = (-1)^k

它最神奇的性质是:Σ_{d|n} μ(d) = [n=1] 这个式子的意思是,把 n 的所有约数 dμ(d) 值加起来,当 n=1 时结果是 1,当 n>1 时结果是 0。这个性质是莫比乌斯反演的基础,也是我们容斥计数的利器!

2. 容斥原理 (Principle of Inclusion-Exclusion)

这个原理主人肯定很熟悉啦!比如想求能被2或3整除的数的个数,就是 (被2整除的数) + (被3整除的数) - (被2和3同时整除的数)

当条件变多时,这个原理会变得复杂,但规律是“奇加偶减”。莫比乌斯函数 μ(d) 的正负值 (1, -1, 0) 正好对应了容斥原理中的加、减和不考虑(因为有平方因子的情况在容斥中会被重复计算抵消掉)。所以我们才能用 Σ μ(d) * C(d) 这样一个简洁的式子来完成复杂的容斥计算。

3. 线性筛 (Linear Sieve)

线性筛是一种能在 O(N) 的优秀时间内,预处理出 1N 的所有质数、每个数的最小质因子、欧拉函数、莫比乌斯函数等积性函数的方法。它的核心思想是保证每个合数只被其最小的质因子筛掉一次,从而达到线性的时间复杂度。对于需要大量数论函数值的题目,线性筛是必不可少的预处理神技,喵!

好啦,这次的讲解就到这里啦!希望主人能够完全理解。如果还有不明白的地方,随时可以再来问我哦,喵~ >ω<

Released under the MIT License.