Atcoder Regular Contest++ 233 A.Strange Subset Sum
目录
题目分析
本题为交互题。隐藏集合 ,其大小为 。如果 的某个子集元素和等于 ,称其为“特殊子集”。我们需要在最多 次询问内,确定是否存在特殊子集;若存在,求出特殊子集的最小大小。
核心数学结论:答案只能是 或
-
最小大小不可能为 :
因为集合中的所有元素都满足 ,任何单个元素都不可能等于 。 -
最小大小不可能 :
定理:如果 中存在元素和为 的子集,且 中不存在和为 的二元组,那么 中一定存在和为 的三元组。简要证明:
若 中没有和为 的二元组,则对于所有的互补对 ,它们在 中的元素个数不能为 。由鸽巢原理和元素总数守恒可知:每对互补对 恰好有且仅有一个元素在 中(若 为偶数,则 )。
假设存在大小 的子集 使得 。
考虑两项之和 :- 因为 是一个“半集”,对于任何值 ,必有 或 。
- 若 且 ,则 构成一个大小为 的互异三元组,其和为 ;
- 若不存在这样的三元组,通过考察最大项与其他项的组合,必能使得 ,从而将这两项合并为一个新元素,归约出和为 的大小为 的子集。
- 由有限降次法(Fermat’s Method of Descent),最终一定能归约到大小为 的特殊子集。
因此,特殊子集的最小大小只有三种可能:、 或 。
交互策略(询问预算 )
我们将 按互补对分组:,其中 。
-
若 为偶数:
- 首先询问 :
- 若 :剩余的 个元素必须全部选自 个互补对。由鸽巢原理,必然至少存在一对互补对的两数同时属于 !此时其和为 ,答案必为 ,只需花费 次询问即可立即输出答案并结束!
- 若 : 占用 个名额,剩余 个元素分布在 个互补对中。
- 首先询问 :
-
配对与抽屉检验(无论是奇数还是偶数且 ):
- 此时剩余 个元素恰好要从 对互补对中选出。设包含 个元素的对数为 ,包含 个元素的对数为 。
- 由总数守恒,必有 。
- 若 ,则存在二元组和为 ,答案为 ;
- 若 ,则每对互补对恰好包含 个元素,无二元组。
- 我们先询问前一半元素 (共 次询问):
- 将在 中的元素记入集合 ;不在 中的记入集合 。
- 显然 ,故 。
- 我们只需检验元素较少的那个集合:
- 若 :对所有 ,询问 。若某个回答为
Yes,则找到了 ,立即输出! 2;若全部为No,则证明 。 - 若 :对所有 ,询问 。若某个回答为
No,说明出现了包含 个元素的对,即 ,立即输出! 2;若全部为Yes,则证明 。
- 若 :对所有 ,询问 。若某个回答为
总询问次数统计:
- 偶数时至多 ;
- 奇数时至多 。
完全严格满足题目限制!
离线判定三元组
当确认 时,集合 的每个元素均已完全确定(;)。
此时二元组不存在,我们只需离线检验 中是否存在互异三元组 满足 :
- 根据初等对称多项式与牛顿恒等式:
其中 。 - 提取 的系数:
- :只需对 做一次多项式自卷积即可。
- : 遍历即可。
- :。
- 由于 的每项系数表示两个 中元素之和等于该下标的组数,其值最大不超过 ,因此直接在模 下做一次 NTT 即可获得精确的非负整数结果,无需 CRT,总时间复杂度为 。
- 若 ,输出
! 3;否则输出! -1。
C++ 代码实现
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <cstdlib>
using namespace std;
const int MOD = 998244353;
const int G = 3;
long long power(long long base, long long exp) {
long long res = 1;
base %= MOD;
while (exp > 0) {
if (exp % 2 == 1) res = (res * base) % MOD;
base = (base * base) % MOD;
exp /= 2;
}
return res;
}
long long modInverse(long long n) {
return power(n, MOD - 2);
}
void ntt(vector<int>& a, bool invert) {
int n = a.size();
for (int i = 1, j = 0; i < n; i++) {
int bit = n >> 1;
for (; j & bit; bit >>= 1) j ^= bit;
j ^= bit;
if (i < j) swap(a[i], a[j]);
}
for (int len = 2; len <= n; len <<= 1) {
long long wlen = power(G, (MOD - 1) / len);
if (invert) wlen = modInverse(wlen);
for (int i = 0; i < n; i += len) {
long long w = 1;
for (int j = 0; j < len / 2; j++) {
long long u = a[i + j];
long long v = (a[i + j + len / 2] * w) % MOD;
a[i + j] = (u + v < MOD ? u + v : u + v - MOD);
a[i + j + len / 2] = (u - v >= 0 ? u - v : u - v + MOD);
w = (w * wlen) % MOD;
}
}
}
if (invert) {
long long n_inv = modInverse(n);
for (int i = 0; i < n; i++) {
a[i] = (a[i] * n_inv) % MOD;
}
}
}
bool ask(int a) {
cout << "? " << a << "\n";
cout.flush();
string resp;
if (!(cin >> resp)) exit(0);
if (resp == "-1") exit(0);
return resp == "Yes";
}
void answer(int s) {
cout << "! " << s << "\n";
cout.flush();
}
void solve() {
int N;
if (!(cin >> N)) exit(0);
int M = (N - 1) / 2;
bool n_half_in_A = false;
// 若 N 为偶数,先询问 N/2
if (N % 2 == 0) {
int half = N / 2;
if (!ask(half)) {
// N/2 不在 A 中,由鸽巢原理必存在二元组互补对
answer(2);
return;
}
n_half_in_A = true;
}
// 询问前一半元素 1..M
vector<int> S1, S0;
for (int x = 1; x <= M; x++) {
if (ask(x)) S1.push_back(x);
else S0.push_back(x);
}
// 检验较小的集合以确定是否存在和为 N 的二元组
bool pair_found = false;
if (S1.size() <= S0.size()) {
for (int x : S1) {
if (ask(N - x)) {
pair_found = true;
break;
}
}
} else {
for (int x : S0) {
if (!ask(N - x)) {
pair_found = true;
break;
}
}
}
if (pair_found) {
answer(2);
return;
}
// 此时不存在和为 N 的二元对,重构 A 的全部元素
vector<int> in_A(N, 0);
vector<int> A_elements;
if (n_half_in_A) {
in_A[N / 2] = 1;
A_elements.push_back(N / 2);
}
for (int x : S1) {
in_A[x] = 1;
A_elements.push_back(x);
}
for (int x : S0) {
in_A[N - x] = 1;
A_elements.push_back(N - x);
}
if (A_elements.size() < 3) {
answer(-1);
return;
}
// 使用 NTT 计算是否存在三元组
int L = 1;
while (L <= 2 * N) L <<= 1;
vector<int> s1(L, 0);
for (int x : A_elements) s1[x] = 1;
vector<int> s1_sq = s1;
ntt(s1_sq, false);
for (int i = 0; i < L; i++) s1_sq[i] = (1LL * s1_sq[i] * s1_sq[i]) % MOD;
ntt(s1_sq, true);
long long term1 = 0;
for (int c : A_elements) {
if (N - c >= 0 && N - c < L) term1 += s1_sq[N - c];
}
long long term2 = 0;
for (int b : A_elements) {
if (N - 2 * b >= 0 && N - 2 * b < N && in_A[N - 2 * b]) term2++;
}
long long term3 = 0;
if (N % 3 == 0 && in_A[N / 3]) term3 = 1;
long long e3 = (term1 - 3 * term2 + 2 * term3) / 6;
if (e3 > 0) {
answer(3);
} else {
answer(-1);
}
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int T;
if (cin >> T) {
while (T--) solve();
}
return 0;
}