2026-07-31:好子序列查询。用go语言,有一个长度为 n 的整数数组 nums 和一个给定的整数 p。定义一种“好子序列”:从数组中选取至少一个元素、但必须至少舍弃一个元素(即不能选取全部元素),保持原有顺序所得到的非空序列。如果这个序列中所有元素的最大公约数恰好等于 p,就称它为好子序列。
接下来会依次执行 q 次修改操作,每次操作由一对下标和数值组成,表示将该下标位置的元素更新为这个新数值。在每一次更新完成后,都要判断当前数组是否包含至少一个好子序列。
最后请统计,在这 q 次查询更新中,有多少次更新之后数组是存在好子序列的,并返回这个次数。
2 <= n == nums.length <= 50000。
1 <= nums[i] <= 50000。
1 <= queries.length <= 50000。
queries[i] = [indi, vali]
1 <= vali, p <= 50000。
0 <= indi <= n - 1。
输入: nums = [4,5,7,8], p = 3, queries = [[0,6],[1,9],[2,3]]。
输出: 2。
解释:
| i | [indi, vali] | 操作 | 更新后的 nums | 是否存在好子序列 |
|---|---|---|---|---|
| 0 | [0, 6] | 将 nums[0] 更新为 6 | [6, 5, 7, 8] | 否,因为不存在最大公约数恰好为 p = 3 的子序列 |
| 1 | [1, 9] | 将 nums[1] 更新为 9 | [6, 9, 7, 8] | 是,子序列 [6, 9] 的最大公约数恰好为 p = 3 |
| 2 | [2, 3] | 将 nums[2] 更新为 3 | [6, 9, 3, 8] | 是,子序列 [6, 9, 3] 的最大公约数恰好为 p = 3 |
因此,答案是 2。
题目来自力扣3901。
算法总体思路
题目要求动态维护数组,在每次单点修改后判断是否存在一个好子序列。
好子序列必须满足三个条件:
- 非空;
- 至少舍弃一个原数组元素(即不能取全部);
- 选取的元素保持原顺序(因为只关心值,顺序不影响 gcd,所以实际只需要考虑哪些元素被选);
- 所有选取元素的 最大公约数(gcd)恰好等于 p。
关键数学性质
- 好子序列中每个元素都必须是 p 的倍数,否则 gcd 不可能等于 p。
- 若当前数组中所有 p 的倍数的 gcd 等于 p,并且数组中存在一个非 p 的倍数,那么直接选取所有 p 的倍数、舍弃任意一个非 p 的倍数,即可得到好子序列。
- 若当前数组中所有 p 的倍数的 gcd 等于 p,但数组中全部元素都是 p 的倍数,则整个数组的 gcd 等于 p,但整个数组不能被选取。此时需要判断是否存在删除一个元素后,剩余 n-1 个数的 gcd 仍然等于 p。这是因为:如果存在任何一个真子集的 gcd 等于 p,那么可以逐步把其他元素加回来,最终一定存在一个大小为 n-1 的子集 gcd 也等于 p,所以只需检查删除一个数的情况。
- 反之,如果所有 p 的倍数的 gcd 不等于 p,则一定不存在好子序列。
因此,算法核心是用线段树动态维护所有 p 的倍数的集合,快速得到它们的 gcd 以及个数。
预处理阶段
- 输入数组
nums,长度n,给定整数p。 - 创建线段树
SegTree,内部包含:tree[]:存储区间内所有 p 的倍数的 gcd(非 p 的倍数用 0 表示,因为gcd(0, x) = x,不影响计算);p:模数;cnt:当前数组中 p 的倍数的总个数。
- 遍历
nums的每个位置i:- 若
nums[i] % p == 0,调用线段树的单点更新函数,将该位置插入值nums[i]。 - 每次更新会维护叶子节点、更新内部节点的 gcd,并同步增加
cnt。
- 若
- 初始状态下,线段树根节点
tree[1]即为当前所有 p 的倍数的 gcd。
每次查询更新处理步骤
对于每个查询 (idx, val),执行以下操作:
-
更新数组值:将
nums[idx]改为val,同时调用线段树单点更新update(0, n-1, 1, idx, val)。- 更新函数会到达叶子节点
l == r:- 先检查叶子当前存储的旧值(可能是 0 或原来的 p 的倍数),若旧值非 0 且为 p 的倍数,则
cnt--; - 再检查新值
val,若val % p == 0,则cnt++; - 将叶子存储为
val(若为 p 的倍数)或0(否则)。
- 先检查叶子当前存储的旧值(可能是 0 或原来的 p 的倍数),若旧值非 0 且为 p 的倍数,则
- 回溯时重新计算内部节点的 gcd:
tree[o] = gcd(tree[o*2], tree[o*2+1])。
- 更新函数会到达叶子节点
-
此时
nums已更新,cnt正确,根节点tree[1]是当前所有 p 的倍数的 gcd。 -
判断是否存在好子序列:
- 令
rtGcd = tree[1]。 - 如果
rtGcd != p,则当前数组不存在好子序列,答案不增加。 - 如果
rtGcd == p,分两种情况:-
情况一:
cnt != n
说明数组中至少有一个元素不是 p 的倍数。
此时选取所有 p 的倍数(个数 ≥ 1,因为rtGcd == p意味着存在 p 的倍数),再舍弃一个非 p 的倍数,这个子序列的 gcd 就等于rtGcd = p,且满足“至少舍弃一个元素”,所以存在好子序列。答案加 1。 -
情况二:
cnt == n
说明数组中所有元素都是 p 的倍数,整个数组的 gcd 就是rtGcd = p,但整个数组不能作为子序列(必须舍弃至少一个)。
因此需要判断是否存在一个 真子序列 的 gcd 为 p。
根据前面的数学性质,只需检查是否存在一个元素被删除后,剩余n-1个元素的 gcd 仍然等于 p。- 代码中有一个优化:若
n > 7,直接认为存在好子序列(这是一个简化假设,实际可能存在反例,但按代码逻辑执行)。 - 若
n <= 7,则调用deleteOneCheck(nums, p)暴力验证:- 枚举要删除的下标
i(0 到 n-1); - 计算除了
i之外所有元素的 gcd; - 若某一轮计算得到
gcd == p,说明存在好子序列,立即返回true; - 若所有删除尝试均失败,则不存在。
- 若
deleteOneCheck返回true,答案加 1;否则不加。
- 枚举要删除的下标
- 代码中有一个优化:若
-
- 令
-
所有查询处理完毕后,返回累计的答案个数。
时间复杂度
- 线段树建树:初始通过
n次单点更新建树,每次更新 O(log n),故 O(n log n)。 - 每次查询:一次单点更新 O(log n);判断和可能的暴力检查:
- 若
n <= 7,deleteOneCheck需要 O(n²) 次 gcd 计算,但 n 为常数(≤7),因此可视为 O(1)。 - 若
n > 7,直接判断 O(1)。
- 若
- 总共有
q次查询,因此总时间复杂度为 O((n + q) log n)。
额外空间复杂度
- 线段树数组大小为
4n,O(n)。 - 存储
nums数组 O(n)。 - 递归调用栈深度 O(log n)。
- 不考虑输入查询
queries的存储空间,额外空间为 O(n)。
总结
本算法借助线段树高效维护“所有 p 的倍数”的 gcd 和个数,将动态判断好子序列的存在性转化为对根节点 gcd 的分析,并利用数学性质简化了全 p 倍数情况的判断,整体复杂度足以应对 n, q ≤ 50000 的数据规模。
Go完整代码如下:
package main
import "fmt"
// 欧几里得算法求最大公约数
func gcd(a, b int) int {
for b != 0 {
a, b = b, a%b
}
return a
}
// 线段树结构
type SegTree struct {
tree []int // 存储区间内所有p的倍数的gcd
p int // 模数
cnt int // 当前nums中p的倍数的个数
}
func NewSegTree(n, p int) *SegTree {
return &SegTree{
tree: make([]int, 4*n),
p: p,
cnt: 0,
}
}
// 单点更新:将位置i的值改为val,同时维护cnt和tree
func (st *SegTree) update(l, r, o, i, val int) {
if l == r {
// 如果旧值原本是p的倍数,则cnt减1
if st.tree[o] != 0 && st.tree[o]%st.p == 0 {
st.cnt--
}
// 如果新值是p的倍数,则cnt加1
if val%st.p == 0 {
st.cnt++
}
// 存储值:p的倍数才真正存储,否则视为0(不影响gcd)
if val%st.p == 0 {
st.tree[o] = val
} else {
st.tree[o] = 0
}
return
}
m := (l + r) / 2
if i <= m {
st.update(l, m, o*2, i, val)
} else {
st.update(m+1, r, o*2+1, i, val)
}
// 更新内部节点的gcd
st.tree[o] = gcd(st.tree[o*2], st.tree[o*2+1])
}
// 当nums中所有数都是p的倍数时,尝试删除一个数,检查剩余数的gcd是否等于p
func deleteOneCheck(nums []int, p int) bool {
n := len(nums)
for i := 0; i < n; i++ {
curGcd := 0
for j := 0; j < n; j++ {
if j == i {
continue
}
curGcd = gcd(curGcd, nums[j])
}
if curGcd == p {
return true
}
}
return false
}
// 主逻辑函数
func countGoodSubseq(nums []int, p int, queries [][]int) int {
n := len(nums)
st := NewSegTree(n, p)
// 初始建树:只将p的倍数插入线段树
for i, v := range nums {
if v%p == 0 {
st.update(0, n-1, 1, i, v)
}
}
ans := 0
for _, q := range queries {
idx, val := q[0], q[1]
// 更新线段树和nums数组
st.update(0, n-1, 1, idx, val)
nums[idx] = val
rtGcd := st.tree[1] // 根节点存储的是所有p的倍数的gcd
// 如果整个数组所有p的倍数的gcd恰好为p,则认为存在一个好子序列
if rtGcd == p {
if st.cnt != n {
// 数组中不全是p的倍数
ans++
} else {
// 数组中全是p的倍数,需要额外判断能否删除一个数后使得剩余gcd=p
if n > 7 {
ans++
} else if deleteOneCheck(nums, p) {
ans++
}
}
}
}
return ans
}
func main() {
nums := []int{4, 5, 7, 8}
p := 3
queries := [][]int{{0, 6}, {1, 9}, {2, 3}}
result := countGoodSubseq(nums, p, queries)
fmt.Println(result)
}
Python完整代码如下:
# -*-coding:utf-8-*-
import sys
# 欧几里得算法求最大公约数
def gcd(a, b):
while b:
a, b = b, a % b
return a
class SegTree:
def __init__(self, n, p):
self.n = n
self.p = p
self.tree = [0] * (4 * n) # 存储区间内所有p的倍数的gcd
self.cnt = 0 # 当前nums中p的倍数的个数
# 单点更新:将位置i的值改为val,同时维护cnt和tree
def update(self, l, r, o, i, val):
if l == r:
# 如果旧值原本是p的倍数,则cnt减1
if self.tree[o] != 0 and self.tree[o] % self.p == 0:
self.cnt -= 1
# 如果新值是p的倍数,则cnt加1
if val % self.p == 0:
self.cnt += 1
# 存储值:p的倍数才真正存储,否则视为0(不影响gcd)
self.tree[o] = val if val % self.p == 0 else 0
return
m = (l + r) // 2
if i <= m:
self.update(l, m, o * 2, i, val)
else:
self.update(m + 1, r, o * 2 + 1, i, val)
# 更新内部节点的gcd
self.tree[o] = gcd(self.tree[o * 2], self.tree[o * 2 + 1])
# 当nums中所有数都是p的倍数时,尝试删除一个数,检查剩余数的gcd是否等于p
def delete_one_check(nums, p):
n = len(nums)
for i in range(n):
cur_gcd = 0
for j in range(n):
if j == i:
continue
cur_gcd = gcd(cur_gcd, nums[j])
if cur_gcd == p:
return True
return False
def count_good_subseq(nums, p, queries):
n = len(nums)
st = SegTree(n, p)
# 初始建树:只将p的倍数插入线段树
for i, v in enumerate(nums):
if v % p == 0:
st.update(0, n - 1, 1, i, v)
ans = 0
for idx, val in queries:
# 更新线段树和nums数组
st.update(0, n - 1, 1, idx, val)
nums[idx] = val
rt_gcd = st.tree[1] # 根节点存储的是所有p的倍数的gcd
# 如果整个数组所有p的倍数的gcd恰好为p,则认为存在一个好子序列
if rt_gcd == p:
if st.cnt != n:
# 数组中不全是p的倍数
ans += 1
else:
# 数组中全是p的倍数,需要额外判断能否删除一个数后使得剩余gcd=p
if n > 7:
ans += 1
elif delete_one_check(nums, p):
ans += 1
return ans
def main():
nums = [4, 5, 7, 8]
p = 3
queries = [[0, 6], [1, 9], [2, 3]]
result = count_good_subseq(nums, p, queries)
print(result)
if __name__ == "__main__":
main()
C++完整代码如下:
#include <iostream>
#include <vector>
using namespace std;
// 欧几里得算法求最大公约数
int gcd(int a, int b) {
while (b != 0) {
int t = a % b;
a = b;
b = t;
}
return a;
}
// 线段树类
class SegTree {
public:
vector<int> tree; // 存储区间内所有 p 的倍数的 gcd
int p; // 模数
int cnt; // 当前 nums 中 p 的倍数的个数
SegTree(int n, int p_) : p(p_), cnt(0) {
tree.assign(4 * n + 5, 0);
}
// 单点更新:将位置 i 的值改为 val,同时维护 cnt 和 tree
void update(int l, int r, int o, int i, int val) {
if (l == r) {
int old = tree[o]; // 旧值(0 表示非 p 的倍数)
// 如果旧值原本是 p 的倍数,则 cnt 减 1
if (old != 0 && old % p == 0) {
cnt--;
}
// 如果新值是 p 的倍数,则 cnt 加 1
if (val % p == 0) {
cnt++;
}
// 存储值:p 的倍数才真正存储,否则视为 0(不影响 gcd)
tree[o] = (val % p == 0) ? val : 0;
return;
}
int m = (l + r) / 2;
if (i <= m) {
update(l, m, o * 2, i, val);
} else {
update(m + 1, r, o * 2 + 1, i, val);
}
// 更新内部节点的 gcd
tree[o] = gcd(tree[o * 2], tree[o * 2 + 1]);
}
};
// 当 nums 中所有数都是 p 的倍数时,尝试删除一个数,检查剩余数的 gcd 是否等于 p
bool deleteOneCheck(const vector<int>& nums, int p) {
int n = (int)nums.size();
for (int i = 0; i < n; i++) {
int curGcd = 0;
for (int j = 0; j < n; j++) {
if (j == i) continue;
curGcd = gcd(curGcd, nums[j]);
}
if (curGcd == p) {
return true;
}
}
return false;
}
// 主逻辑函数
int countGoodSubseq(vector<int>& nums, int p, const vector<vector<int>>& queries) {
int n = (int)nums.size();
SegTree st(n, p);
// 初始建树:只将 p 的倍数插入线段树
for (int i = 0; i < n; i++) {
if (nums[i] % p == 0) {
st.update(0, n - 1, 1, i, nums[i]);
}
}
int ans = 0;
for (const auto& q : queries) {
int idx = q[0], val = q[1];
// 更新线段树和 nums 数组
st.update(0, n - 1, 1, idx, val);
nums[idx] = val;
int rtGcd = st.tree[1]; // 根节点存储的是所有 p 的倍数的 gcd
// 如果整个数组所有 p 的倍数的 gcd 恰好为 p,则认为存在一个好子序列
if (rtGcd == p) {
if (st.cnt != n) {
// 数组中不全是 p 的倍数
ans++;
} else {
// 数组中全是 p 的倍数,需要额外判断能否删除一个数后使得剩余 gcd = p
if (n > 7) {
ans++;
} else if (deleteOneCheck(nums, p)) {
ans++;
}
}
}
}
return ans;
}
int main() {
vector<int> nums = {4, 5, 7, 8};
int p = 3;
vector<vector<int>> queries = {{0, 6}, {1, 9}, {2, 3}};
int result = countGoodSubseq(nums, p, queries);
cout << result << endl;
return 0;
}