题目来源:Codeforces,#678,Codeforces Round #678 (Div. 2),CF1436;CF1436E Complicated Computations,CF1436F Sum Over Subsets。
CF1436E Complicated Computations
从小到大依次枚举每个正整数,看能否作为答案。一个数 (x) 能作为答案的条件是:
- 比它小的值都不是答案。
- 不存在一个区间 ([l,r]),使得 ( ext{mex}(a_l,a_{l+1},dots,a_r) = x)。
因为是从小到大枚举的,所以只要我们一找到答案就退出,那条件 1 就已经解决了。
考虑条件 2。如何判断是否存在一个区间,使得 ( ext{mex}(a_l,a_{l+1},dots,a_r) = x) 呢?
- 首先,(x) 不能在这个区间里出现过。记 (x) 在整个序列里出现的位置分别为 (p_1,p_2,dots,p_k),这些点把整个序列划分为了不超过 (k+1) 个线段。那 ([l,r]) 一定被完全包含在某个线段中。
- 其次,所有小于 (x) 的正整数都必须在区间 ([l,r]) 里出现过。那么我们肯定希望在满足上一条的同时,让区间 ([l,r]) 尽量长(这样更有可能包含到所有值)。所以如果存在合法的 ([l,r]),那也一定存在某个完整的线段是合法的。于是我们只需要对这 (k+1) 个线段进行判断即可。
问题转化为,对这 (k+1) 个线段,分别判断“所有小于 (x) 的正整数是否都在里面出现过”。也就是对它们求 ( ext{mex})。
注意到,对于所有 (x),线段的总数(也就是 (sum(k+1)))只有 (O(n)) 个。
求区间 ( ext{mex}) 有一个经典的莫队做法。具体来说,我们先将这 (O(n)) 个要查询的区间离线下来。然后做莫队。需要支持每次插入 / 删除一个数,求当前集合的 ( ext{mex})。
第一反应是在值域上建一棵线段树。但是这样插入 / 删除操作就是 (O(log n)) 的了,总时间复杂度 (O(nsqrt{n}log n))。事实上,发现我们需要的修改次数很多((O(nsqrt{n})) 次),但查询次数较少((O(n)) 次)。而线段树恰恰是修改较慢((O(log n))),查询较快(因为是全局查询,直接回答根节点信息,是 (O(1)) 的)。所以我们在这里用一个“根号平衡”,即用分块,牺牲查询的复杂度,实现较快的修改。具体来说,我们对值域分块,维护每个块内有几个出现次数非零的值。修改是单点修改,可以 (O(1)) 完成。查询时,从小到大暴力枚举每个块,遇到第一个没有满的块,就在这个块里暴力找到答案。这样查询是 (O(sqrt{n})) 的。
总时间复杂度 (O(nsqrt{n}))。
参考代码:
// problem: CF1436E
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())
typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
template<typename T> inline void ckmax(T& x, T y) { x = (y > x ? y : x); }
template<typename T> inline void ckmin(T& x, T y) { x = (y < x ? y : x); }
const int MAXN = 1e5;
const int MAXM = MAXN * 2; // 考虑妙妙的常数
const int BLOCK_SIZE = 300, BLOCK_NUM = MAXN / BLOCK_SIZE + 10;
int n, a[MAXN + 5];
int st[BLOCK_NUM + 5], ed[BLOCK_NUM + 5], bel[MAXN + 5], cnt_block;
vector<int> vec[MAXN + 5];
vector<int> qid[MAXN + 5];
int ans[MAXM + 5], cntq;
struct Query_t {
int l, r, id;
Query_t(int _l, int _r, int _id) { l = _l; r = _r; id = _id; }
Query_t() {}
}q[MAXM + 5];
bool cmp(Query_t lhs, Query_t rhs) {
return (bel[lhs.l] == bel[rhs.l]) ? (lhs.r < rhs.r) : (lhs.l < rhs.l);
}
int buc[MAXN + 5], nemp[BLOCK_NUM + 5]; // nemp: not empty 块里非空的桶有几个
void modify(int p, int v) {
if (buc[p]) nemp[bel[p]]--;
buc[p] += v;
if (buc[p]) nemp[bel[p]]++;
}
int get_mex() {
for (int i = 1; i <= cnt_block; ++i) {
if (nemp[i] == ed[i] - st[i] + 1)
continue; // 全满了
for (int j = st[i]; j <= ed[i]; ++j) {
if (!buc[j])
return j;
}
assert(0);
}
return n + 1;
}
int curl, curr;
void movel(int t) {
if (t == 1) {
// l 右移, 删数
modify(a[curl], -1);
++curl;
} else {
// l 左移, 加数
--curl;
modify(a[curl], 1);
}
}
void mover(int t) {
if (t == 1) {
// r 右移, 加数
++curr;
modify(a[curr], 1);
} else {
// r 左移, 删数
modify(a[curr], -1);
curr--;
}
}
int main() {
cin >> n;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
vec[a[i]].pb(i);
}
for (int i = 1; i <= n; i += BLOCK_SIZE) {
int j = min(n, i + BLOCK_SIZE - 1);
++cnt_block;
st[cnt_block] = i;
ed[cnt_block] = j;
for (int k = i; k <= j; ++k) {
bel[k] = cnt_block;
}
}
for (int i = 1; i <= n + 1; ++i) {
if (!SZ(vec[i])) {
++cntq;
q[cntq] = Query_t(1, n, cntq);
qid[i].pb(cntq);
} else {
if (vec[i][0] != 1) {
++cntq;
q[cntq] = Query_t(1, vec[i][0] - 1, cntq);
qid[i].pb(cntq);
}
for (int j = 1; j < SZ(vec[i]); ++j) {
if (vec[i][j] != vec[i][j - 1] + 1) {
++cntq;
q[cntq] = Query_t(vec[i][j - 1] + 1, vec[i][j] - 1, cntq);
qid[i].pb(cntq);
}
}
if (vec[i].back() != n) {
++cntq;
q[cntq] = Query_t(vec[i].back() + 1, n, cntq);
qid[i].pb(cntq);
}
}
}
sort(q + 1, q + cntq + 1, cmp);
curl = 1, curr = 0;
for (int i = 1; i <= cntq; ++i) {
while (curl > q[i].l) movel(-1);
while (curr < q[i].r) mover(1);
while (curl < q[i].l) movel(1);
while (curr > q[i].r) mover(-1);
ans[q[i].id] = get_mex();
}
for (int i = 1; i <= n + 2; ++i) {
bool fail = 0;
for (int j = 0; j < SZ(qid[i]); ++j) {
if (ans[qid[i][j]] == i) {
fail = 1;
break;
}
}
if (!fail) {
cout << i << endl;
return 0;
}
}
assert(0);
}
CF1436F Sum Over Subsets
题目要求 (A) 集合里数的 (gcd = 1)。
考虑求出 (g_i),表示 (gcd = i) 的答案。最后输出 (g_1) 即可。
(gcd) 恰好等于 (i) 不好求。我们可以先求一个 (f_i),表示 (gcd) 是 (i) 的倍数的答案,即 (f_i = sum_{i|j}g_j)。然后再容斥回去,或者套用莫比乌斯反演的式子:(g_i = sum_{i|j}f_jcdot mu(frac{j}{i})) 就能求出答案了。
依次枚举每个 (i),考虑求 (f_i)。记所有 (i) 的倍数组成的可重集为 (S)。则 (f_i = sum_{Bsubset Asubseteq S,|B|=|A|-1}sum_{xin A}x imes (sum_{yin B}y))。可以写成:(sum_{A,B}sum_{xin A,yin B}xcdot y)。那么我们考虑可重集 (S) 里任意一对 (x,y) 对答案的贡献。分两种情况。以下记在 (A) 中但不在 (B) 中的数为被踢出的数:
- (x,y) 是同一个数。对答案的贡献是 (x^2 imes (|S|-1) imes2^{|S|-2})。这个式子挺妙的。如果按照一般的想法,考虑 (A) 集合的大小,再从中选出一个数踢出,这就需要枚举集合大小,时间复杂度很高。而现在我们先选出要踢出的数((|S|-1) 种选法),再让剩下的数随意组成集合((2^{|S|-2}) 种方案),就不用枚举集合大小了!
- (x,y) 不是同一个数。对答案的贡献是 (x imes y imes((|S|-2) imes2^{|S|-3} + 2^{|S|-2}))。前半部分和上一种情况类似。后面的 (2^{|S|-2}) 是被踢出的数恰好是 (x)的情况。
记 ( ext{freq}(x)) 表示数值 (x) 的出现次数(请注意,这里与输入格式中的定义略有不同)。由于 ( ext{freq}) 高达 (10^9),(S) 集合的大小是巨大的。我们不可能真的枚举所有数。所以我们只能枚举数值。此时需要进一步讨论:
- (x,y) 是同一个数(也就是上述的情况 1)。对答案的贡献是 (x^2 imes(|S|-1) imes2^{|S|-2})。
- (x,y) 是数值相等的两个数。对答案的贡献是 (x^2 imes((|S| - 2) imes2^{|S|-3}+2^{|S|-2}) imes ext{freq}(x) imes( ext{freq}(x)-1))。
- (x,y) 是数值不相等的两个数。对答案的贡献是 (x imes y imes((|S| - 2) imes2^{|S|-3}+2^{|S|-2}) imes ext{freq}(x) imes ext{freq}(y))。但是这里我们不能真的枚举两个数值。可以先预处理出集合 (S) 里所有数值的 (x imes ext{freq}(x)) 之和,记为 ( ext{sum})。这样只要枚举 (y),对 (f_i) 的贡献就是 (y imes ext{freq}(y) imes( ext{sum}-y imes ext{freq}(y)) imes((|S| - 2) imes2^{|S|-3}+2^{|S|-2}))。
于是我们对于每个 (i),只需要枚举所有数值。即 (i,2i,3i,dots)。时间复杂度是调和级数,即 (O(nlog n))。
参考代码:
// problem: CF1436F
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())
typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
template<typename T> inline void ckmax(T& x, T y) { x = (y > x ? y : x); }
template<typename T> inline void ckmin(T& x, T y) { x = (y < x ? y : x); }
const int MAXN = 1e5;
const int MOD = 998244353;
inline int mod1(int x) { return x < MOD ? x : x - MOD; }
inline int mod2(int x) { return x < 0 ? x + MOD : x; }
inline void add(int &x, int y) { x = mod1(x + y); }
inline void sub(int &x, int y) { x = mod2(x - y); }
inline int pow_mod(int x, int i) {
int y = 1;
while (i) {
if (i & 1) y = (ll)y * x % MOD;
x = (ll)x * x % MOD;
i >>= 1;
}
return y;
}
int p[MAXN + 5], cnt_p, mu[MAXN + 5];
bool v[MAXN + 5];
void sieve(int lim = MAXN) {
mu[1] = 1;
for (int i = 2; i <= lim; ++i) {
if (!v[i]) {
p[++cnt_p] = i;
mu[i] = -1;
}
for (int j = 1; j <= cnt_p && p[j] * i <= lim; ++j) {
v[i * p[j]] = 1;
if (i % p[j] == 0) {
break;
}
mu[i * p[j]] = -mu[i];
}
}
}
int n, m, freq[MAXN + 5];
int f[MAXN + 5];
int main() {
sieve(n = MAXN);
cin >> m;
for (int i = 1; i <= m; ++i) {
int a;
cin >> a;
cin >> freq[a];
}
for (int i = 1; i <= n; ++i) {
ll tot = 0;
for (int j = i; j <= n; j += i) {
tot += freq[j];
}
if (tot < 2) continue;
int w2, w3;
w3 = pow_mod(2, (tot - 2) % (MOD - 1));
w2 = (ll)(tot - 1) % MOD * w3 % MOD;
if (tot >= 3) w3 = ((ll)w3 + (ll)(tot - 2) % MOD * pow_mod(2, (tot - 3) % (MOD - 1))) % MOD;
for (int j = i; j <= n; j += i) if (freq[j]) {
f[i] = ((ll)f[i] + (ll)j * j % MOD * w2 % MOD * freq[j]) % MOD;
if (freq[j] >= 2) {
f[i] = ((ll)f[i] + (ll)j * j % MOD * w3 % MOD * freq[j] % MOD * (freq[j] - 1)) % MOD;
}
}
/*
for (int j = i; j <= n; j += i) if (freq[j]) {
for (int k = i; k <= n; k += i) if (k != j && freq[k]) {
f[i] = ((ll)f[i] + (ll)j * k % MOD * w3 % MOD * freq[j] % MOD * freq[k]) % MOD;
}
}
*/
int sum = 0;
for (int j = i; j <= n; j += i) if (freq[j]) {
sum = ((ll)sum + (ll)j * freq[j]) % MOD;
}
for (int j = i; j <= n; j += i) if (freq[j]) {
int cur = mod2(sum - (ll)j * freq[j] % MOD);
f[i] = ((ll)f[i] + (ll)j * freq[j] % MOD * cur % MOD * w3) % MOD;
}
}
int ans = 0;
for (int i = 1; i <= n; ++i) {
if (mu[i] == 1)
add(ans, f[i]);
if (mu[i] == -1)
sub(ans, f[i]);
}
cout << ans << endl;
return 0;
}