Skip to content

BinaryVector - 题解

标签与难度

标签: 概率论, 线性代数, 数论, 模运算, 快速幂, 递推, 前缀和 难度: 1900

题目大意喵~

你好呀,指挥官!这道题是关于线性代数的,听起来是不是很酷,喵~?

题目是这样的:我们有一个 nn 维的二进制向量空间,也就是说向量的每个分量都只能是 0 或者 1。Roundgod酱每天都会从这 2n2^n 个可能的向量中,等概率地随机选择一个。

她想知道,在接下来的 nn 天里,她生成的这 nn 个向量恰好是线性无关的概率是多少?我们把这个关于 nn 的概率值记作 fnf_n

但是呢,Wcy酱觉得只求一个 fnf_n 太简单啦,她想知道的是 f1f2fNf_1 \oplus f_2 \oplus \dots \oplus f_N 的值,这里的 \oplus 是按位异或(XOR)运算哦。

所有的计算结果都需要对 109+710^9 + 7 取模。我们要处理多组查询,每次给定一个 NN,输出对应的异或和。准备好了吗?让我们一起把这道题拿下吧,喵!

解题思路分析

这道题看起来有点吓人,又是概率又是线性代数的,但别怕,让我一步步带你拆解它,很快你就会发现它的可爱之处啦,喵~

第一步:求出概率 fnf_n

首先,我们的核心任务是搞清楚 fnf_n 到底是什么。fnf_n 是在 nn 天内生成的 nnnn 维向量 {v1,v2,,vn}\{v_1, v_2, \dots, v_n\} 线性无关的概率。

概率嘛,就是“有利情况数”除以“总情况数”。

  • 总情况数:我们每天从 2n2^n 个向量中随机选一个,连续选 nn 天。根据乘法原理,总共有 (2n)n(2^n)^n 种可能的向量序列。

  • 有利情况数:我们要计算有多少种向量序列 {v1,v2,,vn}\{v_1, v_2, \dots, v_n\} 是线性无关的。我们一天天来分析:

    • 第1天 (选择 v1v_1): 为了使最终的向量组线性无关,第一个向量 v1v_1 肯定不能是零向量。nn 维空间里有 2n2^n 个向量,只有一个是零向量。所以 v1v_12n12^n - 1 种选择。
    • 第2天 (选择 v2v_2): 为了让 {v1,v2}\{v_1, v_2\} 线性无关,v2v_2 不能被 v1v_1 线性表示。在二进制向量空间(也叫 GF(2)GF(2) 上的向量空间)里,向量的加法就是异或。v1v_1 能张成的空间 span({v1})span(\{v_1\}) 包含的向量是 {c1v1c1{0,1}}\{c_1v_1 | c_1 \in \{0, 1\}\},也就是 {0,v1}\{\vec{0}, v_1\} 这两个向量。所以,v2v_2 不能是这两个向量中的任意一个。它有 2n22^n - 2 种选择。
    • 第3天 (选择 v3v_3): 此时我们已经有了两个线性无关的向量 {v1,v2}\{v_1, v_2\}。它们张成的空间 span({v1,v2})span(\{v_1, v_2\}) 包含 {c1v1+c2v2c1,c2{0,1}}\{c_1v_1 + c_2v_2 | c_1, c_2 \in \{0, 1\}\},一共有 22=42^2 = 4 个不同的向量。为了让 {v1,v2,v3}\{v_1, v_2, v_3\} 线性无关,v3v_3 不能落在这个空间里。所以 v3v_32n42^n - 4 种选择。
    • ii 天 (选择 viv_i): 以此类推,当我们选择第 ii 个向量 viv_i 时,我们已经有了 i1i-1 个线性无关的向量 {v1,,vi1}\{v_1, \dots, v_{i-1}\}。它们张成了一个 i1i-1 维的子空间,里面有 2i12^{i-1} 个向量。viv_i 必须在这个子空间之外,所以有 2n2i12^n - 2^{i-1} 种选择。

    把这 nn 天的选择数乘起来,得到有利情况的总数是:

    (2n20)(2n21)(2n22)(2n2n1)=i=0n1(2n2i)(2^n - 2^0)(2^n - 2^1)(2^n - 2^2) \cdots (2^n - 2^{n-1}) = \prod_{i=0}^{n-1} (2^n - 2^i)

现在我们可以计算概率 fnf_n 了:

fn=有利情况数总情况数=i=0n1(2n2i)(2n)nf_n = \frac{\text{有利情况数}}{\text{总情况数}} = \frac{\prod_{i=0}^{n-1} (2^n - 2^i)}{(2^n)^n}

这个式子可以化简一下,把分母的 (2n)n(2^n)^n 分配到每一项:

fn=2n202n2n212n2n2n12n=i=0n1(12i2n)f_n = \frac{2^n - 2^0}{2^n} \cdot \frac{2^n - 2^1}{2^n} \cdots \frac{2^n - 2^{n-1}}{2^n} = \prod_{i=0}^{n-1} \left(1 - \frac{2^i}{2^n}\right)

再换个元,让 k=nik = n-i,当 ii00n1n-1 时,kk 就从 nn11。所以:

fn=k=1n(112k)f_n = \prod_{k=1}^{n} \left(1 - \frac{1}{2^k}\right)

哇!我们得到了一个非常漂亮的 fnf_n 表达式,喵~

第二步:寻找递推关系

有了 fnf_n 的表达式,我们来看看 fnf_nfn1f_{n-1} 之间有什么关系。

fn=(k=1n1(112k))(112n)f_n = \left( \prod_{k=1}^{n-1} \left(1 - \frac{1}{2^k}\right) \right) \cdot \left(1 - \frac{1}{2^n}\right)

看,括号里的那部分不就是 fn1f_{n-1} 吗?所以我们得到了一个简单的递推式:

fn=fn1(112n)f_n = f_{n-1} \cdot \left(1 - \frac{1}{2^n}\right)

其中,我们的基础是 f1=1121=12f_1 = 1 - \frac{1}{2^1} = \frac{1}{2}

第三步:预处理与前缀异或和

题目要求我们计算 SN=f1f2fNS_N = f_1 \oplus f_2 \oplus \dots \oplus f_N。由于查询次数 TT 可能很大,每次都从头计算 SNS_N 肯定会超时的说。最好的办法就是预处理

我们可以先用一个大数组,比如 prefix_xor_sums,把所有可能的 NN(题目范围最大到 21072 \cdot 10^7)对应的答案都算出来存好。

具体步骤是这样哒:

  1. 准备工作:

    • 计算模 109+710^9+7 下 2 的逆元。因为 109+710^9+7 是质数,根据费马小定理,ap2a1(modp)a^{p-2} \equiv a^{-1} \pmod p。所以 212109+72(mod109+7)2^{-1} \equiv 2^{10^9+7-2} \pmod{10^9+7}。我们可以用快速幂来算。
  2. 循环计算:

    • 我们从 n=1n=1 开始循环到最大值 21072 \cdot 10^7
    • 在循环中,我们维护几个变量:
      • current_fn:当前计算出的 fnf_n
      • inv_2_power_n:$ (2^n)^{-1} \pmod{10^9+7}。这个也可以递推,。这个也可以递推, (2^n)^{-1} = (2^{n-1})^{-1} \cdot 2^{-1} $。
      • running_xor_sum:当前的异或和 f1fnf_1 \oplus \dots \oplus f_n
    • 对于每个 nn
      • 根据递推式 fn=fn1(1(2n)1)f_n = f_{n-1} \cdot (1 - (2^n)^{-1}) 计算出 current_fn
      • 更新 running_xor_sum = running_xor_sum ^ current_fn
      • 将结果存入 prefix_xor_sums[n] = running_xor_sum
  3. 回答查询:

    • 预处理完成后,对于每个查询 NN,我们直接返回 prefix_xor_sums[N] 就好啦!这样每次查询都是 O(1)O(1) 的时间,非常快!

这样,我们就把一个复杂的概率问题转化成了一个简单的递推和预处理问题了,是不是很巧妙呢?喵~

代码实现

这是我根据上面的思路,用爪爪敲出来的全新代码哦,注释很详细,希望能帮到你,喵~

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

// 定义一个大大的常量作为模数
const long long MOD = 1e9 + 7;
// 题目给定的N的最大值
const int MAX_N = 20000000;

// 我们的答案数组,提前声明为全局变量或静态变量可以避免栈溢出
long long prefix_xor_sums[MAX_N + 5];

// 快速幂函数,用来计算 (base^exp) % mod
// a^b mod m,这是数论中的基本操作哦
long long power(long long base, long long exp) {
    long long res = 1;
    base %= MOD;
    while (exp > 0) {
        if (exp % 2 == 1) res = (res * base) % MOD;
        base = (base * base) % MOD;
        exp /= 2;
    }
    return res;
}

// 模块化逆元函数,根据费马小定理 a^(p-2) = a^(-1) (mod p)
long long modInverse(long long n) {
    return power(n, MOD - 2);
}

// 预处理函数,把所有可能的答案都算出来
void precompute() {
    // 首先计算2在模MOD下的逆元,这是我们递推的基础
    long long inv2 = modInverse(2);

    // 初始化n=1的情况
    long long current_fn = inv2; // f_1 = 1/2
    long long inv_2_power_n = inv2; // (2^1)^-1
    long long running_xor_sum = current_fn; // S_1 = f_1
    prefix_xor_sums[1] = running_xor_sum;

    // 从n=2开始循环到最大值
    for (int n = 2; n <= MAX_N; ++n) {
        // 更新 (2^n)^-1 = (2^(n-1))^-1 * 2^-1
        inv_2_power_n = (inv_2_power_n * inv2) % MOD;
        
        // 计算递推项 (1 - 1/2^n)
        long long term = (1 - inv_2_power_n + MOD) % MOD;
        
        // 计算 f_n = f_{n-1} * (1 - 1/2^n)
        current_fn = (current_fn * term) % MOD;
        
        // 更新前缀异或和 S_n = S_{n-1} ^ f_n
        running_xor_sum ^= current_fn;
        
        // 存储结果
        prefix_xor_sums[n] = running_xor_sum;
    }
}

int main() {
    // C++ iostream 加速,让输入输出更快一点,喵~
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    // 执行预处理
    precompute();

    int T;
    std::cin >> T;
    while (T--) {
        int N;
        std::cin >> N;
        // 直接输出预处理好的答案
        std::cout << prefix_xor_sums[N] << "\n";
    }

    return 0;
}

复杂度分析

  • 时间复杂度: O(MAX_N+T)O(\text{MAX\_N} + T) 我们的 precompute 函数运行一次,内部有一个从 1 到 MAX_N 的循环,所以预处理的时间复杂度是 O(MAX_N)O(\text{MAX\_N})。之后,我们处理 TT 个查询,每个查询只需要 O(1)O(1) 的时间来访问数组。所以总时间复杂度是 O(MAX_N+T)O(\text{MAX\_N} + T)。对于这道题的数据范围来说,是完全可以接受的。

  • 空间复杂度: O(MAX_N)O(\text{MAX\_N}) 我们使用了一个 prefix_xor_sums 数组来存储所有预计算的结果,其大小与 MAX_N 成正比,所以空间复杂度是 O(MAX_N)O(\text{MAX\_N})

知识点总结

这道题真是一次有趣的冒险,我们从中可以学到很多东西呢!

  1. 线性代数基础: 核心是理解在 GF(2)GF(2) 域上向量空间的线性无关张成空间 (span) 的概念。记住,kk 个线性无关的向量可以张成一个包含 2k2^k 个向量的子空间。
  2. 概率与组合计数: 解题的关键一步是正确地计算有利情况数,这需要我们运用组合计数的思想,一步步分析选择过程。
  3. 递推关系: 从复杂的乘积公式 k=1n(12k)\prod_{k=1}^{n} (1 - 2^{-k}) 中发现简单的递推关系 fn=fn1(12n)f_n = f_{n-1} \cdot (1 - 2^{-n}),是简化计算的法宝。
  4. 模运算与逆元: 在处理概率和分数时,模运算是必不可少的。当模数是质数时,使用费马小定理快速幂是求逆元的标准姿势。
  5. 预处理 (打表): 对于有多组查询且查询范围固定的问题,预处理是一种非常强大的思想。用一次性的计算换取后续的快速响应,"空间换时间"的经典应用。
  6. 前缀和思想: 虽然这里用的是异或,但思想和前缀和是一样的,即通过递推快速得到一个序列到某个位置的累计结果。

希望这篇题解能让你对这些知识点有更深的理解,喵~ 如果还有不懂的地方,随时可以再来问我哦!我们下次解题再见啦!