2024 暑假友誼賽 3

Ke_scholar發表於2024-08-04

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 最終轉移方程。

程式碼

#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;
}

相關文章