P4587 [FJOI2016]神秘数 (主席树)

P4587 [FJOI2016]神秘数

题意

一个可重复数字集合 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 1n,m105 ∑ a ≤ 10 9 \sum a\le {10}^9 a109

暴力做法

将所求的区间排序,从小到大扫描。假设当前能表示的区间是 [ 1 , n o w ] [1,now] [1,now],现在扫描到的数字为 x x x

  • x ≤ n o w + 1 x \le now + 1 xnow+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 xnow+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 sumlast+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;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值