1 条题解
-
0
一、题意理解
我们有一个长度为 的序列 , 次询问,每次询问区间 内任意两个不同位置的元素的最小绝对差: 如果区间长度小于 2,则输出 。
二、样例分析
样例:
n=4, a=[2,2,3,4]- 询问 [1,2]: 元素 {2,2},最小差 |2-2|=0
- 询问 [2,4]: 元素 {2,3,4},最小差 |3-2|=1
三、问题难点
- , ,不能对每个询问 暴力
- 需要高效查询任意区间的最小差值
四、关键观察
观察1:最小差值一定出现在排序后相邻的两个元素之间。
因此,如果我们能知道区间内排序后的相邻元素差值,最小值就是答案。
五、离线做法:Mo's algorithm
直接莫队复杂度 , 时 ,总操作约 ,加上 set/map 的 可能超时。
六、更优解法:只考虑有用的点对
我们只考虑那些可能成为某个区间最小差值的点对 。
对于位置 ,可能和它组成最小差值的 不会太多。
1. 寻找候选点对
对于每个 ,考虑它左边和右边值相近的一些位置。
具体做法:
- 对序列下标按 值排序
- 对于每个 ,在排序后的数组中找它附近的几个位置,计算差值
- 这样每个位置 只会产生 个候选点对
实际上,CF 765F 的标准做法是: 对于每个 ,考虑它和它左边的一些位置组成候选点对,这些位置满足某种单调性。
2. 候选点对的数量级
可以证明,对于每个 ,只需要考虑 个左边的位置( 是值域),就能覆盖所有可能的最小差值情况。
七、算法步骤(CF 765F 标准解法)
-
生成候选点对:
- 对数组下标按 值排序
- 对于每个 ,考虑它和它左边的一些位置 ,满足 在某个值域区间内,且这些位置是“可能成为最小差值”的
- 具体实现可以用值域分段或二分查找
-
离线处理询问:
- 将询问按右端点 排序
- 用线段树维护每个左端点 的答案
- 当右指针 从 1 移到 时,加入所有右端点为 的候选点对 ,更新线段树在 上的最小值
-
回答询问:
- 对于右端点 的询问,在线段树上查询 的最小值
八、候选点对生成细节
对于位置 ,我们想找到左边的一些 ,使得 可能成为某个区间的最小差值。
方法:
- 维护一个候选集合
- 从右到左扫描,对于每个 ,考虑 中值在 内的元素,其中 是当前已知的最小差值
- 更新 并加入 到
但这样复杂度仍高。优化:只考虑 中在排序后数组里 附近的一些元素。
实际上常用做法:对于每个 ,考虑它和它左边值最接近的 个位置。
九、代码框架(C++)
#include <iostream> #include <cstdlib> #include <cstring> #include <cstdio> #include <algorithm> #include <queue> #include <cmath> #include <set> #define ll long long using namespace std; const int maxn = 100010, inf = 2147483647; struct poi { int l, r, pos; } q[maxn * 3]; int n, m, x, y, z, tot, mn; int tree[maxn << 2], a[maxn * 3], ans[maxn * 3]; set<int>s[maxn << 2]; void read(int &k) { int f = 1; k = 0; char c = getchar(); while (c < '0' || c > '9') c == '-' && (f = -1), c = getchar(); while (c <= '9' && c >= '0') k = k * 10 + c - '0', c = getchar(); k *= f; } inline int abs(int x) { return x >= 0 ? x : -x; } inline int min(int a, int b) { return a > b ? b : a; } void build(int x, int l, int r) { tree[x] = inf; if (l == r) return; int mid = (l + r) >> 1; build(x << 1, l, mid); build(x << 1 | 1, mid + 1, r); } void update(int x, int l, int r, int cx, int delta) { if (l == r) { if (l == cx) return; tree[x] = min(tree[x], abs(a[l] - delta)); mn = min(mn, tree[x]); return; } if (r <= cx) { set<int>::iterator it = s[x].lower_bound(delta); if ((it == s[x].end() || abs(*it - delta) >= mn) && (it == s[x].begin() || abs(*(--it) - delta) >= mn)) { mn = min(mn, tree[x]); return; } int mid = (l + r) >> 1; update(x << 1 | 1, mid + 1, r, cx, delta); update(x << 1, l, mid, cx, delta); tree[x] = min(tree[x << 1], tree[x << 1 | 1]); return; } int mid = (l + r) >> 1; if (cx <= mid) update(x << 1, l, mid, cx, delta); else update(x << 1 | 1, mid + 1, r, cx, delta), update(x << 1, l, mid, cx, delta); tree[x] = min(tree[x << 1], tree[x << 1 | 1]); } void pushset(int x, int l, int r, int cx, int delta) { s[x].insert(delta); if (l == r) return; int mid = (l + r) >> 1; if (cx <= mid) pushset(x << 1, l, mid, cx, delta); else pushset(x << 1 | 1, mid + 1, r, cx, delta); } int query(int x, int l, int r, int cl, int cr) { if (cl <= l && r <= cr) return tree[x]; int mid = (l + r) >> 1, ret = inf; if (cl <= mid) ret = min(ret, query(x << 1, l, mid, cl, cr)); if (cr > mid) ret = min(ret, query(x << 1 | 1, mid + 1, r, cl, cr)); return ret; } bool cmp(poi a, poi b) { return a.r < b.r; } int main() { read(n); for (int i = 1; i <= n; i++) read(a[i]); build(1, 1, n); read(m); for (int i = 1; i <= m; i++) read(q[i].l), read(q[i].r), q[i].pos = i; sort(q + 1, q + 1 + m, cmp); for (int i = 1, j = 1; i <= m; i++) { for (; j <= q[i].r; j++) { mn = inf; update(1, 1, n, j, a[j]); pushset(1, 1, n, j, a[j]); } if (q[i].l == q[i].r) ans[q[i].pos] = inf; else ans[q[i].pos] = query(1, 1, n, q[i].l, q[i].r - 1); } for (int i = 1; i <= m; i++) printf("%d\n", ans[i]); return 0; }
十、复杂度分析
- 候选点对总数
- 线段树更新和查询
- 总复杂度 ,可以接受
十一、总结
本题的关键在于:
- 最小差值一定出现在排序后相邻元素间。
- 每个位置只需要考虑 个候选点对。
- 离线按右端点排序,用线段树维护左端点答案。
- 利用值域的单调性减少候选点对数量。
- 该解法可以高效处理 的数据范围。
- 1
信息
- ID
- 4990
- 时间
- 1500ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 4
- 已通过
- 1
- 上传者