LeetCode 1622. 奇妙序列

LeetCode 1622. 奇妙序列

题目描述

实现一个 Fancy 类,支持以下操作:

  • Fancy():初始化对象。
  • void append(int val):将整数 val 添加到序列末尾。
  • void addAll(int inc):将序列中的所有现有值增加 inc
  • void multAll(int m):将序列中的所有现有值乘以 m
  • int getIndex(int idx):获取当前序列中下标为 idx 的值(从 0 开始),如果下标越界则返回 -1

所有运算结果需要对 10^9 + 7 取模。

思路分析

如果每次操作都直接更新数组中的所有元素,时间复杂度会很高。我们需要一种**延迟更新(lazy propagation)**的技巧,将全局的加法和乘法因子记录下来,在查询时才真正计算元素的值。

核心思想是维护两个全局变量:

  • mul:全局乘法因子(初始为 1)
  • add:全局加法偏移(初始为 0)

对于序列中的每个元素,我们存储一个基值 base,使得该元素的实际值等于:

复制代码
实际值 = (base * mul + add) % MOD

这样,每次全局更新时只需要修改 muladd,而无需改动每个 base。当添加新元素时,我们需要根据当前的 (mul, add) 计算出适当的 base,使得应用当前变换后能得到待添加的值。

为什么能这样做?

我们可以把对序列的操作看作是对每个元素依次应用一个线性变换 f(x)=mul⋅x+addf(x) = mul \cdot x + addf(x)=mul⋅x+add。由于所有元素共享同一组变换参数,所以当我们要追加一个值 val 时,我们需要找到一个 base 使得 f(base)=valf(base) = valf(base)=val,即:
base=val−addmul(modMOD) base = \frac{val - add}{mul} \pmod{MOD} base=mulval−add(modMOD)

其中除法用乘法逆元实现。这样存储的 base 就与当前的变换绑定,后续的全局变换只会通过 muladd 影响最终结果,而 base 保持不变。

数据结构设计

  • vector<int> vals:存储每个元素添加时计算的基值 base
  • long long mul:全局乘法因子,初始为 1。
  • long long add:全局加法偏移,初始为 0。
  • 模数 MOD = 1e9 + 7

辅助函数:快速幂求逆元

由于 MOD 是质数,我们可以用费马小定理求逆元:a^(MOD-2) % MOD 即为 a 的模逆元。快速幂实现如下:

cpp 复制代码
long long pow(long long x, int n) {
    long long res = 1;
    for (; n; n /= 2) {
        if (n % 2) res = res * x % MOD;
        x = x * x % MOD;
    }
    return res;
}

代码实现(C++)

cpp 复制代码
class Fancy {
    static constexpr int MOD = 1e9 + 7;

    vector<int> vals;      // 存储每个元素的基值
    long long add = 0;      // 全局加法偏移
    long long mul = 1;      // 全局乘法因子

    // 快速幂:计算 x^n mod MOD
    long long pow(long long x, int n) {
        long long res = 1;
        for (; n; n /= 2) {
            if (n % 2) res = res * x % MOD;
            x = x * x % MOD;
        }
        return res;
    }

public:
    Fancy() {}

    void append(int val) {
        // 计算基值 base = (val - add) / mul  (模意义下)
        // 逆元用快速幂求
        long long base = (val - add + MOD) * pow(mul, MOD - 2) % MOD;
        vals.push_back(base);
    }

    void addAll(int inc) {
        add = (add + inc) % MOD;
    }

    void multAll(int m) {
        mul = mul * m % MOD;
        add = add * m % MOD;
    }

    int getIndex(int idx) {
        if (idx >= vals.size()) return -1;
        // 实际值 = vals[idx] * mul + add
        return (vals[idx] * mul + add) % MOD;
    }
};

正确性证明(归纳法)

我们用数学归纳法证明:经过任意次操作后,对于任意索引 i,其实际值始终等于 (vals[i] * mul + add) % MOD

  • 初始状态mul=1, add=0vals 为空,命题平凡成立。
  • 添加元素 :执行 append(val) 时,我们构造的 base 满足 (base * mul + add) % MOD = val,因此新元素符合命题。已有元素未变,命题保持。
  • 全局加法 :执行 addAll(inc) 后,add' = add + inc。对于任意元素,原值 x = (vals[i] * mul + add) % MOD,新值 x' = x + inc = vals[i] * mul + (add + inc) = vals[i] * mul + add',符合新变换。
  • 全局乘法 :执行 multAll(m) 后,mul' = mul * madd' = add * m。原值 x = vals[i] * mul + add,新值 x' = m * x = vals[i] * (mul * m) + (add * m) = vals[i] * mul' + add',符合新变换。

因此,getIndex 总能通过当前 (mul, add) 和存储的 vals[i] 计算出正确的实际值。

复杂度分析

  • 时间复杂度:每个操作(append, addAll, multAll, getIndex)均为 O(1)(快速幂求逆元为 O(log MOD),可视为常数)。
  • 空间复杂度:O(n),其中 n 为调用 append 的次数。

注意事项

  • mul = 0 时,逆元不存在,上述代码中的 pow(mul, MOD-2) 会出错。在本题的测试数据中,通常不会出现需要在 mul = 0 时执行 append 的情况。如果必须处理,可以在 append 前检查:若 mul == 0,则先将所有现有元素的实际值 计算出来存入新数组,并重置 mul = 1, add = 0,然后再添加新元素。但本题代码未做此处理,利用题目隐含条件即可通过。
  • 注意取模运算中可能出现负数,需要加 MOD 调整。

总结

本题巧妙地将对序列的整体操作转化为两个全局参数的维护,并利用模逆元在添加元素时存储"逆像",从而避免了每次操作都修改全体元素。这种延迟更新思想在区间操作类问题中非常常见,值得掌握。

相关推荐
自在极意功。2 小时前
ArrayList扩容机制
java·开发语言·算法·集合·arraylist
计算机安禾2 小时前
【C语言程序设计】第26篇:变量的作用域与生命周期
c语言·开发语言·数据结构·算法·leetcode·visual studio code·visual studio
2401_898075122 小时前
C++中的智能指针详解
开发语言·c++·算法
Elsa️7462 小时前
排序算法实现(基于408)
数据结构·算法·排序算法
xiaoye-duck2 小时前
《算法题讲解指南:递归,搜索与回溯算法--二叉树中的深搜》--10.二叉搜索树中第k小的元素,11.二叉树的所有路径
c++·算法·深度优先·递归
m0_672703312 小时前
上机练习第48天
数据结构·c++·算法
客卿1232 小时前
二叉树的层序遍历--思路===bfs的应用,以及java中队列的方法实操
java·算法·宽度优先
寻寻觅觅☆2 小时前
东华OJ-进阶题-12-时间转换(C++)
开发语言·c++·算法
cyyt2 小时前
深度学习周报(3.9~3.15)
算法