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
这样,每次全局更新时只需要修改 mul 和 add,而无需改动每个 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 就与当前的变换绑定,后续的全局变换只会通过 mul 和 add 影响最终结果,而 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=0,vals为空,命题平凡成立。 - 添加元素 :执行
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 * m,add' = 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调整。
总结
本题巧妙地将对序列的整体操作转化为两个全局参数的维护,并利用模逆元在添加元素时存储"逆像",从而避免了每次操作都修改全体元素。这种延迟更新思想在区间操作类问题中非常常见,值得掌握。