[ 2020牛客NOIP赛前集训营-提高组(第一场)] 牛牛的凑数游戏
Problem
题目地址
Solution
集合最小不能表示数
定义 :对于一个多重数集 \(S\),对于一非负整数 \(x\),若存在 \(S'⊆ S\) 且 \(S'\) 中所有数字之和恰好等于 \(x\),则说 \(S\) 可以表示 \(x\) 。显然对于任意的多重数集都可以表示 \(0\),因为空集是所有集合的子集。定义集合 \(S\) 的最小不能表示数为,一个最小的非负整数 \(x\),\(S\) 不能表示 \(x\)。举个例子来说,例如 \(S=\{1,2,3,8,9\}\) ,那么集合 \(S\) 的最小不能表示数就为 \(7\) 。
如何求一个集合的最小不能表示数?
将数字按升序排列,假设前 \(i\) 个数可以表示的数是 \([0,k]\),那么如果 \(a_{i+1} \le k+1\),则前 \(i+1\) 个数可以表示的数有 \([0,k+a_{i+1}]\),否则前 \(i+1\) 个数可以表示的数是 \([0,k]\),进一步可以推断出整个集合的的最小不能表示数就是 \(k+1\)。
相当于我们要将数字排序,然后用 \(sum\) 记录前 \(i\) 个数的和。如果 \(a_{i} \le sum+1\),就把 \(a_i\) 加入 \(sum\),否则如果 \(a_{i} > sum\),那么 \(sum+1\) 就是这个集合最小不能表示数。求单个集合时间复杂度 \(O(n \log n)\) (排序)。
迭代算法求解
排序求解法是我们直接推导得到的,我们可以考虑用一种迭代法:
不将数字排序,把 \(n\) 个数扫一遍,将 \(\le sum+1\) 的数加入 \(sum\),上一轮已经加入过 \(sum\) 的数不能再加入。记新的 \(sum\) 为 \(sum'\)。用 \(sum'\) 代替 \(sum\),重复迭代上述操作。若第 \(k\) 轮操作后 \(sum = sum'\)(没有数被加入进去)则算法结束 break。
接近 \(\log n\) 层迭代,每层迭代 \(O(n)\) 的复杂度,时间复杂度 \(O(n \log n)\)
进一步优化
一共 \(m\) 个查询区间,将查询区间离线,同时进行 \(m\) 个查询区间的迭代求解。用树状数组处理每个查询区间,统计该区间 \(\le\) 当前区间的 \(sum+1\) 的数之和。树状数组从左到右把数加入处理一遍可以把 \(m\) 的查询区间全部处理完,时间复杂度 \(O(n \log n)\)
总复杂度 \(O(n \log^2 n)\)
Code
Talk is cheap.Show me the code.
#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read() {
int x = 0, f = 1; char ch = getchar();
while(ch<'0' || ch>'9') { if(ch=='-') f=-1; ch=getchar(); }
while(ch>='0'&&ch<='9') { x=(x<<3)+(x<<1)+(ch^48); ch=getchar(); }
return x * f;
}
const int N = 1e5+7;
int n,m,ntmp;
int a[N],ida[N],L[N],R[N],sum[N],pre[N],c[N],btmp[N];
vector<pair<int,int> > query[N];
void Add(int x,int y) {
while(x <= ntmp) {
c[x] += y; x += x & -x;
}
}
int Ask(int x) {
int res = 0;
while(x > 0) {
res += c[x]; x -= x & -x;
}
return res;
}
int Getpos(int x) {
int l = 1, r = ntmp, mid, ans;
while(l <= r) {
mid = (l + r) >> 1;
if(btmp[mid] <= x) {
ans = mid;
l = mid + 1;
} else r = mid - 1;
}
return ans;
}
signed main()
{
n = read(), m = read();
for(int i=1;i<=n;++i) btmp[i] = a[i] = read();
sort(btmp+1, btmp+1+n);
ntmp = unique(btmp+1, btmp+1+n) - btmp - 1;
for(int i=1;i<=n;++i) ida[i] = lower_bound(btmp+1, btmp+1+ntmp, a[i]) - btmp;
for(int i=1;i<=m;++i) {
L[i] = read(), R[i] = read();
query[L[i]-1].push_back(make_pair(i, -1));
query[R[i]].push_back(make_pair(i, 1));
}
for(int i=1;i<=m;++i) pre[i] = -1;
while(true) {
bool flag = 1;
cout << endl << endl;
for(int i=1;i<=m;++i) {
if(sum[i] != pre[i]) {
flag = 0; break;
}
}
if(flag == 1) break;
for(int i=1;i<=m;++i) pre[i] = sum[i];
memset(c ,0, sizeof(c));
for(int i=1;i<=n;++i) {
Add(ida[i],a[i]);
for(int j=0;j<query[i].size();++j) {
int qid = query[i][j].first, f = query[i][j].second;
int sid = Getpos(pre[qid] + 1);
sum[qid] += Ask(sid) * f;
}
}
for(int i=1;i<=m;++i) sum[i] -= pre[i];
}
for(int i=1;i<=m;++i) printf("%lld\n",sum[i]+1);
return 0;
}
/*
10 10
10 6 2 7 1 4 1 6 3 4
2 3
1 9
6 6
2 8
1 4
2 7
4 5
7 7
6 8
7 10
1
41
1
28
1
22
2
2
2
2
*/
Summary
-
集合最小不能表示数
-
迭代算法思想