题意
一个可重复数字集合 S S S 的神秘数定义为最小的不能被 S S S 的子集的和表示的正整数。
现给定长度为 n n n 的正整数序列 a a a, m m m 次询问,每次询问包含两个参数 l , r l,r l,r,你需要求出由 a l , a l + 1 , ⋯ , a r a_l,a_{l+1},\cdots,a_r al,al+1,⋯,ar 所组成的可重集合的神秘数。
1 ≤ n , m ≤ 10 5 1\le n,m\le {10}^5 1≤n,m≤105, ∑ a ≤ 10 9 \sum a\le {10}^9 ∑a≤109。
暴力做法
将所求的区间排序,从小到大扫描。假设当前能表示的区间是 [ 1 , n o w ] [1,now] [1,now],现在扫描到的数字为 x x x:
- x ≤ n o w + 1 x \le now + 1 x≤now+1,那么更新 n o w now now 为 n o w + x now + x now+x;
- 否则 a n s + 1 ans + 1 ans+1 无法被表示,输出 n o w + 1 now + 1 now+1。
另外会发现一个性质,能表示的数字最大值 n o w now now 就等于已经扫描过的数字之和。
优化
思路仍然是把 a l . . . r a_{l...r} al...r 内所有的数字取出来排序,从小到大加入待选集合。但这次我们尝试批量加入。
因为是从小到大加入,假设不超过 l a s t last last 的数字已经被全部加入了。所以新加入的数字必须满足 x > l a s t x > last x>last。
同时假设当前能表示的区间是 [ 1 , n o w ] [1,now] [1,now],而 n o w now now 就等于已经加入的数字之和。此时新加入的数字 x x x 必须满足 x ≤ n o w + 1 x \le now + 1 x≤now+1,否则会导致 n o w + 1 now + 1 now+1 无法被表示。
所以我们一次性将大小在 [ l a s t + 1 , n o w + 1 ] [last + 1, now + 1] [last+1,now+1] 范围中的数字批量加入,假设满足这个要求的数字之和为 s u m sum sum。那么:
- l a s t last last 被更新为 n o w + 1 now + 1 now+1(虽然新加入的数字可能不包含 n o w + 1 now + 1 now+1,但是我们保证了不超过 n o w + 1 now + 1 now+1 的数字已经被全部加入了,所以 n o w + 1 now + 1 now+1 可以作为新的下界);
- n o w now now 被更新为 n o w + s u m now + sum now+sum;
- 如果 s u m = 0 sum=0 sum=0,说明 n o w + 1 now + 1 now+1 就是最小的无法被表示的正整数,break 并输出 n o w + 1 now + 1 now+1。
现在考虑一下复杂度。只要 s u m > 0 sum > 0 sum>0 ,就必然有 s u m ≥ l a s t + 1 sum \ge last + 1 sum≥last+1。所以 l a s t last last 的增幅是倍增的,单次查询的循环次数不会超过 log A \log A logA。 A A A 是 a a a 的值域。
至于怎样查询原数组 [ l , r ] [l,r] [l,r] 区间内,在某个范围 [ l a s t + 1 , n o w + 1 ] [last + 1, now + 1] [last+1,now+1] 内的数字之和,就是主席树了。本题值域较大,所以使用动态开点权值线段树。总复杂度是 m log 2 A m\log^2 A mlog2A。
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define PII pair<int, int>
#define endl "\n"
/********************** Core code begins **********************/
const int INF = 1e9 + 7;
// 可持久化动态开点权值线段树
struct segtrs {
struct Info {
int l = 0, r = 0, sum = 0;
};
vector<Info> info;
int cnt = 0;
segtrs(int n): info(n * 64) {}
// 以 p 为根的线段树,维护区间 [l,r]。将位置 pos 的权值增加 val,返回新建的节点
int insert(int p, int l, int r, int pos, int val) {
int rt = ++cnt;
info[rt] = info[p];
info[rt].sum += val;
if (l == r) {
return rt;
}
int mid = (l + r) >> 1;
if (pos <= mid) {
info[rt].l = insert(info[p].l, l, mid, pos, val);
} else {
info[rt].r = insert(info[p].r, mid + 1, r, pos, val);
}
return rt;
}
// 将 p2 和 p1 位根的权值线段树作差。查询范围 [x, y] 的数字之和
int query(int p1, int p2, int l, int r, int x, int y) {
if (x <= l && r <= y) {
return info[p2].sum - info[p1].sum;
}
int mid = (l + r) >> 1, res = 0;
if (x <= mid) {
res += query(info[p1].l, info[p2].l, l, mid, x, y);
}
if (y > mid) {
res += query(info[p1].r, info[p2].r, mid + 1, r, x, y);
}
return res;
}
};
void SolveTest() {
int n, q;
cin >> n;
vector<int> a(n + 1), root(n + 1);
segtrs tr(n);
for (int i = 1; i <= n; i++) {
cin >> a[i];
root[i] = tr.insert(root[i - 1], 1, INF, a[i], a[i]);
}
cin >> q;
while (q--) {
int l, r;
cin >> l >> r;
int now = 0, last = 0;
while (1) {
int sum = tr.query(root[l - 1], root[r], 1, INF, last + 1, now + 1);
if (sum == 0) {
break;
}
last = now + 1;
now += sum;
}
cout << now + 1 << endl;
}
}
/********************** Core code ends ***********************/
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int T = 1;
// cin >> T;
for (int i = 1; i <= T; i++) {
SolveTest();
}
return 0;
}