Atcoder Beginner Contest 267

C. Index × A(Continuous ver.)

题目大意

给定一个序列\(A\),找一长度为 \(m\)的连续子序列\(B\),其 \(\sum\limits_{i=1}^{m} i \times B_i\)值最大,求该最大值。

解题思路

枚举该子序列即可,注意到下一个子序列的值可以由上一个子序列的值\(O(1)\)转移过来,它们就相差了一个区间和\(sum[i-1] - sum[i - 1 - m]\)以及 \(m\times A[i]\)

神奇的代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;

int main(void) {
    ios::sync_with_stdio(false); 
    cin.tie(0); cout.tie(0);
    int n, m;
    cin >> n >> m;
    vector<LL> a(n + 1, 0);
    LL ans = -1e18;
    LL sum = 0;
    LL tmp = 0;
    for(int i = 1; i <= n; ++ i){
        cin >> a[i];
        if (i <= m){
            sum += a[i];
            tmp += a[i] * i;
        }else{
            ans = max(ans, tmp);
            tmp -= sum;
            sum -= a[i - m];
            sum += a[i];
            tmp += a[i] * m;
        }
    }
    ans = max(ans, tmp);
    cout << ans << endl;
    return 0;
}


D. Index × A(Not Continuous ver.)

题目大意

给定一个序列\(A\),找一长度为 \(m\)的(不连续)子序列\(B\),其 \(\sum\limits_{i=1}^{m} i \times B_i\)值最大,求该最大值。

解题思路

\(dp[i][j]\)表示前 \(i\)个数选了 \(j\)个数的最大值,对于第\(i+1\)个数考虑选或不选转移即可。

神奇的代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;

int main(void) {
    ios::sync_with_stdio(false); 
    cin.tie(0); cout.tie(0);
    int n, m;
    cin >> n >> m;
    vector<vector<LL>> dp(n + 1, vector<LL>(m + 1, -1e18));
    dp[0][0] = 0;
    for(int i = 1; i <= n; ++ i){
        LL a;
        cin >> a;
        dp[i][0] = 0;
        for(int j = 1, up = min(i, m); j <= up; ++ j){
            dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - 1] + a * j);
        }
    }
    cout << dp[n][m] << '\n';
    return 0;
}


E. Erasing Vertices 2

题目大意

给定一张\(n\)个点 \(m\)条边的无向图,点有点权。需要进行\(n\)次操作,每次操作,选择一个点\(a\),并移除该点以及与该点相连的所有边,其代价是与点\(a\)直接相连的所有点权和。问所有操作的代价的最大值的最小值是多少。

解题思路

我们每次移除当前代价最小的那个,那么最终一定是最大值最小的情况。于是我们用优先队列维护这个代价最小的那个点,移除后暴力修改周围点的代价,并把新的代价加入到优先队列里。

优先队列里出队时先看其代价是不是当前点的代价,就能判断这个是不是最新的代价了。

暴力修改的复杂度其实就是边的数量,因此总的时间复杂度是\(O((n+m)\log (n+m) + m)\)

神奇的代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;

int main(void) {
    ios::sync_with_stdio(false); 
    cin.tie(0); cout.tie(0);
    int n, m;
    cin >> n >> m;
    vector<LL> a(n);
    for(auto &i : a)
        cin >> i;
    vector<vector<int>> edge(n, vector<int>());
    vector<LL> sum(n, 0);
    for(int i = 1; i <= m; ++ i){
        int x, y;
        cin >> x >> y;
        -- x;
        -- y;
        edge[x].push_back(y);
        edge[y].push_back(x);
        sum[x] += a[y];
        sum[y] += a[x];
    }
    priority_queue<pair<LL, int>> qwq;
    for(int i = 0; i < n; ++ i){
        qwq.push({- sum[i], i});
    }
    LL ans = 0;
    int cnt = n;
    set<int> ff;
    while(cnt){
        auto [val, u] = qwq.top();
        val = -val;
        qwq.pop();
        if (val != sum[u])
            continue;
        ff.insert(u);
        ans = max(ans, val);
        cnt --;
        for(auto &v : edge[u]){
            if (ff.find(v) != ff.end())
                continue;
            sum[v] -= a[u];
            qwq.push({-sum[v], v});
        }
    }
    cout << ans << '\n';
    return 0;
}


F. Exactly K Steps

题目大意

给定一棵树,有\(q\)个询问,第 \(i\)个询问求任意一个与点 \(u_i\) 距离\(k_i\)的点的下标。

解题思路

考虑到树上往父亲跑容易,但往儿子跑不太容易,因此我们都尽量往父亲跑。

注意到,任意一个点与树上的点距离最远的,一定是这棵树的直径上的点,这个从直径的证明里可以得出。

因此先跑两次\(dfs\)求得树的直径,然后再分别以直径两点为根,预处理往上跳的倍增数组。对于询问中的点\(u_i\),就在距离根节点最远的那棵树往上跳\(k_i\)步就能得到答案。

神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
#define FOR(i, x, y) for (decay<decltype(y)>::type i = (x), _##i = (y); i < _##i; ++i)
#define FORD(i, x, y) for (decay<decltype(x)>::type i = (x), _##i = (y); i > _##i; --i)

const int N = 2e5 + 8;
const int SP = 25;
vector<int> G[N];
int n, q;

struct tu{

    int up[N][SP];
    int dep[N];

    void dfs(int u, int fa) {
        up[u][0] = fa; dep[u] = dep[fa] + 1;
        FOR (i, 1, SP) up[u][i] = up[up[u][i - 1]][i - 1];
        for (int& v: G[u]) {
            if (v == fa) continue;
            dfs(v, u);
        }
    }

    int lca(int u, int t) {
        FOR (i, 0, SP) if (t & (1 << i)) u = up[u][i];
        return u;
    }
}l1, l2;

int dep[N];

int solve(int u, int fa) {
    dep[u] = dep[fa] + 1;
    int maxDeep = u;
    for (int& v: G[u]) {
        if (v == fa) continue;
        int maxd = solve(v, u);
        if (dep[maxDeep] < dep[maxd])
            maxDeep = maxd;
    }
    return maxDeep;
}

int main(void) {
    ios::sync_with_stdio(false); 
    cin.tie(0); cout.tie(0);
    cin >> n;
    for(int i = 1; i < n; ++ i){
        int u, v;
        cin >> u >> v;
        G[u].push_back(v);
        G[v].push_back(u);
    }
    int l = solve(1, 1);
    dep[l] = 0;
    int r = solve(l, l);
    l1.dfs(l, l);
    l2.dfs(r, r);
    cin >> q;
    tu* L;
    for(int i = 1; i <= q; ++ i){
        int u, k;
        cin >> u >> k;
        if (l1.dep[u] < l2.dep[u]){
            L = &l2;
        }else {
            L = &l1;
        }
        if (L->dep[u] <= k)
            cout << -1 << '\n';
        else 
            cout << L->lca(u, k) << '\n';
    }
    return 0;
}


G. Increasing K Times

题目大意

给定一个序列\(A\),可打乱其排列顺序,问有多少个排列情况满足,恰好有\(k\)个数对,前者小于后者。

解题思路

神奇的代码


Ex. Odd Sum

题目大意

给定一个序列\(A\),其中 \(1 \leq A_i \leq 10\),问有多少个长度为奇数的子序列其和为\(M\)

解题思路

朴素的\(DP\)很容易可以想到设 \(dp[i][0/1][j]\)表示前 \(i\)个数,选了偶数/奇数个,和为 \(j\)的方案数。虽然第一维可以再压缩一下,将 \(1-10\)\(10\)个数依次考虑,考虑选多少个数出来,最后再 \(C\)一下,但因为 \(j\)状态里面,\(O(NM)\)的状态始终降不下来。

这里考虑下转移策略,我们的转移方程始终是从\(i-1\)转移过来的,即 \((1,i-1)\)的状态和 \((i,i)\) 的状态结合,得到了\((1,i)\)的状态。每次转移长度都增加 \(1\)

那么考虑倍增地方式结合。一开始我们有 \(n\)个状态,分别是 \((1,1),(2,2),...,(n,n)\)。然后像线段树合并或者分治合并的方式,先让 \((1,1),(2,2)\)合并成 \((1,2)\)\((3,3),(4,4)\)合并成 \((3,4)\),然后 \((1,2),(3,4)\)合并成 \((1,4)\)这样,这样我们每次合并时的长度都会翻倍,因此我们只需要合并 \(\log n\)次就可以得到\((1,n)\)的状态,也即 \(dp[n]\)

而对于合并,观察\(dp\)转移方程\(dp[i][0][j] = dp[i - 1][0][k] + dp[i - 1][0][j - k]......\) ,第三维是个卷积形式,因此我们将\(dp[i][0]\)\(dp[i][1]\)分别看成一个多项式,其中 \(x_j\)的系数为 \(dp[i][0][j]\)\(dp[i][1][j]\),转移的时候就是两个多项式相乘,用 \(NTT\)加速计算。

合并两个多项式也即:

\[dp[i][0] = dp[j][0] \times dp[k][0] + dp[j][1] \times dp[k][1] \]

\[dp[i][1] = dp[j][0] \times dp[k][1] + dp[j][1] \times dp[k][0] \]

理论时间复杂度是\(O(M\log M\log n)\),但由于中间实现会产生较多次的临时数组创建开销,不知该怎么优雅的实现榜一3分钟A的代码还确实用这种做法能过,但其贴了多项式全家桶长达800行不知怎么做到的

有个一个小小的优化,就是由于只有\(10\)个数,我们就预处理每一个数的选择方案,也即 \(dp[i][0/1]\)表示一个由选择了偶数个/奇数个\(i\)的多项式, 其中\(x_j\)的系数表示和为 \(j=i\times cnt\)的方案数,其实就是个组合数\(C^{cnt}_{num_i}\),其中\(num_i\)表示序列\(A\)\(i\)个个数。然后我们只需要做 \(10\)\(NTT\)就可以了。

神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
#define FOR(i, x, y) for (decay<decltype(y)>::type i = (x), _##i = (y); i < _##i; ++i)
#define FORD(i, x, y) for (decay<decltype(x)>::type i = (x), _##i = (y); i > _##i; --i)

const int N = 1e5 + 8;
const LL MOD = 998244353;
const int G = 3;
int n, m;
int cnt[11];
vector<LL> dp[11][2];
vector<LL> f[2], g[2];

LL bin(LL x, LL n, LL MOD) {
    LL ret = MOD != 1;
    for (x %= MOD; n; n >>= 1, x = x * x % MOD)
        if (n & 1) ret = ret * x % MOD;
    return ret;
}
 
inline LL get_inv(LL x, LL p) { return bin(x, p - 2, p); }
 
LL wn[(N * 10) << 2], rev[(N * 10) << 2];
int NTT_init(int n_) {
    int step = 0; int n = 1;
    for ( ; n < n_; n <<= 1) ++step;
    FOR (i, 1, n)
        rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (step - 1));
    int g = bin(G, (MOD - 1) / n, MOD);
    wn[0] = 1;
    for (int i = 1; i <= n; ++i)
        wn[i] = wn[i - 1] * g % MOD;
    return n;
}
 
void NTT(vector<LL>& a, int n, int f) {
    FOR (i, 0, n) if (i < rev[i])
        std::swap(a[i], a[rev[i]]);
    for (int k = 1; k < n; k <<= 1) {
        for (int i = 0; i < n; i += (k << 1)) {
            int t = n / (k << 1);
            FOR (j, 0, k) {
                LL w = f == 1 ? wn[t * j] : wn[n - t * j];
                LL x = a[i + j];
                LL y = a[i + j + k] * w % MOD;
                a[i + j] = (x + y) % MOD;
                a[i + j + k] = (x - y + MOD) % MOD;
            }
        }
    }
    if (f == -1) {
        LL ninv = get_inv(n, MOD);
        FOR (i, 0, n)
            a[i] = a[i] * ninv % MOD;
    }
}
 
vector<LL> operator+(vector<LL> a, const vector<LL>& b){
    a.resize(max(a.size(), b.size()));
    for(int i = 0; i < b.size(); ++ i)
        a[i] = (a[i] + b[i]) % MOD;
    return a;
}
 
vector<LL> conv(vector<LL> a, vector<LL> b) {
    int len = a.size() + b.size() - 1;
    int n = NTT_init(len);
    a.resize(n);
    b.resize(n);
    NTT(a, n, 1);
    NTT(b, n, 1);
    FOR (i, 0, n)
        a[i] = a[i] * b[i] % MOD;
    NTT(a, n, -1);
    a.resize(len);
    return a;
}

LL invf[N], fac[N] = {1};
void fac_inv_init(LL n, LL p) {
    FOR (i, 1, n)
        fac[i] = i * fac[i - 1] % p;
    invf[n - 1] = bin(fac[n - 1], p - 2, p);
    FORD (i, n - 2, -1)
        invf[i] = invf[i + 1] * (i + 1) % p;
}

LL C(int n, int m){
    if (n < m)
        return 0;
    return fac[n] * invf[m] % MOD * invf[n - m] % MOD;
}

int main(void) {
    ios::sync_with_stdio(false); 
    cin.tie(0); cout.tie(0);
    cin >> n >> m;
    fac_inv_init(n + 1, MOD);
    for(int i = 1; i <= n; ++ i){
        int a;
        cin >> a;
        cnt[a] ++;
    }
    for(int i = 1; i <= 10; ++ i){
        dp[i][0].resize(cnt[i] * i + 1);
        dp[i][1].resize(cnt[i] * i + 1);
        for(int j = 0; j <= cnt[i]; ++ j){
            dp[i][j & 1][i * j] = C(cnt[i], j);
        }
    }
    f[0].resize(1);
    f[0][0] = 1;
    for(int i = 1; i <= 10; ++ i){
        g[0] = conv(f[0], dp[i][0]) + conv(f[1], dp[i][1]);
        g[1] = conv(f[1], dp[i][0]) + conv(f[0], dp[i][1]);
        f[0].swap(g[0]);
        f[1].swap(g[1]);
    }
    if (f[1].size() <= m)
        cout << 0 << '\n';
    else 
        cout << f[1][m] << '\n';
    return 0;
}


posted @ 2022-09-06 11:27  ~Lanly~  阅读(128)  评论(0编辑  收藏  举报