训练赛 - 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\)这样的式子。关键是求每一项的总和。就是每一项取与不取的,即

\[\prod_{i=1} ^{n}{(1+a_ix)} \]

然后多项式每一项的系数就是需要的值。还需要乘上每一项对应的序列前面的组合个数。

有点卡时间,可以预处理出原根的次幂,减少常数。

#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\)是必胜的,反之亦然。然后就有很简单的转移方程

\[f(i)=\operatorname{NOT}{(\operatorname{AND} f(j))} \]

\(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);
}
posted @ 2021-10-15 19:52  limil  阅读(496)  评论(0编辑  收藏  举报