2024 暑假友谊赛 3

2024 暑假友谊赛 3

A - A

CodeForces - 1187E

思路

\(f_i\) 表示以 \(i\) 为根的子树产生的贡献,则有 \(f_i=size_i+\sum\limits_{j\in son_i} f_j\),即起初选定 \(i\) 为起点后产生 \(size_i\) 的贡献,后续是它的子树产生的贡献。

但这样以不同根节点去求贡献是 \(O(n^2)\) 的,所以考虑换根 dp。

\(dp_i\) 表示为以 \(i\) 为根的答案。

image

以上图为例,设整棵树大小为 \(n\) ,我们要将 \(x\) 的答案换根到 \(y\) 上:

\[dp_x=n+\sum\limits_{v\in substree_x}f_v(\text{$substrr_x$表示x的子树})\\ \]

\[dp_x=n+\sum\limits_{v\in Son_x|v\ne y}f_v+f_y\\ \]

\[dp_x=n+\sum\limits_{v\in Son_x|v\ne y}f_v+size_y+\sum\limits_{u\in Son_y|u\ne x}f_u\\ \]

\[dp_x=n+\sum\limits_{v\in Son_x|v\ne y}f_v+(n-size_y) +size_y+\sum\limits_{u\in Son_y|u\ne x}f_u-(n-size_y)\\ \]

\[dp_x=n+f_x+size_y+\sum\limits_{u\in Son_y|u\ne x}f_u-(n-size_y)\\ \]

\[且 dp_y=n+\sum\limits_{u\in subtree_y}f_u=n+f_x+\sum\limits_{u\in Son_y|u\ne x}f_u\\ \]

\[\therefore dp_x=dp_y+2\times size_y-n\\ \]

\[\therefore dp_y=n+dp_x-2\times size_y \]

即换根 dp 最终转移方程,当 \(n=1\) 时,有 \(dp_1=f_1\)

代码

#include <bits/stdc++.h>

using namespace std;

using i64 = long long;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;

    vector g(n + 1, vector<int>());
    for (int i = 1; i < n; i ++) {
        int u, v;
        cin >> u >> v;
        g[u].emplace_back(v);
        g[v].emplace_back(u);
    }

    vector<i64> f(n + 1), dp(n + 1), siz(n + 1);
    auto dfs = [&](auto && self, int u, int fa)->void{
        siz[u] = 1;
        for (auto v : g[u]) {
            if (v == fa) continue;
            self(self, v, u);
            siz[u] += siz[v];
            f[u] += f[v];
        }
        f[u] += siz[u];
    };

    dfs(dfs, 1, 0);

    i64 ans = 0;
    ans = dp[1] = f[1];

    auto dpdfs = [&](auto && self, int u, int fa)->void{
        if (u != 1) {
            dp[u] = dp[fa] + n - 2 * siz[u];
            ans = max(ans, dp[u]);
        }

        for (auto v : g[u]) {
            if (v == fa) continue;
            self(self, v, u);
        }
    };

    dpdfs(dpdfs, 1, 0);

    cout << ans << '\n';

    return 0;
}

B - B

CodeForces - 977D

思路

很经典的一道题,把能被 \(3\) 整除或者能 \(× 2\) 得到的数看成由 \(x\)\(y\) 的一条有向边,这样就转换成了 \(DAG\) 模型,要求一条长度为 \(n\) 的变化方案,其实就是这个模型上的最长路,上拓扑序即可。

代码

#include <bits/stdc++.h>

using namespace std;

using i64 = long long;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;

    vector<i64> a(n + 1);
    for (int i = 1; i <= n; i ++)
        cin >> a[i];

    vector<int> in(n + 1);
    vector g(n + 1, vector<int>());
    for (int i = 1; i <= n; i ++) {
        for (int j = 1; j <= n; j ++) {
            if (j == i) continue;
            if (a[i] * 2 == a[j]) {
                g[i].push_back(j);
                in[j] ++;
            }
            if (a[i] % 3 == 0 && a[i] / 3 == a[j]) {
                g[i].push_back(j);
                in[j] ++;
            }
        }
    }

    queue<int> Q;
    for (int i = 1; i <= n; i ++) {
        if (!in[i]) {
            Q.push(i);
        }
    }

    vector<int> ans;
    while (Q.size()) {
        auto u = Q.front();
        Q.pop();

        ans.push_back(u);
        for (auto v : g[u]) {
            if (!--in[v]) {
                Q.push(v);
            }
        }
    }

    for (auto i : ans)
        cout << a[i] << " \n"[i == ans.back()];

    return 0;
}

C - C

CodeForces - 1368D

思路

注意到选择的 \(x\)\(y\) 会变成两个数 \(x\& y\)\(x|y\),其实以二进制的角度来看,就是这两个数的对应位上的 \(1\) 发生了转移,但是总共的 \(1\) 的个数未变,题目要求 \(a_i^2\),则 \(a_i\) 应该越大对答案的贡献才会越大,所以存下每个数对应位上的 \(1\) 有多少个,贪心地去凑出最大的数即可。

代码

#include <bits/stdc++.h>

using namespace std;

using i64 = long long;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;

    vector<i64> a(n + 1), cnt(30);
    for (int i = 1; i <= n; i ++) {
        cin >> a[i];
        for (int j = 0; j < 20; j ++) {
            if (a[i] >> j & 1)
                cnt[j] ++;
        }
    }

    i64 ans = 0;
    for (int i = 1; i <= n; i ++) {
        int x = 0;
        for (int j = 0; j < 20; j ++) {
            if (cnt[j]) {
                x += 1 << j;
                cnt[j] --;
            }
        }
        ans += 1ll * x * x;
    }

    cout << ans << '\n';

    return 0;
}

D - D

AtCoder - arc082_b

思路

\(p_i=i\) 时,那它和旁边的数交换一定可以使得两边的数都不等于其下标,所以遍历一遍,碰到 \(p_i=i\) 的直接和旁边的数交换一下记录答案即可。

代码

#include <bits/stdc++.h>

using namespace std;

using i64 = long long;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;

    vector<int> p(n + 1);
    for (int i = 1; i <= n; i ++) {
        cin >> p[i];
    }

    int ans = 0;
    for (int i = 1; i <= n; i ++) {
        if (p[i] != i) continue;
        if (i + 1 <= n)
            swap(p[i], p[i + 1]);
        else
            swap(p[i], p[i - 1]);
        ans ++;
    }

    cout << ans << '\n';

    return 0;
}

E - E

CodeForces - 794C

思路

一道细节题。

贪心的思路是排序后 \(a\)\(b\) 依次把最小字母和最大字母往前面填,但当 \(S_a>S_b\) 的时候,\(a,b\) 往前填反而会成全对方,所以这个时候得往后填。

写法不同对细节的处理有不同。

代码

#include <bits/stdc++.h>

using namespace std;

using i64 = long long;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    string a, b;
    cin >> a >> b;

    sort(a.begin(), a.end());
    sort(b.begin(), b.end(), greater<>());

    int n = a.size();

    string ans1 = "", ans2 = "";

    while (a.size() > (n + 1) / 2) a.pop_back();
    while (b.size() > (n) / 2) b.pop_back();

    while (n--) {
        if (a[0] < b[0]) {
            ans1 += a[0];
            a.erase(a.begin());
        } else {
            ans2 += a.back();
            a.pop_back();
        }
        if (n) {
            if (a[0] < b[0]) {
                ans1 += b[0];
                b.erase(b.begin());
            } else {
                ans2 += b.back();
                b.pop_back();
            }
            n--;
        }
    }

    reverse(ans2.begin(), ans2.end());
    cout << ans1 + ans2 << '\n';

    return 0;
}

F - F

CodeForces - 1076E

思路

这几天有点魔怔了,看到子树 (u,v) 对什么的老是想到树上启发式,唉,杭电害得。

\(x\) 子树中距离小于等于 \(k\) 的点全都加上一个值 \(x\),假设这棵树只有一条链,那很显然,答案其实就是做一个差分,然后跑一个前缀和。

现在是多条链,但是这多条链上的点都需要加上 \(x\),那么不妨将深度看成一个序列,在深度上进行差分,用 dfs 跑前缀和,这样就完成了树上差分以及区间求值的操作。

需要注意的是,因为一棵树有不同的子树,也就是有许多不同的链,在对当前子树做完差分的操作,回溯的时候要记得还原,否则会影响其他子树的答案。

代码

#include <bits/stdc++.h>

using namespace std;

using i64 = long long;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;

    vector g(n + 1, vector<int>());
    for (int i = 1; i < n; i ++) {
        int u, v;
        cin >> u >> v;
        g[u].push_back(v);
        g[v].push_back(u);
    }

    int m;
    cin >> m;

    vector Q(n + 1, vector<pair<int, int>>());
    while (m--) {
        int v, d, x;
        cin >> v >> d >> x;
        Q[v].emplace_back(d, x);
    }

    vector<i64> pre(n + 1), ans(n + 1);
    auto dfs = [&](auto && self, int u, int fa, int dep, i64 sum)->void{
        for (auto &[d, x] : Q[u]) {
            pre[dep] += x;
            int to = dep + d + 1;
            if (to <= n) {
                pre[to] -= x;
            }
        }

        sum += pre[dep];
        ans[u] = sum;

        for (auto v : g[u]) {
            if (v == fa) continue;
            self(self, v, u, dep + 1, sum);
        }

        for (auto &[d, x] : Q[u]) {
            pre[dep] -= x;
            int to = dep + d + 1;
            if (to <= n) {
                pre[to] += x;
            }
        }
    };

    dfs(dfs, 1, 0, 0, 0);

    for (int i = 1; i <= n; i ++)
        cout << ans[i] << " \n"[i == n];

    return 0;
}

G - G

CodeForces - 611D

思路

考虑 dp。

\(dp_{i,j}\) 表示以从 \(j\)\(i\) 构成的数字(以下称做\(num_{i,j}\))作为结尾的方案数。

image

\(L=i-j\) 表示为该数字的长度,那么显然,大于这个长度的一定不可能转移过来,而小于这个长度的字符串构成的数字也一定小于 \(num_{i,j}\),那么这一步得到的转移方程为:

\[dp_{i,j}+=\sum\limits_{k=j-L}^{j-1}dp_{j-1,k} \]

这一步可以用前缀和优化。

接下来就是考虑相等的情况,相等的情况下可以通过 \(LCP(\text{最长公共前缀})\)\(n^2\) 算法预处理,然后判断 \(lcp\) 后的第一位大小情况即可,也可以通过 \(Sam\) 算法等。

\[dp_{i,j}+=dp_{j-1,k-1}[num_{i,j}>num_{j-1,k-1}] \]

我这里采用的是 二分+Hash 的做法(也有倍增+Hash),会比 \(n^2\) 的做法多一个 \(log\),总复杂度 \(O(n^2logn)\)

代码

#include <bits/stdc++.h>

using namespace std;

using i64 = long long;

struct Hash {
    using u64 = unsigned long long;
    u64 base = 13331;
    vector<u64> pow, hash;
    Hash(string &s) {
        int N = s.size();
        pow.resize(N + 1), hash.resize(N + 1);
        pow[0] = 1, hash[0] = 0;
        for (int i = 1; i < s.size(); i ++) {
            pow[i] = pow[i - 1] * base;
            hash[i] = hash[i - 1] * base + s[i];
        }
    }

    u64 get(int l, int r) {
        return hash[r] - hash[l - 1] * pow[r - l + 1];
    }

    //拼接两个子串
    u64 link(int l1, int r1, int l2, int r2) {
        return get(l1, r1) * pow[r2 - l2 + 1] + get(l2, r2);
    }

    bool same(int l1, int r1, int l2, int r2) {
        return get(l1, r1) == get(l2, r2);
    }

};

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;

    string s;
    cin >> s;

    s = " " + s;
    Hash hash(s);

    auto check = [&](int x, int y)->bool{
        int l = 0, r = y - x, ans = 0;
        while (l <= r) {
            int mid = l + r >> 1;
            if (hash.same(x, x + mid - 1, y, y + mid - 1)) l = mid + 1, ans = mid;
            else r = mid - 1;
        }

        if (ans == y - x) return false;
        return s[ans + x] < s[ans + y];
    };

    vector dp(n + 1, vector<i64>(n + 1));
    vector sum(n + 1, vector<i64>(n + 1));
    for (int i = 1; i <= n; i ++)
        dp[i][1] = 1;

    const i64 mod = 1e9 + 7;

    for (int i = 1; i <= n; i ++) {
        for (int j = 1; j <= i; j ++) {
            if (s[j] == '0') continue;
            int k = max(1, j - (i - j));
            (dp[i][j] += (sum[j - 1][j - 1] - sum[j - 1][k - 1] + mod) % mod) %= mod;
            k --;
            if (k >= 1 && check(k, j)) {
                (dp[i][j] += dp[j - 1][k]) %= mod;
            }
        }
        for (int j = 1; j <= i; j ++)
            sum[i][j] = (sum[i][j - 1] + dp[i][j]) % mod;
    }

    i64 ans = 0;
    for (int i = 1; i <= n; i ++)
        ans = (ans + dp[n][i]) % mod;

    cout << ans << '\n';

    return 0;
}

H - H

CodeForces - 1416B

思路

要获得均分首先得保证 \(Sum_a\bmod n=0\)

考虑一种做法就是,首先将所有的数都汇集到 \(a_1\) 上,然后其他数就是 \(0\) 了,然后由 \(a_1\) 统一分配 \(Avg\) 平均数给其他 \(n-1\) 个数,这样的做法有 \(2\times (n-1)\) 次操作。

期间会有一些数会产生余数,那么不妨让 \(a_1\) ‘借’点数给它使得被 \(i\) 整除,然后又一并还给 \(a_1\),题目保证 \(1\le a_i\le 1e5\),所以最开始一定有 \(1\) 去弥补 \(i=2\) 的余数,然后 \(a_2\) 把所有数给 \(a_1\) 后,又能保证有一定的数弥补 \(i=3 \dots\) 这样的操作最多也就 \((n-1)\) 次,所以总次数不会超过 \(3(n-1)\),满足题目要求。

代码

#include <bits/stdc++.h>

using namespace std;

using i64 = long long;

void solve() {

    int n;
    cin >> n;

    i64 sum = 0;
    vector<int> a(n + 1);
    for (int i = 1; i <= n; i ++) {
        cin >> a[i];
        sum += a[i];
    }

    if (sum % n != 0) {
        cout << -1 << '\n';
        return ;
    }

    int avg = sum / n;
    vector<array<int, 3>> ans;
    for (int i = 2; i <= n; i ++) {
        int x;
        if (a[i] % i != 0) {
            x = i - a[i] % i;
            a[1] -= x;
            a[i] += x;
            ans.push_back({1, i, x});
        }
        x = a[i] / i;
        a[1] += a[i];
        a[i] = 0;
        ans.push_back({i, 1, x});
    }

    for (int i = 2; i <= n; i ++) {
        ans.push_back({1, i, avg - a[i]});
    }

    cout << ans.size() << '\n';
    for (auto [a, b, c] : ans) {
        cout << a << ' ' << b << ' ' << c << '\n';
    }

}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int t;
    cin >> t;
    while (t--) {
        solve();
    }

    return 0;
}
posted @ 2024-08-04 01:21  Ke_scholar  阅读(2)  评论(0编辑  收藏  举报