P4587 [FJOI2016] 神秘数

题目描述

一个可重复数字集合 \(S\) 的神秘数定义为最小的不能被 \(S\) 的子集的和表示的正整数。例如 \(S=\{1,1,1,4,13\}\),有:\(1 = 1\)\(2 = 1+1\)\(3 = 1+1+1\)\(4 = 4\)\(5 = 4+1\)\(6 = 4+1+1\)\(7 = 4+1+1+1\)

\(8\) 无法表示为集合 \(S\) 的子集的和,故集合 \(S\) 的神秘数为 \(8\)

现给定长度为 \(n\)正整数序列 \(a\)\(m\) 次询问,每次询问包含两个参数 \(l,r\),你需要求出由 \(a_l,a_{l+1},\cdots,a_r\) 所组成的可重集合的神秘数。

对于 \(100\%\) 的数据点,\(1\le n,m\le {10}^5\)\(\sum a\le {10}^9\)

题目解析

先考虑单组询问,\(l=1, r=n\) 的做法。

\(a\) 数组从小到大排序,设遍历到一个数 \(a_i\) 时,前面的数可以表示出 \([1, x]\) 中的所有数,那么:

  • \(a_i \le x + 1\),则任何 \([a_i + 1, a_i + x]\) 中的数都可以被表示为 \(a_i + u\) 的形式,其中 \(u \in [1, x]\). 于是新的可表示区间变为 \([1, a_i + x]\)
  • \(a_i > x + 1\),则 \(x+1\) 无法被表示,寄。

时间复杂度 \(O(n)\).

考虑优化。假设当前表示出了 \([1, x]\) 中的所有数,那么下一步放进 \([1, x+1]\) 中的任何数都一定是合法的。于是依次放入所有先前还未放入的 \(\le x + 1\) 的数,新的可表示区间为 \([1, x + sum]\).

注意到 \(x+sum\) 其实就是 \([1, x + 1]\) 中所有数的和,于是用线段树维护这个和即可。

这个操作很类似斐波那契数列的累加,再乘上线段树的复杂度,大概为 \(\log \sum a_i \cdot \log n\).

加上数都要在 \([l, r]\) 中的限制,就把线段树换成主席树即可,每次相当于在第 \(l \sim r\) 个版本之间查询。

#include <bits/stdc++.h>
using namespace std;

const int N = 1e5 + 5;

int n, a[N], root[N], lc[N << 5], rc[N << 5], idx, sum[N << 5], m, l, r;

int update(int l, int r, int p, int x)
{
	int y = ++idx;
	lc[y] = lc[x], rc[y] = rc[x], sum[y] = sum[x] + p;
	if(l == r) return y;
	int mid = (l + r) >> 1;
	if(p <= mid) lc[y] = update(l, mid, p, lc[y]);
	if(mid < p) rc[y] = update(mid + 1, r, p, rc[y]);
	return y;
}

int query(int l, int r, int L, int R, int x, int y)
{
	if(l > r || L > R) return 0;
	if(L <= l && r <= R) return sum[y] - sum[x];
	int mid = (l + r) >> 1, res = 0;
	if(L <= mid) res += query(l, mid, L, R, lc[x], lc[y]);
	if(mid < R) res += query(mid + 1, r, L, R, rc[x], rc[y]);
	return res;
}

int main()
{
	ios :: sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	cin >> n;
	for(int i = 1; i <= n; i++) cin >> a[i];
	for(int i = 1; i <= n; i++)
		root[i] = update(1, 1e9, a[i], root[i - 1]);
	cin >> m;
	while(m--)
	{
		cin >> l >> r;
		int x = 0;
		while(1)
		{
			int c = query(1, 1e9, 1, x + 1, root[l - 1], root[r]);
			if(c > x) x = c;
			else break;
		}
		cout << x + 1 << '\n';
	}
	return 0;
}
posted @ 2025-04-10 14:54  心灵震荡  阅读(31)  评论(0)    收藏  举报