题解:AcWing 246 区间最大公约数
【题目来源】
AcWing:246. 区间最大公约数 - AcWing题库
【题目描述】
给定一个长度为 \(N\) 的数列 \(A\),以及 \(M\) 条指令,每条指令可能是以下两种之一:
C l r d,表示把 \(A[l],A[l+1],…,A[r]\) 都加上 \(d\)。Q l r,表示询问 \(A[l],A[l+1],…,A[r]\) 的最大公约数(\(GCD\))。
对于每个询问,输出一个整数表示答案。
【输入】
第一行两个整数 \(N,M\)。
第二行 \(N\) 个整数 \(A[i]\)。
接下来 \(M\) 行表示 \(M\) 条指令,每条指令的格式如题目描述所示。
【输出】
对于每个询问,输出一个整数表示答案。
每个答案占一行。
【输入样例】
5 5
1 3 5 7 9
Q 1 5
C 1 5 1
Q 1 5
C 3 3 6
Q 2 4
【输出样例】
1
2
4
【核心思想】
-
问题分析:给定长度为 \(N\) 的数列 \(A\),以及 \(M\) 条指令,支持区间 \([l, r]\) 加 \(d\) 和查询区间 \([l, r]\) 的最大公约数。关键在于如何在区间修改的同时高效维护区间 GCD。
-
算法选择:
- 差分数组(Difference Array):将区间加法转化为差分数组的单点修改,实现 \(O(1)\) 区间标记
- 线段树(Segment Tree):维护差分数组的区间 GCD,支持单点修改和区间 GCD 查询
- 树状数组(Fenwick Tree):维护差分数组的前缀和,用于快速查询 \(A[l]\) 的值
- 数学性质:利用 \(\gcd(a_1, a_2, ..., a_n) = \gcd(a_1, a_2-a_1, a_3-a_2, ..., a_n-a_{n-1})\) 的性质,将区间 GCD 转化为差分数组的 GCD
-
关键步骤:
- 初始化:读取 \(N\)(数组长度)、\(M\)(指令数)、\(A[1..N]\)(初始数组)
- 构建差分数组:\(b[i] = A[i] - A[i-1]\),其中 \(b[1] = A[1]\)
- 树状数组初始化:将差分数组 \(b[i]\) 加入树状数组,支持前缀和查询得到 \(A[i]\)
- 线段树建立:维护差分数组 \(b\) 的区间 GCD
- 处理查询指令
Q l r:- 通过树状数组前缀和查询 \(A[l] = \sum_{i=1}^{l} b[i]\)
- 通过线段树查询 \(\gcd(b[l+1], b[l+2], ..., b[r])\)
- 答案为 \(\gcd(A[l], \gcd(b[l+1..r]))\),即 \(\gcd(A[l..r])\)
- 若 \(l = r\),直接输出 \(|A[l]|\)
- 处理修改指令
C l r d:- 树状数组单点修改:
add(l, d)和add(r+1, -d) - 线段树单点更新:
update(l, d)和update(r+1, -d)(若 \(r+1 \leq n\))
- 树状数组单点修改:
-
时间/空间复杂度:
- 时间复杂度:\(O((N + M) \log N)\),线段树和树状数组操作均为 \(O(\log N)\)
- 空间复杂度:\(O(N)\),线段树(\(4N\))+ 树状数组(\(N\))+ 原数组
-
线段树维护 GCD 的核心思想:
- 差分降维:利用 \(\gcd(A[l..r]) = \gcd(A[l], \gcd(b[l+1..r]))\) 的数学性质,将区间修改下的区间 GCD 查询转化为差分数组的区间 GCD 查询
- 区间修改转单点修改:对原数组区间 \([l, r]\) 加 \(d\),等价于对差分数组 \(b[l] += d\)、\(b[r+1] -= d\),仅影响两个端点
- 线段树维护 GCD:父节点的 GCD 等于左右子节点 GCD 的 GCD,即 \(\gcd(l.v, r.v)\)
- 树状数组维护前缀和:通过前缀和还原原数组的值 \(A[l]\),用于与差分 GCD 合并得到最终答案
- 适用于区间加减与区间 GCD 查询类问题
【算法标签】
线段树
【代码详解】
#include <bits/stdc++.h>
using namespace std;
#define int long long // 将 int 定义为 long long,防止数值溢出
const int N = 500005; // 最大数组长度
int n, m; // n: 数组长度, m: 指令数
int w[N]; // 原数组
int tr1[N]; // 树状数组(维护差分数组的前缀和)
struct Node
{
int l, r, v; // 区间左右端点、区间差分数组的GCD值
} tr[N * 4]; // 线段树数组(4倍空间)
// 树状数组:lowbit运算,提取x的最低位1对应的2次幂数
int lowbit(int x)
{
return x & -x;
}
// 树状数组:单点修改(向后修),在位置x加上c
void add(int x, int c)
{
for (int i = x; i <= n; i += lowbit(i)) tr1[i] += c;
}
// 树状数组:前缀查询(向前查),查询[1,x]的和
int sum(int x)
{
int res = 0;
for (int i = x; i; i -= lowbit(i)) res += tr1[i];
return res;
}
// 辗转相除法求GCD
int gcd(int a, int b)
{
return b ? gcd(b, a % b) : a;
}
// 线段树:向上更新,父节点的v = 左右子节点v的GCD
void pushup(int u)
{
auto &root = tr[u], &l = tr[u << 1], &r = tr[u << 1 | 1];
tr[u].v = gcd(l.v, r.v);
}
// 线段树:建立,维护差分数组b[i] = w[i] - w[i-1]的GCD
void build(int u, int l, int r)
{
if (l == r) // 叶子节点
tr[u] = {l, r, w[r] - w[r - 1]}; // 差分数组的值
else
{
tr[u] = {l, r}; // 初始化当前节点的区间范围
int mid = l + r >> 1; // 取中点(等价于 (l+r)/2)
build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r); // 递归建立左右子树
pushup(u); // 向上更新当前节点
}
}
// 线段树:区间查询GCD
int query(int u, int l, int r)
{
if (tr[u].l >= l && tr[u].r <= r) // 当前节点区间完全包含在查询区间内
return tr[u].v;
int mid = tr[u].l + tr[u].r >> 1; // 取中点
int v = 0;
if (l <= mid) v = query(u << 1, l, r); // 左子树有贡献
if (r > mid) v = gcd(v, query(u << 1 | 1, l, r)); // 右子树有贡献,与左子树结果取GCD
return v;
}
// 线段树:单点修改,在差分数组位置pos加上d
void update(int u, int pos, int d)
{
if (tr[u].l == tr[u].r) // 叶子节点
{
tr[u].v += d; // 差分数组的值增加d
return;
}
int mid = tr[u].l + tr[u].r >> 1; // 取中点
if (pos <= mid) update(u << 1, pos, d); // 在左子树
else update(u << 1 | 1, pos, d); // 在右子树
pushup(u); // 修改后向上更新
}
signed main() // 使用 signed 替代 int,因为 #define int long long
{
cin >> n >> m; // 读入数组长度和指令数
for (int i = 1; i <= n; i++)
cin >> w[i]; // 读入原数组
// 初始化树状数组:将差分数组 b[i] = w[i] - w[i-1] 加入树状数组
for (int i = 1; i <= n; i++)
add(i, w[i] - w[i - 1]);
build(1, 1, n); // 建立线段树,维护差分数组,区间为[1, n]
char op;
int l, r, d;
while (m--) // 依次处理每条指令
{
cin >> op;
if (op == 'Q') // 查询指令
{
cin >> l >> r;
int left = sum(l); // 树状数组查询A[l]的值(差分数组前缀和)
int right = query(1, l + 1, r); // 线段树查询差分数组[l+1, r]的GCD
if (l == r) // 区间长度为1
cout << abs(left) << endl; // 直接输出A[l]的绝对值
else
// GCD(A[l..r]) = GCD(A[l], GCD(b[l+1], b[l+2], ..., b[r]))
// 其中 b[i] = A[i] - A[i-1] 为差分数组
cout << abs(gcd(left, right)) << endl;
}
else // 修改指令
{
cin >> l >> r >> d;
// 树状数组:区间加d,转化为差分数组的两个单点修改
add(l, d);
add(r + 1, -d);
// 线段树:同步更新差分数组的两个端点
update(1, l, d);
if (r + 1 <= n) // 注意线段树建到了n
update(1, r + 1, -d);
}
}
return 0;
}
【运行结果】
5 5
1 3 5 7 9
Q 1 5
1
C 1 5 1
Q 1 5
2
C 3 3 6
Q 2 4
4
浙公网安备 33010602011771号