Skip to content

generator 2 - 题解

标签与难度

标签: 数论, BSGS, 离散对数, 快速幂, 模运算, 数学, 几何级数求和 难度: 2100

题目大意喵~

主人你好呀,喵~ 这道题是这样的:

我们有一个很长很长的序列 x0,x1,x2,,xn1x_0, x_1, x_2, \dots, x_{n-1}。它的生成规则是:

  1. 第一个数 x0x_0 是给定的。
  2. 对于 i>0i > 0,后面的数由前一个数生成:xi=(axi1+b)(modp)x_i = (a \cdot x_{i-1} + b) \pmod p

这里的 a,b,pa, b, p 都是给定的整数。这个序列的长度 nn 可以非常非常大,达到 101810^{18} 那么多!

然后会有 QQ 次询问,每次询问会给一个整数 vv,需要我们找出最小的下标 ii (必须满足 0i<n0 \le i < n),使得 xi=vx_i = v。如果序列里根本没有 vv 这个数,就输出 -1 呐。

简单来说,就是在一个用线性同余法生成的超长序列里找一个数第一次出现的位置,喵~

解题思路分析

这么大的 nn (101810^{18}),想一个一个把序列算出来再去找,肯定是不行的啦,会超时到天荒地老!所以我们必须找到一个数学上的捷径,喵~

这个递推式 xi=(axi1+b)(modp)x_i = (a \cdot x_{i-1} + b) \pmod p 是一个很经典的一阶线性递推关系。我们可以尝试推导它的通项公式,这样就能直接计算 xix_i 而不用一步步递推了。

让我们来展开看看规律吧! x1=ax0+bx_1 = a \cdot x_0 + bx2=ax1+b=a(ax0+b)+b=a2x0+ab+bx_2 = a \cdot x_1 + b = a(a \cdot x_0 + b) + b = a^2 \cdot x_0 + ab + bx3=ax2+b=a(a2x0+ab+b)+b=a3x0+a2b+ab+bx_3 = a \cdot x_2 + b = a(a^2 \cdot x_0 + ab + b) + b = a^3 \cdot x_0 + a^2b + ab + b ... 发现了喵?xix_i 的表达式里, x0x_0 的系数是 aia^i,而 bb 后面跟着一串 aa 的幂次。

xi=aix0+b(ai1+ai2++a1+1)(modp)x_i = a^i \cdot x_0 + b(a^{i-1} + a^{i-2} + \dots + a^1 + 1) \pmod p

括号里是一个经典的等比数列求和!

接下来,我们就要根据 aa 的取值来分情况讨论了,呐。

情况一:a=0a = 0

如果 a=0a=0,那递推式就变成了 xi=b(modp)x_i = b \pmod p (对于 i>0i > 0)。 序列会是这个样子:x0,b,b,b,x_0, b, b, b, \dots

  • 如果要找的 v=x0v = x_0,那么最小的下标就是 0。
  • 如果要找的 v=bv = b,并且 n>1n > 1 (题目保证 n1n \ge 1, 所以只要 vx0v \ne x_0v=bv=b,最小下标就是 1)。
  • 其他情况都找不到,输出 -1。

情况二:a=1a = 1

如果 a=1a=1,等比数列求和就变成了 ii 个 1 相加,也就是 ii。 通项公式简化为:xi=(x0+ib)(modp)x_i = (x_0 + i \cdot b) \pmod p。 我们要找 vv,就是解一个关于 ii 的线性同余方程:

vx0+ib(modp)v \equiv x_0 + i \cdot b \pmod p

ibvx0(modp)i \cdot b \equiv v - x_0 \pmod p

如果 b≢0(modp)b \not\equiv 0 \pmod p,我们可以两边同乘以 bb 在模 pp 下的逆元 b1b^{-1} 来解出 ii

i(vx0)b1(modp)i \equiv (v - x_0) \cdot b^{-1} \pmod p

因为 pp 是素数(题目没说,但这是这类题的常见套路,参考代码也用了费马小定理求逆元,所以我们这么假设),我们可以用快速幂求出 b1bp2(modp)b^{-1} \equiv b^{p-2} \pmod p。 算出的最小非负整数解 ii 之后,别忘了检查一下是否 i<ni < n 哦! 如果 b0(modp)b \equiv 0 \pmod p,那 xi=x0x_i = x_0 恒成立。如果 v=x0v=x_0 就返回0,否则返回-1。

情况三:a>1a > 1 (最核心的情况!)

a>1a>1 时,等比数列的和是 ai1a1\frac{a^i - 1}{a-1}。 通项公式就是:

xi(aix0+bai1a1)(modp)x_i \equiv \left(a^i \cdot x_0 + b \cdot \frac{a^i - 1}{a-1}\right) \pmod p

为了解出 ii,我们需要把这个式子变成 ai某个值(modp)a^i \equiv \text{某个值} \pmod p 的形式。这需要一些代数变换,跟我一起来推吧! 为了方便,我们用 (a1)1(a-1)^{-1} 表示 a1a-1 在模 pp 下的逆元。

xiaix0+b(ai1)(a1)1(modp)x_i \equiv a^i \cdot x_0 + b(a^i - 1)(a-1)^{-1} \pmod p

xiaix0+aib(a1)1b(a1)1(modp)x_i \equiv a^i \cdot x_0 + a^i \cdot b(a-1)^{-1} - b(a-1)^{-1} \pmod p

把含 aia^i 的项合并:

xi+b(a1)1ai(x0+b(a1)1)(modp)x_i + b(a-1)^{-1} \equiv a^i \cdot (x_0 + b(a-1)^{-1}) \pmod p

两边再同乘以 (x0+b(a1)1)1(x_0 + b(a-1)^{-1})^{-1}

aixi+b(a1)1x0+b(a1)1(modp)a^i \equiv \frac{x_i + b(a-1)^{-1}}{x_0 + b(a-1)^{-1}} \pmod p

现在,我们把要查询的 vv 代入 xix_i,就得到了一个形如 aiY(modp)a^i \equiv Y \pmod p 的方程,其中 YY 是一个我们能算出来的具体数值。 这个问题叫做离散对数问题。就像解 2i=82^i = 8 得到 i=3i=3 一样,但我们是在模 pp 的世界里做这件事!

解决离散对数问题,最常用的算法就是大步小步算法 (Baby-Step Giant-Step, BSGS)。 它的思想是“相遇在中间”,有点像猫咪悄悄接近猎物,喵~ 我们想找的 ii 可能很大,但我们可以把它拆成两部分:i=kmji = k \cdot m - j,其中 mpm \approx \sqrt{p}

akmjY(modp)a^{k \cdot m - j} \equiv Y \pmod p

akmYaj(modp)a^{k \cdot m} \equiv Y \cdot a^j \pmod p

(am)kYaj(modp)(a^m)^k \equiv Y \cdot a^j \pmod p

现在,方程左边只跟 kk 有关(大步),右边只跟 jj 有关(小步)。 算法步骤如下:

  1. 小步 (Baby Steps): 计算所有 Yaj(modp)Y \cdot a^j \pmod p 的值,其中 jj00m1m-1。把这些结果 (value, j) 存到一个哈希表里。这就像猫咪迈着小碎步,探索附近的小范围。
  2. 大步 (Giant Steps): 计算 A=am(modp)A = a^m \pmod p。然后,我们计算 Ak(modp)A^k \pmod p 的值,其中 kk11mm。对于每个 AkA^k,我们去哈希表里查一下,看它是否出现过。这就像猫咪迈开大步,进行远距离跳跃。
  3. 相遇 (Meet): 如果在哈希表中找到了 AkA^k,假设它对应的小步是 jj,那么我们就找到了一个解:i=kmji = k \cdot m - j。因为我们是从小到大枚举 kk 的,所以第一个找到的解就是最小的正整数解。

这里有个小细节,如果 v=x0v=x_0,那么答案就是 0,可以直接特判。如果 x0+b(a1)10(modp)x_0 + b(a-1)^{-1} \equiv 0 \pmod p,那么分母为0,不能用上面的公式。这种情况意味着 xix_i 是一个常数序列,也需要特判一下。

把这些思路整合起来,我们就能写出完整的代码啦!

代码实现

这是我根据上面的思路,重新编写的清晰易懂的代码哦!希望能帮到你,喵~

cpp
#include <iostream>
#include <vector>
#include <cmath>
#include <unordered_map>

// 使用 long long 防止溢出
using ll = long long;

// 快速幂函数,用于计算 (base^exp) % mod
// 喵~ a^b mod p
ll power(ll base, ll exp, ll mod) {
    ll res = 1;
    base %= mod;
    while (exp > 0) {
        if (exp % 2 == 1) res = (__int128)res * base % mod;
        base = (__int128)base * base % mod;
        exp /= 2;
    }
    return res;
}

// 模逆元函数,利用费马小定理 a^(p-2) mod p
// 只有当 p 是素数时才可以用哦
ll mod_inverse(ll n, ll mod) {
    return power(n, mod - 2, mod);
}

// 大步小步算法 (BSGS),解决 a^i = y (mod p)
// 返回最小的非负整数 i
ll bsgs(ll a, ll y, ll p) {
    // 特判,如果 y=1,那么 i=0 就是一个解
    if (y % p == 1) return 0;

    std::unordered_map<ll, ll> baby_steps;
    ll m = sqrt(p) + 1;

    // Baby steps: 存下 a^j -> j
    ll current_a_j = 1;
    for (ll j = 0; j < m; ++j) {
        // 如果有重复的值,我们只存第一个遇到的(最小的 j)
        if (baby_steps.find(current_a_j) == baby_steps.end()) {
            baby_steps[current_a_j] = j;
        }
        current_a_j = (__int128)current_a_j * a % p;
    }

    // Giant steps: 计算 a^(-m)
    ll inv_a_m = mod_inverse(power(a, m, p), p);
    
    // 寻找相遇点
    ll giant_step_val = y;
    for (ll k = 0; k < m; ++k) {
        if (baby_steps.count(giant_step_val)) {
            ll j = baby_steps[giant_step_val];
            return k * m + j;
        }
        giant_step_val = (__int128)giant_step_val * inv_a_m % p;
    }
    
    // 没找到解
    return -1;
}

void solve() {
    ll n, x0, a, b, p;
    std::cin >> n >> x0 >> a >> b >> p;
    int q;
    std::cin >> q;

    while (q--) {
        ll v;
        std::cin >> v;

        ll ans = -1;

        if (v == x0) { // 特判:要找的值就是第一个值
            ans = 0;
        } else if (a == 0) { // 情况一:a = 0
            if (n > 1 && v == b) {
                ans = 1;
            }
        } else if (a == 1) { // 情况二:a = 1
            if (b == 0) { // 如果 b=0, 序列永远是 x0
                // v != x0 的情况,上面已经处理过了,所以这里无解
                ans = -1;
            } else {
                ll inv_b = mod_inverse(b, p);
                // i = (v - x0) / b
                ll i = (__int128)(v - x0 + p) % p * inv_b % p;
                if (i < n) {
                    ans = i;
                }
            }
        } else { // 情况三:a > 1 (核心情况)
            ll inv_a_minus_1 = mod_inverse(a - 1, p);
            ll term_b = (__int128)b * inv_a_minus_1 % p;
            
            // Y = (v + term_b) / (x0 + term_b)
            ll numerator = (v + term_b) % p;
            ll denominator = (x0 + term_b) % p;

            if (denominator == 0) {
                // 如果分母为0,说明 x_i 都是一个常数
                // 此时 x_i = -term_b
                // 这种情况应该不会发生,因为 v != x0
                // 如果 v == -term_b,所有 i>0 都满足,最小的是 1
                if (numerator == 0) { // 即 v == -term_b
                    if (n > 1) ans = 1;
                }
            } else {
                ll inv_denom = mod_inverse(denominator, p);
                ll y = (__int128)numerator * inv_denom % p;
                
                ll i = bsgs(a, y, p);
                if (i != -1 && i < n) {
                    ans = i;
                }
            }
        }
        std::cout << ans << std::endl;
    }
}

int main() {
    // 加速输入输出,喵~
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);
    int t;
    std::cin >> t;
    while (t--) {
        solve();
    }
    return 0;
}

复杂度分析

  • 时间复杂度: O(T(Qp))O(T \cdot (Q \cdot \sqrt{p})) 对于每组测试数据,我们都需要处理 QQ 个查询。对于每个查询(在 a>1a>1 的情况下),我们都需要运行一次BSGS算法。 BSGS算法的复杂度主要由两部分组成:

    1. 小步:构建哈希表,需要 O(p)O(\sqrt{p}) 次乘法和哈希表插入。
    2. 大步:查询哈希表,需要 O(p)O(\sqrt{p}) 次乘法和哈希表查找。 所以单次BSGS的复杂度是 O(p)O(\sqrt{p})。在我的代码实现中,每次查询都重新计算了BSGS,所以总时间复杂度是 O(TQp)O(T \cdot Q \cdot \sqrt{p})。 (一个可以做的优化是,对于同一组 a,pa, p,BSGS的小步部分可以预计算并复用,这样总时间可以优化到 O(T(p+Qp))O(T \cdot (\sqrt{p} + Q \cdot \sqrt{p})),但因为 QQ 不大,所以差别不大。)
  • 空间复杂度: O(p)O(\sqrt{p}) 主要的额外空间开销来自BSGS算法中的哈希表,它需要存储大约 m=pm = \sqrt{p} 个键值对。

知识点总结

这道题是数论知识的绝佳练习,喵~ 主要涉及了以下几个方面:

  1. 线性同余生成器: 理解 xi=(axi1+b)(modp)x_i = (a \cdot x_{i-1} + b) \pmod p 这种序列的性质。
  2. 通项公式推导: 将递推关系转化为封闭形式的数学表达式,这是解决问题的关键一步。里面用到了等比数列求和公式。
  3. 模运算: 熟练掌握模加、模乘、模逆元快速幂是基础。特别是利用费马小定理求逆元。
  4. 离散对数问题: 认识到 aiY(modp)a^i \equiv Y \pmod p 这种形式的问题,并知道它叫离散对数。
  5. BSGS (大步小步) 算法: 掌握解决离散对数问题的标准算法。核心思想是时空权衡相遇在中间,将指数搜索的复杂度从 O(p)O(p) 降到了 O(p)O(\sqrt{p})

希望这篇题解能帮助你更好地理解这个问题!继续加油哦,主人!喵~