2026-07-31:好子序列查询。用go语言,有一个长度为 n 的整数数组 nums 和一个给定的整数 p。定义一种“好子序列”:从数组中选取至少一个元素、但

9 阅读11分钟

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。

算法总体思路

题目要求动态维护数组,在每次单点修改后判断是否存在一个好子序列
好子序列必须满足三个条件:

  1. 非空;
  2. 至少舍弃一个原数组元素(即不能取全部);
  3. 选取的元素保持原顺序(因为只关心值,顺序不影响 gcd,所以实际只需要考虑哪些元素被选);
  4. 所有选取元素的 最大公约数(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 以及个数。


预处理阶段

  1. 输入数组 nums,长度 n,给定整数 p
  2. 创建线段树 SegTree,内部包含:
    • tree[]:存储区间内所有 p 的倍数的 gcd(非 p 的倍数用 0 表示,因为 gcd(0, x) = x,不影响计算);
    • p:模数;
    • cnt:当前数组中 p 的倍数的总个数。
  3. 遍历 nums 的每个位置 i
    • nums[i] % p == 0,调用线段树的单点更新函数,将该位置插入值 nums[i]
    • 每次更新会维护叶子节点、更新内部节点的 gcd,并同步增加 cnt
  4. 初始状态下,线段树根节点 tree[1] 即为当前所有 p 的倍数的 gcd。

每次查询更新处理步骤

对于每个查询 (idx, val),执行以下操作:

  1. 更新数组值:将 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(否则)。
    • 回溯时重新计算内部节点的 gcd:tree[o] = gcd(tree[o*2], tree[o*2+1])
  2. 此时 nums 已更新,cnt 正确,根节点 tree[1] 是当前所有 p 的倍数的 gcd。

  3. 判断是否存在好子序列

    • 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;否则不加。
  4. 所有查询处理完毕后,返回累计的答案个数。


时间复杂度

  • 线段树建树:初始通过 n 次单点更新建树,每次更新 O(log n),故 O(n log n)。
  • 每次查询:一次单点更新 O(log n);判断和可能的暴力检查:
    • n <= 7deleteOneCheck 需要 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;
}

在这里插入图片描述