AtCoder Beginner Contest 369

~Lanly~發表於2024-09-07

A - 369 (abc369 A)

題目大意

給定兩個數\(a,b\),問有多少個整數\(x\),使得 \(a,b,x\)經過某種排列後成為等差數列,

解題思路

就三種情況:\(xab\)\(axb\)\(abx\),三種情況都求出來,然後放到 set去重即為答案。中間的情況要判斷是否是實數。

神奇的程式碼
#include <bits/stdc++.h>
using namespace std;
using LL = long long;

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int a, b;
    cin >> a >> b;
    if (a > b)
        swap(a, b);
    set<int> ans{a - (b - a), b + (b - a)};
    if ((b - a) % 2 == 0)
        ans.insert(a + (b - a) / 2);
    cout << ans.size() << '\n';

    return 0;
}



B - Piano 3 (abc369 B)

題目大意

談鋼琴,給出左右手依次要彈奏的鍵,問左右手移動的距離數。

解題思路

模擬即可,用一個map記錄左右手當前位置,然後移動到下一個位置時計算距離,累計求和集合。

神奇的程式碼
#include <bits/stdc++.h>
using namespace std;
using LL = long long;

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n;
    cin >> n;
    int ans = 0;
    map<char, int> pos;
    while (n--) {
        int p;
        string s;
        cin >> p >> s;
        if (pos.find(s[0]) != pos.end()) {
            ans += abs(pos[s[0]] - p);
        }
        pos[s[0]] = p;
    }
    cout << ans << '\n';

    return 0;
}



C - Count Arithmetic Subarrays (abc369 C)

題目大意

給定一個陣列\(a\),問有多少個 \(l,r\),使得 \(a[l..r]\)是一個等差數列。

解題思路

等差數列即公差相等。從\(a\)的差分陣列\(b\)來看, \(a[l..r]\)是等差數列,意味著差分陣列的對應區間的數是相等的,那就是說,對於\(a[l..r]\)是等差數列的 \(l,r\)對數,等價於 \(b[i..j]\)是相同數的對數。(特判下\(a[l..r]\)長度是 \(1\)的情況)

那先求 \(a\)的差分陣列\(b\),然後對該差分陣列的相同數的區間,比如 \(b[i..j] = c\),那麼對於\(a\)陣列符合條件的 \(l,r\) 就有 \(\frac{(j - i + 1)(j - 1)}{2}\)種取法。

對每個這樣的區間累計求和即為答案。

神奇的程式碼
#include <bits/stdc++.h>
using namespace std;
using LL = long long;

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n;
    cin >> n;
    vector<int> a(n);
    for (auto& i : a)
        cin >> i;
    vector<int> l(n);
    int la = -1;
    int cnt = 0;
    LL ans = n;
    for (int i = 1; i < n; ++i) {
        if (a[i] - a[i - 1] != la) {
            ans += 1ll * cnt * (cnt - 1) / 2;
            cnt = 2;
            la = a[i] - a[i - 1];
        } else {
            ++cnt;
        }
    }
    ans += 1ll * cnt * (cnt - 1) / 2;
    cout << ans << '\n';

    return 0;
}



D - Bonus EXP (abc369 D)

題目大意

\(n\)個怪獸,你要依次打他們。

對於第 \(i\)只怪獸,要麼與它戰鬥,要麼放走他。

如果與它戰鬥,你會獲勝,且會獲得 \(x_i\)經驗。如果它是你第偶數只打敗的怪獸,則還可以額外獲得 \(x_i\)經驗,即共獲得雙倍經驗。

問獲得的經驗數的最大值。

解題思路

比較樸素的\(dp\),很顯然對於每隻怪獸考慮打或不打,如果選擇打,其結果會受到 是否是第偶數只打敗這一狀態的影響,因此我們的\(dp\)狀態,除了包含基本狀態 考慮前$i$只怪獸外,還要加上狀態打敗了奇數/偶數只怪獸這一\(0/1\)狀態。

有了這一狀態後,就可以寫出關於經驗的轉移式子了。即 \(dp[i][0/1]\)表示考慮前 \(i\)只怪獸,已經打敗了偶數只/奇數只怪獸時,獲得的最大經驗值。

然後考慮第\(i\)只打或不打 ,得到對應的經驗值,轉移到後續狀態即可。

神奇的程式碼
#include <bits/stdc++.h>
using namespace std;
using LL = long long;

LL inf = 1e18;

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n;
    cin >> n;
    vector<int> a(n);
    for (auto& x : a)
        cin >> x;
    array<LL, 2> dp = {0, -inf};
    for (int i = 0; i < n; i++) {
        array<LL, 2> dp2 = {0, 0};
        dp2[0] = max(dp[0], dp[1] + a[i] + a[i]);
        dp2[1] = max(dp[1], dp[0] + a[i]);
        dp2.swap(dp);
    }
    cout << max(dp[0], dp[1]) << '\n';

    return 0;
}



E - Sightseeing Tour (abc369 E)

題目大意

給定一張無向圖,邊有邊權。

回答\(q\)個詢問。

每個詢問給定 \(k \leq 5\)條邊,表示從\(1 \to n\),必須經過至少一次這些邊,的最短路徑。

解題思路

這裡給的邊數很少。

考慮最簡單的情況,即\(k=1\),給的邊是\(u,v\),那麼很顯然答案就是 \(1 \to u \to v \to n\)或者 \(1 \to v \to u \to n\), 即考慮從\(1\)節點出發,以最短路先到 \(u\)還是先到 \(v\)

這裡 \(k=5\),但情況數仍然不多,我們仍然列舉中途經過的點,共有\(O(k! 2^k)\)種情況(列舉遍歷邊的順序,對於每條邊再列舉訪問端點的順序), \(k=5\)的話就是 \(3e3\),情況數不大,有了經過的點之後,剩下的就是以最短路徑依次遍歷每個點。由於 \(n\leq 400\),可以事先用 \(floyd\)求出任意兩點的距離,然後對於每個詢問,花費 \(O(k! 2^k)\)列舉遍歷點的順序,然後用 \(O(2k)\)計算該順序對應的最短路長度,所有情況取最小即為答案。

總的時間複雜度為\(O(n^3 + q(k! 2^k + k))\)

神奇的程式碼
#include <bits/stdc++.h>
using namespace std;
using LL = long long;

const LL inf = 1e18;

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n, m;
    cin >> n >> m;
    vector<array<int, 3>> edge(m);
    vector<vector<LL>> dis(n, vector<LL>(n, inf));
    for (int i = 0; i < n; i++)
        dis[i][i] = 0;
    for (int i = 0; i < m; i++) {
        int u, v, w;
        cin >> u >> v >> w;
        --u, --v;
        dis[u][v] = min(dis[u][v], (LL)w);
        dis[v][u] = min(dis[v][u], (LL)w);
        edge[i] = {u, v, w};
    }
    for (int k = 0; k < n; ++k) {
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
            }
        }
    }
    int q;
    cin >> q;
    auto calc = [&](vector<int>& p) -> LL {
        LL sum = 0;
        int st = 0;
        for (int i = 0; i < p.size(); i += 2) {
            int u = p[i], v = p[i + 1];
            sum += dis[st][u];
            st = v;
        }
        sum += dis[st][n - 1];
        return sum;
    };

    while (q--) {
        int k;
        cin >> k;
        vector<int> b(k);
        for (auto& x : b) {
            cin >> x;
            --x;
        }
        int up = (1 << k);
        LL ans = inf;
        do {
            for (int i = 0; i < up; ++i) {
                vector<int> p;
                LL sumb = 0;
                for (int j = 0; j < k; ++j) {
                    auto [u, v, w] = edge[b[j]];
                    sumb += w;
                    if ((i >> j) & 1) {
                        swap(u, v);
                    }
                    p.push_back(u);
                    p.push_back(v);
                }
                LL sum = calc(p) + sumb;
                ans = min(ans, sum);
            }
        } while (next_permutation(b.begin(), b.end()));
        cout << ans << '\n';
    }

    return 0;
}



F - Gather Coins (abc369 F)

題目大意

\(h\times w\)網格,有些格子有金幣。

從左上走到右下,只能向右走和向下走。

問取得金幣的最大值。

解題思路

樸素\(dp\)就是 \(dp[i][j] = \max(dp[i - 1][j], dp[i][j - 1]) + (coin[i][j] == 1)\),但\(h \times w\)可達 \(1e10\),整不動。但金幣數最多隻有 \(10^5\),我們知道 \(dp\)的值只有在有金幣的格子才會變動,實際有效的格子只有 \(10^5\)個。我們僅考慮這些格子的 \(dp\)值怎麼計算。

考慮 \(dp[i][j]\)表示當前處於有金幣的格子 \((i,j)\)時的最大金幣數,考慮能夠轉移到此的狀態,即 $dp[i][j] = \max_{x \leq i, y \leq j, coin[i][j]}(dp[x][y]) + 1。

這個轉移條件其實就是個二維偏序,因此對金幣的位置\((x,y)\)從小到大排序,然後依次列舉這些金幣,當考慮到第 \(i\)個金幣時, \(j \leq i\)的金幣 一定滿足\(x_j \leq x_i\),因此我們只需找到 \(y_j \leq y_i\)的最大的 \(dp[x_j][y_j]\)值即可,這是一個區間最值查詢,用線段樹維護即可。

即對金幣的位置\((x,y)\)從小到大排序,然後依次列舉這些金幣,用線段樹維護列舉過的金幣關於y_j下標的dp最大值。考慮上述的轉移條件,線上段樹查詢時,由於列舉順序的緣故,天然滿足\(x \leq i\)的條件,而線段樹的區間查詢找到滿足 \(y < j\)\(\max(dp[x][y])\),因此上述的二維偏序的最值問題就可以用線段樹解決了。

神奇的程式碼
#include <bits/stdc++.h>
using namespace std;
using LL = long long;

const int N = 2e5 + 8;
const int inf = 1e9 + 7;

class segment {
#define lson (root << 1)
#define rson (root << 1 | 1)
  public:
    int val[N << 2];
    int id[N << 2];

    void pushup(int root) {
        if (val[lson] > val[rson]) {
            val[root] = val[lson];
            id[root] = id[lson];
        } else {
            val[root] = val[rson];
            id[root] = id[rson];
        }
    }

    void build(int root, int l, int r) {
        if (l == r) {
            val[root] = -inf;
            return;
        }
        int mid = (l + r) >> 1;
        build(lson, l, mid);
        build(rson, mid + 1, r);
        pushup(root);
    }

    void update(int root, int l, int r, int pos, int v, int i) {
        if (l == r) {
            if (val[root] < v) {
                val[root] = v;
                id[root] = i;
            }
            return;
        }
        int mid = (l + r) >> 1;
        if (pos <= mid)
            update(lson, l, mid, pos, v, i);
        else
            update(rson, mid + 1, r, pos, v, i);
        pushup(root);
    }

    pair<int, int> query(int root, int l, int r, int L, int R) {
        if (L <= l && r <= R) {
            return {val[root], id[root]};
        }
        int mid = (l + r) >> 1;
        pair<int, int> resl{}, resr{};
        if (L <= mid)
            resl = query(lson, l, mid, L, R);
        if (R > mid)
            resr = query(rson, mid + 1, r, L, R);
        return max(resl, resr);
    }

} seg;

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int h, w, n;
    cin >> h >> w >> n;
    vector<array<int, 2>> pos(n + 2);
    for (int i = 1; i <= n; ++i)
        cin >> pos[i][0] >> pos[i][1];
    pos[0] = {1, 1};
    pos[n + 1] = {h, w};
    sort(pos.begin(), pos.end());
    seg.build(1, 1, w);
    seg.update(1, 1, w, 1, 0, 0);
    vector<int> tr(n);
    for (int i = 1; i <= n + 1; ++i) {
        auto& [x, y] = pos[i];
        auto res = seg.query(1, 1, w, 1, y);
        int dp = res.first + 1;
        tr[i] = res.second;
        seg.update(1, 1, w, y, dp, i);
    }
    auto [ans, p] = seg.query(1, 1, w, w, w);
    cout << ans - 1 << '\n';
    string op;
    while (p != 0) {
        auto [x1, y1] = pos[p];
        p = tr[p];
        auto [x2, y2] = pos[p];
        auto dx = abs(x1 - x2);
        auto dy = abs(y1 - y2);
        if (dx) {
            op += string(dx, "UD"[x1 > x2]);
        }
        if (dy) {
            op += string(dy, "LR"[y1 > y2]);
        }
    }
    reverse(op.begin(), op.end());
    cout << op << '\n';

    return 0;
}



G - As far as possible (abc369 G)

題目大意

給定一棵樹,邊有邊權。

對於\(k=1,2,...,n\),要求選 \(k\)個點,使得從 \(1\)號點出發,遍歷每個點,最終回到 \(1\)號點的距離的最小值最大。

解題思路

如果我給定了\(k\)個點,怎麼求這個的最小值呢。

容易發現答案其實就是這 \(k\)個點到根的路徑的的長度的兩倍。

\(k=1\)時,很顯然我們選擇距離根最遠的點。

然後當 \(k=2\)時,由於先前的選擇,會導致一些點對答案的貢獻發生了變化——其到根的路徑有一部分與之前選擇的點到根的路徑有交集,那交集的部分不會有額外的貢獻。因此當我們選擇一個點後,除了一路沿父親節點更新貢獻外,還要更新父親兄弟節點及其子樹的貢獻改變。這個貢獻改變自然是一棵子樹,透過樹的\(dfs\) 序來維護這個貢獻改變,其實就是一個區間操作,可以用線段樹維護,其複雜度只有\(O(\log)\),而貢獻改變會發生多少次呢?一個點最多隻會帶來一次貢獻改變,因此最多區間操作 \(O(n)\)次,因此總的複雜度只有 \(O(n \log n)\)次。

\(val[i]\)表示\(dfs\)序裡的第 \(i\)個節點,如果我選擇它,它對答案貢獻(增加)了多少。每次我們肯定選擇最大的 \(val\),選擇這個 \(val\)後,會使得一些子樹內的節點對答案的貢獻減少(減去交集路徑長度),每個子樹內的節點在 \(dfs\)序裡面對應了一個區間,因此我們用線段樹維護這個 \(val\)陣列,每次查詢就是個區間最值,每次更新貢獻就是個區間操作。

但如果從另一個角度來看,考慮對這棵樹進行長鏈剖分,容易發現答案就是最長的 \(k\)個長鏈的長度的兩倍。

神奇的程式碼
#include <bits/stdc++.h>
using namespace std;
using LL = long long;

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n;
    cin >> n;
    vector<vector<array<int, 2>>> edge(n);
    for (int i = 0; i < n - 1; ++i) {
        int u, v, w;
        cin >> u >> v >> w;
        --u, --v;
        edge[u].push_back({v, w});
        edge[v].push_back({u, w});
    }
    vector<int> mxson(n, -1);
    vector<LL> deep(n, 0), maxdeep(n, 0);
    function<void(int, int)> dfs1 = [&](int u, int fa) {
        maxdeep[u] = deep[u];
        for (auto [v, w] : edge[u]) {
            if (v == fa)
                continue;
            deep[v] = deep[u] + w;
            dfs1(v, u);
            maxdeep[u] = max(maxdeep[u], maxdeep[v]);
            if (mxson[u] == -1 || maxdeep[mxson[u]] < maxdeep[v]) {
                mxson[u] = v;
            }
        }
    };
    dfs1(0, -1);
    vector<LL> lian;
    function<void(int, int, LL)> dfs2 = [&](int u, int fa, LL dis) {
        for (auto [v, w] : edge[u]) {
            if (v == fa)
                continue;
            if (v == mxson[u])
                dfs2(v, u, dis + w);
            else
                dfs2(v, u, w);
        }
        if (mxson[u] == -1) {
            lian.push_back(dis);
        }
    };
    dfs2(0, -1, 0);
    sort(lian.begin(), lian.end(), greater<LL>());
    int up = 0;
    LL ans = 0;
    for (int i = 0; i < lian.size(); ++i) {
        ans += lian[i];
        cout << ans * 2 << '\n';
    }
    for (int i = lian.size(); i < n; ++i) {
        cout << ans * 2 << '\n';
    }

    return 0;
}



相關文章