训练赛 - 2020icpc澳门
A | B | C | D | E | F | G | H | I | J | K | L |
---|---|---|---|---|---|---|---|---|---|---|---|
1 | 0 | 2 | 1 | 0 | 2 | 2 | 0 | 2 | 0 | 0 | 1 |
0:未完成
1:赛时做出
2:赛后补
总结:
3题铁牌。比赛时负责C和G,想法的方向是正确的,但是想的过于浮于表面,没有深入地去优化。签到题常规题基本都是 复杂度高做法+使劲优化 = 正解。想好写的做法,多寻找问题所拥有的特别性质,观察数据范围有助于发现突破口。
A - Accelerator(分治fft)
即求类似\(a_1a_2a_3 + a_2a_3+a_3\)这样的式子。关键是求每一项的总和。就是每一项取与不取的,即
然后多项式每一项的系数就是需要的值。还需要乘上每一项对应的序列前面的组合个数。
有点卡时间,可以预处理出原根的次幂,减少常数。
#include <bits/stdc++.h>
typedef long long ll;
#define endl '\n'
using namespace std;
const int N = 3e5 + 10;
const int M = 998244353;
inline ll qpow(ll a ,ll b, ll m) {
ll res = 1;
while(b) {
if(b & 1) {
res = (res * a) % m;
}
a = (a * a) % m;
b >>= 1;
}
return res;
}
int rev[N];
void change(vector<int> &y, int len) {
for(int i = 0; i < len; i++) {
rev[i] = rev[i >> 1] >> 1;
if(i & 1) {
rev[i] |= len >> 1;
}
}
for(int i = 0; i < len; i++) {
if(i < rev[i]) {
swap(y[i], y[rev[i]]);
}
}
return ;
}
int ngn[N];
int rgn[N];
int inv[N];
void ntt(vector<int> &y, int len, int on) {
change(y, len);
for(int h = 2; h <= len; h <<= 1) {
int gn = ngn[h];
if(on == -1) {
gn = rgn[h];
}
for(int j = 0; j < len; j += h) {
ll g = 1;
for(int k = j; k < j + h / 2; k++) {
int u = y[k];
int t = g * y[k + h / 2] % M;
y[k] = (u + t) % M;
y[k + h / 2] = (u - t + M) % M;
g = g * gn % M;
}
}
}
if(on == -1) {
int iv = inv[len];
for(int i = 0; i < len; i++) {
y[i] = 1ll * y[i] * iv % M;
}
}
}
int get(int x) {
int res = 1;
while(res < x) {
res <<= 1;
}
return res;
}
int arr[N];
int fact[N];
vector<int> solve(int l, int r) {
if(l == r) {
vector<int> res(2);
res[0] = 1;
res[1] = arr[l];
return res;
}
int mid = (l + r) / 2;
vector<int> f = solve(l, mid);
vector<int> g = solve(mid + 1, r);
int tdeg = f.size() + g.size() - 2;
int len = get(tdeg + 1);
f.resize(len, 0);
g.resize(len, 0);
ntt(f, len, 1);
ntt(g, len, 1);
for(int i = 0; i < len; i++) {
f[i] = 1ll * f[i] * g[i] % M;
}
ntt(f, len, -1);
f.resize(tdeg + 1);
return move(f);
}
int main() {
fact[0] = 1;
for(int i = 1; i < N; i++) {
fact[i] = 1ll * fact[i - 1] * i % M;
inv[i] = qpow(i, M - 2 ,M);
}
for(int i = 1; i < N; i <<= 1) {
ngn[i] = qpow(3, (M - 1) / i, M);
rgn[i] = qpow(ngn[i], M - 2, M);
}
int t;
scanf("%d", &t);
while(t--) {
int n;
scanf("%d", &n);
for(int i = 1; i <= n; i++) scanf("%d", &arr[i]);
vector<int> res = solve(1, n);
ll ans = 0;
for(int i = 1; i <= n; i++) {
ans += 1ll * fact[i] * fact[n - i] % M * res[i] % M;
}
ans %= M;
printf("%d\n", ans * qpow(fact[n] , M - 2, M) % M);
}
}
C - Club Assignment(分治,暴力)
将所有数排序,然后枚举最高位,每次可以分为最高位为0/1两部分。可以发现,横跨两组的数之间异或起来一定比组内的之间异或的大,即横跨两组的数之间异或对答案没有贡献(因为不会是最小值),即两组之间没有分配的限制,不用管。于是就可以继续分成两部分处理。
一直分下去,如果都是相同的数,相同的数大于2,说明答案为0;否则将它们分到不同的集合。否则最后一定会分成小于等于4的集合。对于每个小于等于4的集合,直接暴力枚举每一种分配的组合,从中选择结果最大的分配。最后答案就是所有这些结果的最小值。
这个最优的分配方案和异或最小生成树也有关。
#include <bits/stdc++.h>
typedef long long ll;
#define endl '\n'
using namespace std;
const int N = 3e5 + 10;
const int M = 998244353;
#define INF 0x3f3f3f3f3f3f3f3f
typedef pair<int, int> PII;
PII arr[N];
int ans[N];
ll solve(int l, int r, int cur) {
if(r - l + 1 == 1) {
ans[arr[l].second] = 1;
return INF;
}
if(r - l + 1 <= 4) {
if(r - l + 1 == 2) {
ans[arr[l].second] = 1;
ans[arr[r].second] = 2;
return INF;
}
vector<PII> s1, s2;
ll mx = 0;
for(int i = l; i <= r; i++) {
for(int j = i + 1; j <= r; j++) {
vector<PII> a, b;
a.push_back(arr[i]);
a.push_back(arr[j]);
for(int k = l; k <= r; k++) {
if(k == i || k == j) continue;
b.push_back(arr[k]);
}
ll va = INF, vb = INF;
va = a.front().first ^ a.back().first;
if(b.size() > 1) vb = b.front().first ^ b.back().first;
if(min(va, vb) >= mx) {
s1 = move(a);
s2 = move(b);
mx = min(va, vb);
}
}
}
for(auto p : s1) ans[p.second] = 1;
for(auto p : s2) ans[p.second] = 2;
return mx;
}
if(cur < 0) {
if(r - l + 1 >= 3) {
for(int i = l; i <= r; i++) ans[arr[i].second] = 1;
return 0;
}
if(r - l + 1 == 2) {
ans[arr[l].second] = 1;
ans[arr[r].second] = 2;
} else {
ans[arr[l].second] = 1;
}
return INF;
}
int p = l;
while(p <= r) {
if((arr[p].first & (1 << cur))) {
break;
}
p++;
}
ll mi = INF;
if(p > l) mi = min(mi, solve(l, p - 1, cur - 1));
if(p <= r) mi = min(mi, solve(p, r, cur - 1));
return mi;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int t;
cin >> t;
while(t--) {
int n;
cin >> n;
for(int i = 1; i <= n; i++) {
int x;
cin >> x;
arr[i] = {x, i};
}
sort(arr + 1, arr + 1 + n);
cout << solve(1, n, 30) << endl;
for(int i = 1; i <= n; i++) {
cout << ans[i];
}
cout << endl;
}
}
G - Game on Sequence(暴力)
设\(f(i)=1\),代表棋子到位置\(i\)是必胜的,反之亦然。然后就有很简单的转移方程
\(j\)代表\(i\)能转移到的位置。显然这样时间复杂度太高。观察发现,如果位置\(i\)的值为\(A\),在它之后也有一个位置\(j\)值为\(A\),那么\(f(i)=1\)。因为如果\(f(j)=0\),有\(f(i)=1\);如果\(f(j)=1\),说明\(j\)之后有个位置\(k\)有\(f(k)=0\),那么就有\(f(i)=1\)。因此只需维护最后面的不同的\(A\)的对应位置的\(f\)值即可。\(A\)的值域只有255,直接暴力计算即可。时限为6s,非常充裕。
#include <bits/stdc++.h>
#define endl '\n'
#define IOS std::ios::sync_with_stdio(0); cin.tie(0); cout.tie(0)
#define mp make_pair
#define seteps(N) fixed << setprecision(N)
typedef long long ll;
using namespace std;
/*-----------------------------------------------------------------*/
ll gcd(ll a, ll b) {return b ? gcd(b, a % b) : a;}
#define INF 0x3f3f3f3f
const int N = 5e5 + 10;
const int M = 500;
const double eps = 1e-5;
int pos[500];
int num[500];
int npos[500];
bitset<500> vis;
bitset<500> flag;
int arr[N];
vector<int> np[500];
int len;
void add(int x, int p) {
if(!npos[x]) {
npos[x] = ++len;
num[len] = x;
} else {
for(int i = npos[x]; i + 1 <= len; i++) {
num[i] = num[i + 1];
npos[num[i]] = i;
}
num[len] = x;
npos[x] = len;
}
pos[x] = p;
arr[p] = x;
}
bool que(int p) {
if(p < pos[arr[p]]) return true;
vis.reset();
flag.reset();
for(int i = len; i >= 1; i--) {
int x = num[i];
bool ok = 0;
for(int nt : np[x]) {
if(vis[nt]) {
ok |= (!flag[nt]);
}
}
flag.set(x, ok);
vis.set(x);
}
return flag[arr[p]];
}
bool chk(int a, int b) {
int c = a ^ b;
int cnt = 0;
while(c) {
if(c & 1) cnt++;
c >>= 1;
}
return cnt <= 1;
}
int main() {
IOS;
for(int i = 0; i < 256; i++) {
for(int j = 0; j < 256; j++) {
if(chk(i, j)) np[i].push_back(j);
}
}
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i++) {
int x;
cin >> x;
add(x, i);
}
int cur = n + 1;
while(m--) {
int op;
cin >> op;
if(op == 1) {
int x;
cin >> x;
add(x, cur++);
} else {
int p;
cin >> p;
if(que(p)) cout << "Grammy" << endl;
else cout << "Alice" << endl;
}
}
}
I - Nim Cheater(轻重链性质)
Bob必胜就是石子数异或和为0,那么用简单的背包dp,就可以得到答案。
每次都在序列尾部加入和删除一个数的一系列操作,可以构成一颗树。加入操作等价于在当前结点下插入一个结点;删除操作相当于返回父亲结点。
因此构造出这个树,然后直接在树上跑dp即可。空间复杂度为\(O(nm)\),其中\(n\)代表操作数(结点数),\(m\)代表石头数值域。
显然这样空间会超,因此题解提供一个优秀的解法:找到这个树轻重儿子,因为每个结点最多只有一个重儿子,每条路径最多包含\(\log n\)个轻儿子,因此可以先遍历轻儿子,再遍历重儿子;每次到轻儿子存下当前dp状态,等返回时再还原;重儿子则直接更新dp状态。这样空间复杂度就只有\(O(m\log n)\)。
#include <bits/stdc++.h>
#define endl '\n'
#define IOS std::ios::sync_with_stdio(0); cin.tie(0); cout.tie(0)
#define mp make_pair
#define seteps(N) fixed << setprecision(N)
typedef long long ll;
using namespace std;
/*-----------------------------------------------------------------*/
ll gcd(ll a, ll b) {return b ? gcd(b, a % b) : a;}
#define INF 0x3f3f3f3f
const int N = 2e4 + 10;
const int M = 16385;
const double eps = 1e-5;
int ans[N];
int fa[N];
int val[N], cost[N];
vector<int> np[N];
int si;
bool hson[N];
int dfs(int p) {
int cnt = 1;
int mx = 0, tar = -1;
for(int nt : np[p]) {
int res = dfs(nt);
if(res > mx) {
mx = res;
tar = nt;
}
cnt += res;
}
if(tar >= 0) hson[tar] = 1;
return cnt;
}
int res[M];
void solve(int p, int tot) {
int *bk;
if(!hson[p]) {
bk = new int[M];
for(int i = 0; i < M; i++) bk[i] = res[i];
}
for(int i = 0; i < M; i++) {
res[i ^ val[p]] = min(res[i ^ val[p]], res[i] + cost[p]);
}
ans[p] = res[tot ^ val[p]];
int tar = -1;
for(int nt : np[p]) {
if(hson[nt]) {
tar = nt;
continue;
}
solve(nt, tot ^ val[p]);
}
if(tar >= 0) solve(tar, tot ^ val[p]);
if(!hson[p]) {
for(int i = 0; i < M; i++) res[i] = bk[i];
delete [] bk;
}
}
int num;
void printans(int p) {
if(!num) return ;
if(p) {
cout << ans[p] << endl;
num--;
}
for(int nt : np[p]) {
if(!num) return ;
printans(nt);
if(!num) return ;
cout << ans[p] << endl;
num--;
}
}
int main() {
IOS;
int n;
cin >> n;
int cur = 0;
for(int i = 1; i <= n; i++) {
string op;
cin >> op;
if(op == "ADD") {
si++;
cin >> val[si] >> cost[si];
np[cur].push_back(si);
fa[si] = cur;
cur = si;
} else {
cur = fa[cur];
}
}
memset(res, INF, sizeof res);
res[0] = 0;
dfs(0);
solve(0, 0);
num = n;
printans(0);
}