AtCoder Beginner Contest 361

~Lanly~發表於2024-07-06

A - Insert (abc361 A)

題目大意

給定一個陣列\(a\)和數 \(k,x\),將 \(x\)插入第 \(k\)個數之後,並輸出新陣列。

解題思路

\(vector\)的直接 \(insert\)即可。

神奇的程式碼
#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, k, x;
    cin >> n >> k >> x;
    vector<int> a(n);
    for (auto& i : a)
        cin >> i;
    a.insert(a.begin() + k, x);
    for (auto i : a)
        cout << i << ' ';
    cout << '\n';

    return 0;
}



B - Intesection of Cuboids (abc361 B)

題目大意

給定兩個立方體,問是否相交。

解題思路

因為立方體都是平行座標軸擺放的,若相交,則說明在各個維度上的線段都相交。(可以考慮二維的長方形)

因此判斷三個維度的線段是否都相交即可。

神奇的程式碼
#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, c, d, e, f;
    int g, h, i, j, k, l;

    cin >> a >> b >> c >> d >> e >> f;
    cin >> g >> h >> i >> j >> k >> l;

    auto overlap = [](int l1, int r1, int l2, int r2) {
        return max(l1, l2) < min(r1, r2);
    };

    bool x = overlap(a, d, g, j);
    bool y = overlap(b, e, h, k);
    bool z = overlap(c, f, i, l);

    if (x && y && z) {
        cout << "Yes" << endl;
    } else {
        cout << "No" << endl;
    }

    return 0;
}



C - Make Them Narrow (abc361 C)

題目大意

給定\(n\)個數\(a_i\),刪去其中的 \(k\)個數,使其極差(最大值與最小值的差)最小。

解題思路

\(a_i\)從小到大排序,我刪除其中的 \(k\)個數,一定是從最小值和最大值開始刪除,不會從中間刪(這對極差不會有任何影響)。

決策就是我從最小值開始刪多少個數,如果我選擇刪 \(x\)個數,那麼從最大值開始要刪 \(k-x\)個數。所有的 \(x\)的情況的極差取個最小值即為答案。

\(x\)的範圍就是 \(O(n)\),直接列舉 \(x\)即可。

神奇的程式碼
#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, k;
    cin >> n >> k;
    vector<int> a(n);
    for (auto& x : a)
        cin >> x;
    sort(a.begin(), a.end());
    int ans = 1e9 + 7;
    for (int i = 0; i <= k; i++) {
        ans = min(ans, a[n - (k - i) - 1] - a[i]);
    }
    cout << ans << '\n';

    return 0;
}



D - Go Stone Puzzle (abc361 D)

題目大意

\(n+2\)個格子,其中前 \(n\)個格子有石頭,石頭有黑有白。每次操作。

將相鄰兩個石頭移動到無石頭的位置,倆石頭相對順序不變。

給定初始局面和最終局面,問操作次數的最小值。

解題思路

注意到\(n \leq 14\),局面數最多隻有 \(C_{14}^{7} < 1e5\)。因此直接從初始局面進行 \(BFS\),列舉操作,轉移後續狀態即可。

列舉操作即,先花 \(O(n)\)找到空位,然後花 \(O(n)\)列舉要移動的兩個石頭,移動後得到後繼狀態。轉移複雜度即為\(O(n)\)

由於數很小,狀態記錄可以直接用vector,用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;
    string s, t;
    cin >> n >> s >> t;
    vector<int> st(n + 2, 2), ed(n + 2, 2);
    for (int i = 0; i < n; i++) {
        st[i] = s[i] == 'B';
        ed[i] = t[i] == 'B';
    }
    map<vector<int>, int> cnt;
    queue<vector<int>> q;
    q.push(st);
    cnt[st] = 0;
    while (!q.empty()) {
        auto u = q.front();
        q.pop();
        int d = cnt[u];
        if (u == ed) {
            break;
        }
        int empty = find(u.begin(), u.end(), 2) - u.begin();
        for (int i = 0; i < n + 1; ++i) {
            auto v = u;
            if (v[i] != 2 && v[i + 1] != 2) {
                swap(v[i], v[empty]);
                swap(v[i + 1], v[empty + 1]);
                if (!cnt.count(v)) {
                    cnt[v] = d + 1;
                    q.push(v);
                }
            }
        }
    }
    if (!cnt.count(ed)) {
        cnt[ed] = -1;
    }
    cout << cnt[ed] << '\n';

    return 0;
}



E - Tree and Hamilton Path 2 (abc361 E)

題目大意

給定一棵樹,邊有邊權。問從一個點出發,訪問完所有節點的最小路徑長度。

解題思路

首先注意到,從一個點出發\(st\),最終停下來的點一定是葉子\(ed\)

其次,考慮每條邊訪問的次數,會發現只有\(st \to ed\)這條鏈上的邊只訪問了一次,其他邊均訪問兩次。

因此最終的路徑長度即為 \(2 \sum e_i - cost(st \to ed)\),即所有邊權和的兩倍減去根到葉子的距離

最小化路徑長度,即最大化根到葉子的距離

由於根不是指定的,因此要找的是樹上兩點的最長距離

這即為樹的直徑,兩次 \(DFS\)即可找出。

神奇的程式碼
#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);
    LL sum = 0;
    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});
        sum += w;
    }
    vector<LL> dis(n, 0);
    auto dfs = [&](auto&& dfs, int u, int fa) -> void {
        for (auto [v, w] : edge[u]) {
            if (v == fa)
                continue;
            dis[v] = dis[u] + w;
            dfs(dfs, v, u);
        }
    };
    dfs(dfs, 0, 0);
    int l = max_element(dis.begin(), dis.end()) - dis.begin();
    dis.assign(n, 0);
    dfs(dfs, l, l);
    LL max_dis = *max_element(dis.begin(), dis.end());
    cout << sum * 2 - max_dis << endl;

    return 0;
}



F - x = a^b (abc361 F)

題目大意

給定\(n\),求 \(x \in [1,n]\) ,滿足存在\(a,b(b \geq 2)\),使得 \(x = a^b\)

\(n \leq 10^{18}\)

解題思路

由於\(b \geq 2\),因此 \(a\)的範圍就是 \([1, \sqrt{n}]\),即\(a \in [1, 10^9]\),直接列舉\(a,b\)的話是 \(O(10^9 \log 10^9)\)

注意到當\(b \geq 3\)時,\(a \in [1, 10^6]\) ,因此先列舉\(a \in [1, 10^6]\) ,把\(a \leq 10^6, b \geq 2\)\(x\)全部找出來,這裡的時間複雜度為 \(O(10^6 \log 10^6)\),全部存在vector然後排序去重即可得到這部分的\(x\)的數量。

然後考慮剩下的 \(10^6 < a \leq 10^9, b = 2\)\(x\)的數量。

一個比較淺顯的想法,認為這部分的數量為\(\lfloor \sqrt{n} \rfloor - 10^6\)。但容易發現會算重:如果\(a = c^2\),其中\(c \leq 10^6\),那麼 \(a^2 = c^4\) ,這個數其實是上面算過的(\(a \leq 10^6, b \geq 2\)部分)。

因此要把重複的部分去掉,考慮怎樣的\(a\)是重複的,即存在\(b \geq 2, a = c^b(c \leq 10^6)\),容易發現這個條件就是上面列舉計算的條件,即重複的都在vector裡出現過。

因此\(\lfloor \sqrt{n} \rfloor - 10^6\)再減去vector裡出現過的\((10^6, \lfloor \sqrt{n} \rfloor]\)的數即可,二分找到對應的下標相減即為重複的數量。

神奇的程式碼
#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);
    LL n;
    cin >> n;
    int up = 1e6;
    LL upp = 1e18;
    vector<LL> s{1};
    for (int i = 2; i <= up; i++) {
        __int128 x = 1ll * i * i;
        while (x <= n) {
            s.push_back(x);
            x *= i;
        }
    }
    sort(s.begin(), s.end());
    s.erase(unique(s.begin(), s.end()), s.end());
    LL ans = s.size();
    int half = sqrt(n);
    if (half > up) {
        ans += half - up;
        auto r = upper_bound(s.begin(), s.end(), half) - s.begin();
        auto l = upper_bound(s.begin(), s.end(), up) - s.begin();
        ans -= r - l;
    }
    cout << ans << '\n';

    return 0;
}



G - Go Territory (abc361 G)

題目大意

二維平面,有障礙物,可以上下左右走。

問有多少個點,不可以走到\((-1, -1)\)

解題思路

看的時候發覺很久以前做過的類似的題,基本做法一致。

首先看樣例給的圖

example

一個樸素的想法就是找到不在原點連通塊的點,這些點的個數和就是答案。

如果二維平面很小的話,可以對每個點進行\(BFS\),找到所有的連通塊,然後累計非原點連通塊的點,其值即為答案。

但是這裡的平面大小有 \(10^5 \times 10^5\),不能\(BFS\)

由於每個連通塊都是一個封閉的圖形,我們要統計的就是這個圖形的點數,類似面積,可以使用掃描線的方法。從下往上掃描每一行的線段,這些線段是由障礙物作分割,然後用並查集維護這些線段所屬的連通塊。

考慮第\(i\)行,第 \(i\)行的障礙物把該行分割成了 \(x\)條線段,並且已經用並查集維護好了這些線段所屬的連通塊。然後考慮第 \(i+1\)行,第 \(i+1\)行的障礙物同樣把該行分割成了 \(y\)條線段,現在我們就需要將這兩行線段合併,得到第 \(i+1\)行的每個線段所屬的連通塊是哪個。

合併即考慮上下兩行的兩個線段,如果它們是相交的,那麼它們應屬於同一個連通塊,並查集合並起來。這是一個模擬的過程,有點小細節。

得到第 \(i+1\)行的線段的連通塊關係,繼續合併第 \(i+2\)行,依次往復,掃描整個平面。最後遍歷所有連通塊,把不在原點連通塊的點累加即為答案。

而對於該行沒有障礙物的,則視為一條線段,如果有連續若干行無障礙物,這我們可以把這若干行壓成一行,視為一條線段。最後掃描時按照障礙物的第一維排序掃描。

特殊處理無障礙物的情況。

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

struct segg {
    int l, r, id;
};

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);

    int n, h = 2e5 + 8, w = 2e5 + 8; // 大平面[1, h] * [1, w]
    cin >> n;
    vector<pair<int, int>> pos(n);

    for (auto& i : pos) {
        cin >> i.first >> i.second;
        i.first += 2;
        i.second += 2; // 原點視為(1, 1)
    }
    sort(pos.begin(), pos.end());

    int cur = 0;
    vector<int> f;
    vector<LL> cnt;
    array<vector<segg>, 2> seg;

    auto findfa = [&](auto& findfa, int x) -> int {
        return f[x] == x ? x : f[x] = findfa(findfa, f[x]);
    };
    auto crossover = [&](segg& a, segg& b) { return a.r >= b.l && a.l <= b.r; };
    auto add_seg = [&](int l, int r, LL cc) {
        int id = f.size();
        f.push_back(id);
        cnt.push_back(cc);
        seg[cur].push_back({l, r, id});
    };

    auto unionn = [&]() { // 將上下兩行線段合併
        auto& last_seg = seg[cur ^ 1];
        auto& cur_seg = seg[cur];
        auto last_pt = last_seg.begin();
        for (auto& i : cur_seg) {
            while (true) {
                if (last_pt != last_seg.end() &&
                    crossover(i, *last_pt)) { // 線段相交
                    int fa = findfa(findfa, i.id);
                    int fb = findfa(findfa, last_pt->id);
                    if (fa != fb) {
                        f[fa] = fb;
                        cnt[fb] += cnt[fa];
                    }
                    last_pt = next(last_pt);
                } else if (last_pt == last_seg.end() ||
                           last_pt->l > i.r) { // 形如 cur_pt ..... last_pt
                    if (last_pt != last_seg.begin())
                        last_pt = prev(last_pt);
                    break;
                } else // 形如 last_pt ..... cur_pt
                    last_pt = next(last_pt);
            }
        }
    };

    auto skip_line = [&](int l, int r) { // 空行[l, r],無障礙物
        if (l > r)
            return;
        cur ^= 1;
        seg[cur].clear();
        add_seg(1, w, (r - l + 1ll) * w);
        unionn();
    };

    auto solve_pos = [&](int l, int r) { // 處理該行的所有障礙物pos[l..r]
        if (l > r)
            return;
        cur ^= 1;
        seg[cur].clear();
        int la = 0;
        for (int i = l; i <= r; ++i) {
            if (pos[i].second - la > 1) {
                add_seg(la + 1, pos[i].second - 1, pos[i].second - la - 1);
            }
            la = pos[i].second;
        }
        if (w > la) {
            add_seg(la + 1, w, w - la);
        }
        unionn();
    };

    LL ans = 0;
    if (n == 0) {
        ans = 0;
    } else {
        skip_line(1, pos[0].first - 1);
        int la = 0;
        for (int i = 1; i < n; ++i) {
            if (pos[i].first != pos[la].first) {
                solve_pos(la, i - 1); // 處理同行的所有障礙物
                skip_line(pos[la].first + 1, pos[i].first - 1); // 處理空行
                la = i;
            }
        }
        solve_pos(la, n - 1);
        skip_line(pos[la].first + 1, h); // 最頂部還有空行
        int origin = findfa(findfa, seg[cur].front().id); // 起點的連通塊編號
        for (int i = 0; i < f.size(); ++i) {
            if (findfa(findfa, i) == i && i != origin) {
                ans += cnt[i];
            }
        }
    }
    cout << ans << '\n';

    return 0;
}