【UER #11】科考工作
- 给定 \(2n-1\) 个 \([0,n-1]\) 中的整数,选择恰好 \(n\) 个使得和为 \(n\) 的倍数。保证 \(n\) 为质数。
- \(n\leq 3\times 10^5\)。
本质思想就是若在 \(\bmod n\) 意义下,对于任意 \(x\in[0,n-1]\) 集合 \(s\) 满足 \(s\cup s+x = s\)(即对于任意集合中的数 \(y\),\((y+x)\bmod n\in s\)),那么 \(s\) 必定 \(=\{0,1,\cdots, n-1\}\)。
这个结论还是比较显然的,考虑 \(0,x,2x,3x, \cdots, (n-1)x\) 在 \(\bmod n\) 意义下构成了长度为 \(n\) 的环,因为一定有 \(0\in s\),故如果有数不在 \(s\) 中,那么一定能找到相邻的两个数 \(kx, (k+1)x\),使得 \(kx\in s, (k+1)x\not \in s\),得到矛盾。
回到原问题,先任意选出一个数扔一边,再给剩下的 \(2n-2\) 个数两两配对,使得配对的两数不同。如果无法达到说明有一种数字出现次数 \(>n-1\),直接输出 \(n\) 个这种数即可。
之后先选择每个配对中的第一个数,结合一开始扔出去的数得到和为 \(s\) 的 \(n\) 个数。对于每个配对 \((a,b)\) 处理出反悔的贡献 \(d = b-a\),现在变成要求选出集合 \(d\) 的一个子集使得和为 \(-s\)。
要做的就是循环 01 背包,根据一开始的结论每次至少在集合中新增一个数,所以一定有解。
至少可以用 bitset 优化到 \(O(\frac{n^2}{\omega})\),利用 二分 + Hash 每次在循环背包上精准定位 \(0\to 1\) 的位置可以做到 \(O(n\log ^2 n)\)(动态 Hash 需要树状数组),同时还有两倍常数(会有同样多个 \(1\to 0\) 被定位到)。
更进一步的,这里每次让集合新增恰好一个数就足够了,并且已知 \(0\in s\),只需要找到任意一个不在集合中的数 \(c\),解出 \(c = kd\),那么序列在 \(0,d,2d,\cdots, kd\) 这个首为 \(1\) 尾为 \(0\) 的部分中一定有相邻的 \((1, 0)\),这是经典的可二分的结构。
code
#include<bits/stdc++.h>
typedef long long ll;
typedef unsigned long long ull;
#define rep(i, a, b) for(int i = (a); i <= (b); i ++)
#define per(i, a, b) for(int i = (a); i >= (b); i --)
#define Ede(i, u) for(int i = head[u]; i; i = e[i].nxt)
using namespace std;
#define eb emplace_back
typedef pair<int, int> pii;
#define mp make_pair
#define fi first
#define se second
inline int read() {
int x = 0, f = 1; char c = getchar();
while(c < '0' || c > '9') f = (c == '-') ? - 1 : 1, c = getchar();
while(c >= '0' && c <= '9') x = x * 10 + c - 48, c = getchar();
return x * f;
}
const int N = 3e5 + 10;
int n, a[N << 1], d[N]; pii b[N];
int f[N]; bool r[N];
int p[N << 1];
void solve1() {
rep(i, 1, (n << 1) - 2) p[i] = i;
sort(p + 1, p + (n << 1) - 1, [](int x, int y) {return a[x] < a[y];});
rep(i, 1, n - 1) {
if(a[p[i]] == a[p[i + n - 1]]) {
rep(j, 1, n) printf("%d%c", p[i + j - 1], j == n ? '\n' : ' ');
exit(0);
}
b[i] = mp(p[i], p[i + n - 1]);
d[i] = (a[p[i + n - 1]] - a[p[i]] + n) % n;
}
}
void exgcd(int a, int b, int &x, int &y) {
if(!b) return x = 1, y = 0, void();
exgcd(b, a % b, x, y);
int z = x; x = y, y = z - y * (a / b);
}
int calc(int b, int a) {
int x, y; exgcd(a, n, x, y);
x = (x % n + n) % n;
return 1ll * x * b % n;
}
void solve2() {
f[0] = -1; int c = 1;
rep(i, 1, n - 1) {
int l = 1;
int r = calc(c, d[i]);
while(l < r) {
int mid = (l + r) >> 1;
int cur = 1ll * mid * d[i] % n;
if(!f[cur]) r = mid; else l = mid + 1;
}
f[1ll * l * d[i] % n] = i;
while(c < n && f[c]) c ++;
if(c == n) break;
}
int s = a[(n << 1) - 1];
rep(i, 1, n - 1) s = (s + a[b[i].fi]) % n;
s = (n - s) % n;
while(s) {
r[f[s]] = true;
s = (s - d[f[s]] + n) % n;
}
}
int main() {
n = read();
rep(i, 1, (n << 1) - 1) a[i] = read();
solve1();
solve2();
rep(i, 1, n - 1) printf("%d ", r[i] ? b[i].se : b[i].fi);
printf("%d\n", (n << 1) - 1);
return 0;
}
实际上,这题是可以扩展到 \(n\) 为非质数的!主要问题是此时 \(0,x,2x,\cdots\) 的环大小可能 \(<n\),所以上述做法不成立,但是既然有了质数的解法,不妨尝试分解质因数。
假设 \(n=mp\),其中 \(p\) 为质数。现在序列中有 \(2mp -1= (2m-1)p+(p-1)\) 个数。考虑先任意扔出 \(p-1\) 个数,再将剩下的每 \(p\) 个分为一组,构成 \(2m-1\) 组,且每一组的和均为 \(p\) 的倍数。这个是可以做到的,因为每次可以直接选出 \(p\) 个数,和扔出的 \(p-1\) 个数组成 \(2p-1\) 个数,从中选 \(p\) 个构成 \(p\) 的倍数就是之前的问题,处理后将不用的 \(p-1\) 个数继续扔出来即可。
之后就会有 \(2m-1\) 组数,以及 \(p-1\) 个数。直接不要那 \(p-1\) 个数,而是在组中选,因为每组的和都是 \(p\) 的倍数,所以可以直接将 每组的和除以 \(p\) 作为一个新的序列,递归到在 \(2m-1\) 个数中选择 \(m\) 的倍数的子问题。
因为不断递归必然回到质数问题,而质数一定有解,所以这样一定有解!
于是我们就成功构造性的证明了:给定任意 \(2n-1\) 个 \([0,n-1]\) 中的整数,都能够选择恰好 \(n\) 个使得和为 \(n\) 的倍数。这个事实。