AtCoder Beginner Contest 374

~Lanly~發表於2024-10-05
省流版
  • A. 判斷末三位即可
  • B. 逐位判斷即可
  • C. 列舉所有分組情況即可
  • D. 列舉線段順序、端點順序即可
  • E. 二分答案,發現貴的機器數量不超過\(100\),列舉求最小花費看是否可行即可
  • F. 樸素DP,複雜度分析得到有效時刻不超過\(O(n^2)\)而非\(O(s_i)\),直接\(DP\)即可
  • G. 最小路徑覆蓋問題,建有向圖、縮點、求傳遞閉包、二分圖最大匹配即可

稍微完善了E題的證明
更新了G

A - Takahashi san 2 (abc374 A)

題目大意

給定一個字串,問結尾是不是san

解題思路

直接判斷最後三個字母即可,python可以一行。

神奇的程式碼
print("Yes" if input().strip().endswith('san') else "No")


B - Unvarnished Report (abc374 B)

題目大意

給定兩個字串,問第一個字母不相同的次數。

解題思路

逐位判斷即可。

python的想法,即先找出不同的位置,然後取最小值。

神奇的程式碼
a = input().strip()
b = input().strip()
if len(a) > len(b):
    a, b = b, a
if len(a) < len(b):
    a += ' ' * (len(b) - len(a))
pos = [i for i in range(len(a)) if a[i] != b[i]]
if not pos:
    print(0)
else:
    print(pos[0] + 1)


C - Separated Lunch (abc374 C)

題目大意

給定\(n\)個數字,分成兩組,使得和最大值最小。

解題思路

\(n \leq 20\),直接花 \(O(2^n)\)列舉分組情況,每種情況花 \(O(n)\)統計和,所有情況取最小值即可。總的時間複雜度為\(O(2^nn)\)

神奇的程式碼
#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& x : a)
        cin >> x;
    int tot = accumulate(a.begin(), a.end(), 0);
    int up = (1 << n);
    int ans = 2e9 + 7;
    for (int i = 0; i < up; ++i) {
        int cnt = 0;
        for (int j = 0; j < n; ++j) {
            cnt += ((i >> j) & 1) * a[j];
        }
        ans = min(ans, max(cnt, tot - cnt));
    }
    cout << ans << '\n';

    return 0;
}



D - Laser Marking (abc374 D)

題目大意

二維平面,給定\(n\)個線段,用鐳射印表機列印。

鐳射移動速率為\(s\),列印時的速率為 \(t\)

規定列印順序,使得耗時最短。初始鐳射位於\((0,0)\)

解題思路

列印順序,即規定列印線段的順序,以及每個線段從哪個端點開始列印。

由於\(n \leq 6\),因此花 \(O(n!)\)列舉順序,花 \(O(2^n)\)列舉線段的列印端點,然後花 \(O(n)\)計算時間即可。

總的時間複雜度為\(O(n!2^nn)\)

神奇的程式碼
#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, s, t;
    cin >> n >> s >> t;
    vector<array<int, 4>> a(n);
    for (auto& x : a) {
        cin >> x[0] >> x[1] >> x[2] >> x[3];
    }
    vector<int> id(n);
    iota(id.begin(), id.end(), 0);
    double ans = 1e9 + 7;
    int up = (1 << n);
    auto dist = [](int x, int y, int sx, int sy) -> double {
        return sqrt((x - sx) * (x - sx) + (y - sy) * (y - sy));
    };
    auto solve = [&](vector<int>& id, int dir) -> double {
        int x = 0, y = 0;
        double res = 0;
        for (auto& i : id) {
            auto [sx, sy, ex, ey] = a[i];
            if ((dir >> i) & 1) {
                swap(sx, ex);
                swap(sy, ey);
            }
            res += dist(x, y, sx, sy) / s;
            res += dist(sx, sy, ex, ey) / t;
            x = ex;
            y = ey;
        }
        return res;
    };
    do {
        for (int i = 0; i < up; i++) {
            ans = min(ans, solve(id, i));
        }
    } while (next_permutation(id.begin(), id.end()));
    cout << fixed << setprecision(10) << ans << '\n';

    return 0;
}



E - Sensor Optimization Dilemma 2 (abc374 E)

題目大意

製作產品,有\(n\)道工序。

每道工序有兩種裝置,單價 \(p_i\)\(q_i\),一天可做 \(a_i\)\(b_i\)的產品。

最終的生產效率是所有工序的產品數量的最小值。

現有 \(x\)元,問生產效率的最大值。

解題思路

首先列舉這個最大值,然後看可不可行。容易發現生產效率越小越容易滿足,越大越難滿足,因此這個答案可以二分(典型的最小值最大)。

二分了生產效率\(m\)後,剩下的問題就是讓每一個工序的產能都\(\geq m\),且讓花費最小,然後看所有的工序的花費是否\(\geq x\)

這裡只有兩種機器,一個樸素的想法就是計算單位產能的價格,即 \(\frac{p_i}{a_i}\)\(\frac{q_i}{b_i}\)看看那個小,那我們肯定買小的那個。

但是會有個問題,如果我們只買小的,然後產能剛好 \(=m\),此時肯定是最優策略。但如果產能 \(>m\),此時滿足了\(\geq m\),但花費不見得是最小的:比如可以少買幾個,多買另外的,使得產能剛好 \(=m\),且花費比前面的還小(樣例一就是個反例)。

假設 \(a_i\)的單價更低,上述考慮的是全買\(a_i\)的,但不一定是花費最小的,退而求其次,買一些 \(b_i\)來替換 \(a_i\),那我應該買多少個\(b_i\)呢?

由於\(a_i, b_i \leq 100\),比賽時就直接猜的 \(b_i\)的範圍就是 \(1 \sim 100\),再大的話完全可以由等價的\(a_i\)替換之類的。然後就過了。

現在細細想來,這裡的問題即為\(a_i x + b_iy \geq m\),找到一個最好的\((x,y)\)滿足該不等式,且 \(p_i x + q_i y\)最小。\(x,y\)的範圍都高達 \((10^7)\),直接遍歷不現實,但我們知道 \(x\)儘可能大是最好的,因此這裡的 \(y\)的範圍不會很大,但有多小呢?

假設\(y=0\)的情況,此時全買 \(a_i\),最壞情況就是\(a_ix = m + a_i - 1\),產量超了太多,現在想透過買一些\(b_i\)使得產量超標少一點,比如多一臺\(b_i\),少幾臺\(a_i\),就能讓超標量少 \(1\) ,這樣\(b_i\)最多多買\(a_i-1\)臺,就能調整產能剛剛好\(=m\)之類的。

或者從另一個角度來看,因為\(a_i\)是儘可能多買,因此注意到最終的產量範圍是\([m, m + a_i - 1]\),這裡只有 \(a_i\)個數,如果多買一臺\(b_i\),少買幾臺\(a_i\),產量要麼不變,要麼變成另外的一個數(比如產量\(-2\)了)。這裡的另外的一個數最壞情況下只有\(a_i\)種情況,因此\(y\)的範圍最多就到 \(a_i\),就能遍歷到可行的\([m, m + a_i - 1]\)中的所有情況了。注意這裡可行的情況不一定是\(a_i\)種,有可能一種(比如多買一臺 \(b_i\),少買幾臺 \(a_i\),最終產量不變),也可能其他種(產量 \(-1\)就有 \(a_i\)種,如果 \(-2\)就可能只有一半的數能取到),具體多少種呢?其實就是\(\frac{a_i}{gcd(a_i, b_i)}\),但它們一定是\(a_i\)的因子,因此 \(y\)直接遍歷 \([0,a_i)\)就一定能遍歷到所有能取到的情況了(雖然可能有一些情況被多次考慮了),當然\(y\)遍歷\([0, \frac{a_i}{gcd(a_i, b_i)})\)也是正確的,這樣所有的情況都只考慮一次了。

神奇的程式碼
#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, x;
    cin >> n >> x;
    vector<array<int, 4>> a(n);
    for (auto& i : a) {
        cin >> i[0] >> i[1] >> i[2] >> i[3];
    }
    auto check = [&](int t) {
        LL sum = 0;
        for (auto& i : a) {
            auto [a, p, b, q] = i;
            if (1ll * p * b > 1ll * q * a)
                swap(a, b), swap(p, q);
            LL tmp = 1e9 + 7;
            for (int j = 0; j < a; ++j) { // j < a / gcd(a,b) 也可以
                LL cost = 1ll * max(0, (t - j * b + a - 1) / a) * p + j * q;
                tmp = min(tmp, cost);
            }
            sum += tmp;
        }
        return sum <= x;
    };
    int l = 0, r = 1e9 + 8;
    while (l + 1 < r) { // [l,r)
        int mid = (l + r) / 2;
        if (check(mid))
            l = mid;
        else
            r = mid;
    }
    cout << l << '\n';

    return 0;
}



F - Shipping (abc374 F)

題目大意

\(n\)個單,第 \(i\)個單從 \(s_i\)時刻可以接。

一次最多接 \(k\)個單,接了後 \(x\)時刻之後才能再接。

一個單的不滿意度為接單時刻與可接時刻的差,即\(t_i - s_i\)

求最小的不滿意度,

解題思路

這題難在複雜度分析。

樸素\(dp\)\(dp[i][j]\)表示前 \(i\)時刻,完成了前 \(j\)個單的最小不滿意度。但這裡時刻數高達\(10^{12}\),不大行。

時刻數不能作為狀態。但考慮上述狀態,有非常多的顯然不優的狀態:我接單的時刻,只有兩類:

  • 我現在剛剛可以接單,就立刻接還在囤積的單。
  • 或者我等等下一個單,然後一起接。

考慮這樣的時刻數有多少:

  • 第一類的時刻數,就是形如\(s_i + x + x + x...\),但注意到每 \(+x\),必定有一個囤積的單,如果沒有囤積的單,那下一個時刻就是第二類的(某個\(s_j\))。因此第一類的時刻數,對於每個 \(i\)來說只有\(O(n)\)個,即最多有\(n\)\(+x\) 。因此總的時刻數就\(O(n^2)\)個。
  • 第二類的時刻數,顯然就是\(O(n)\)個。

所以,上述\(dp[i][j]\)中的 \(i\),拋去顯然不優的狀態,剩下的只有 \(O(n^2)\)個需要考慮的狀態,加之 \(j\)\(O(n)\)狀態,其狀態數就是 \(O(n^3)\),加之轉移複雜度是\(O(k)\),總的時間複雜度就是\(O(n^3k)\)

轉移考慮往後轉移的方式,程式碼裡,則為\(dp[i][j]\)表示完成前 \(i\)個單,此時時刻為 \(j\)的最小不滿意度,因為\(j\)是離散的所以是個 \(map\)。然後列舉接下來完成的單的數量\(l\),計算不滿意度轉移即可。計算不滿意度即\(\sum t - s_i\),可以用字首和最佳化,或者列舉\(l\)時維護 \(\sum s_i\)

標準寫法500ms
#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<LL> t(n);
    for (auto& i : t)
        cin >> i;
    vector<LL> sum(n);
    partial_sum(t.begin(), t.end(), sum.begin());
    vector<map<LL, LL>> dp(n + 1);
    dp[0][0] = 0;
    auto get_sum = [&](int l, int r) {
        if (l > r)
            return 0ll;
        return sum[r] - (l ? sum[l - 1] : 0);
    };
    for (int i = 0; i < n; ++i) {
        for (auto& [now, val] : dp[i]) {
            int st = i;
            for (int j = 1; j <= k && i + j <= n; ++j) {
                int ed = st + j;
                LL cur = max(now, t[ed - 1]);
                LL nxt = val + j * cur - get_sum(st, ed - 1);
                if (dp[i + j].count(cur + x)) {
                    dp[i + j][cur + x] = min(dp[i + j][cur + x], nxt);
                } else {
                    dp[i + j][cur + x] = nxt;
                }
            }
        }
    }
    LL ans = 1e18 + 7;
    for (auto& [_, val] : dp[n]) {
        ans = min(ans, val);
    }
    cout << ans << '\n';

    return 0;
}


下面的是比賽時寫的,加了點小小的轉移最佳化,省去了一些不必要的轉移(即囤積的單肯定全部處理)。

賽場時寫的稍加最佳化的1ms
#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<LL> t(n);
    for (auto& i : t)
        cin >> i;
    vector<LL> sum(n);
    partial_sum(t.begin(), t.end(), sum.begin());
    vector<map<LL, LL>> dp(n + 1);
    dp[0][0] = 0;
    auto get_sum = [&](int l, int r) {
        if (l > r)
            return 0ll;
        return sum[r] - (l ? sum[l - 1] : 0);
    };
    for (int i = 0; i < n; ++i) {
        for (auto& [now, val] : dp[i]) {
            int st = i;
            int ed = upper_bound(t.begin(), t.end(), now) - t.begin(); // 囤積的單
            int cnt = max(1, ed - st);
            for (int j = min(k, cnt); j <= k && i + j <= n; ++j) { // 囤積的單肯定全部處理
                ed = st + j;
                LL cur = max(now, t[ed - 1]);
                LL nxt = val + j * cur - get_sum(st, ed - 1);
                if (dp[i + j].count(cur + x)) {
                    dp[i + j][cur + x] = min(dp[i + j][cur + x], nxt);
                } else {
                    dp[i + j][cur + x] = nxt;
                }
            }
        }
    }
    LL ans = 1e18 + 7;
    for (auto& [_, val] : dp[n]) {
        ans = min(ans, val);
    }
    cout << ans << '\n';

    return 0;
}



G - Only One Product Name (abc374 G)

題目大意

給定\(n\)個長度為 \(2\)的大寫字串,構造一個字串列表,滿足每個大寫字元都作為子串出現在這列表裡,且列表裡每個字串的 \(2\)字母子串都是這 \(n\)個字串裡的,即出現的都在子串裡,子串裡的都出現了。

問這個列表的字串數量的最小值。

解題思路

顯然答案的下界就是\(n\),即每個字串都在這個列表裡。

如何讓答案更小呢?那就是可以將其拼接,比如兩個字串ABBC拼接起來,得到ABC,這樣仍能滿足題意條件,且該字串列表的字串數量減少了一個。

這啟發我們考慮如何拼接,將每個字串看作圖的點,兩個字串\(u,v\)可以拼接,則一條有向邊\(u \to v\)

每一條有向邊就是一次拼接,一條路徑就是拼接多次,也就對應字串列表裡的一個字串。

因此問題轉換成,用最少的路徑,覆蓋所有頂點。注意這裡的路徑的點可以重複,同時也可以相互相交。

著名的最小路徑覆蓋是適用於\(DAG\)的,而這裡有環,但因為一條路徑的點可以重複,因此可以先用Tarjan將環縮成一個點 ,得到一張DAG,剩下的問題就是一個最小可相交路徑覆蓋問題,就是套路了。

關於套路的理解,首先得明白最小不相交路徑覆蓋二分圖最大匹配的關係:因為路基不能相交,可以理解成每個點只能有一個入度和出度,因此每個點拆成兩個點,代表出度入度分居兩邊,一個匹配就是一個點的出度和一個點的入度匹配,即選了一條有向邊,即拼接了一次,答案減一。而最大匹配就對應了最小答案。

理解了匹配的意義,來看可相交的情況下,此時每個點不一定只有一個入度和出度,但有多少個無所謂,我們只關心一條路徑的起點和終點,中間的無所謂覆蓋過與否,因此如果兩點\(u \to v\)可透過若干個中間點可達,無所謂,我們直接連一條 \(u \to v\)的邊即可,一旦匹配到這條邊,意味著有一條路徑 \(u \to v\),中間的點是什麼無所謂,因為可以重複覆蓋。而這是一個傳遞閉包(可達性)。

總體而言,

  • 根據拼接關係建立有向圖
  • Tarjan將環收縮成點後重建新圖(轉成\(DAG\)
  • 新圖跑一遍floyd得到可達性的閉包,根據可達性建立新圖(兩點可達則有邊)
  • 將新圖轉換成二分圖,求最大匹配,答案即為 點數-匹配數
神奇的程式碼
#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<string> s(n);
    for (auto& i : s)
        cin >> i;
    vector<vector<int>> G(n);
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < n; ++j) {
            if (s[i].back() == s[j].front())
                G[i].push_back(j);
        }
    }

    // SCC
    vector<int> low(n), dfn(n), belong(n), in(n);
    vector<vector<int>> bcc;
    int clk = 0;
    auto tarjan = [&](auto&& tarjan, int u) -> void {
        static stack<int> st;
        dfn[u] = low[u] = ++clk;
        st.push(u);
        in[u] = true;
        for (int& v : G[u]) {
            if (!dfn[v]) {
                tarjan(tarjan, v);
                low[u] = min(low[u], low[v]);
            } else if (in[v])
                low[u] = min(low[u], dfn[v]);
        }
        if (dfn[u] == low[u]) {
            vector<int> tmp;
            while (1) {
                int x = st.top();
                st.pop();
                in[x] = false;
                belong[x] = bcc.size();
                tmp.push_back(x);
                if (x == u)
                    break;
            }
            bcc.push_back(tmp);
        }
    };
    for (int i = 0; i < n; ++i) {
        if (!dfn[i])
            tarjan(tarjan, i);
    }

    // reconstract
    int m = bcc.size();
    vector<vector<int>> edge(m, vector<int>(m));
    for (int i = 0; i < n; ++i) {
        for (auto j : G[i]) {
            if (belong[i] != belong[j])
                edge[belong[i]][belong[j]] = 1;
        }
    }

    // floyd
    for (int k = 0; k < m; ++k) {
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < m; ++j) {
                if (i == j)
                    continue;
                if (edge[i][k] && edge[k][j])
                    edge[i][j] = 1;
            }
        }
    }

    // augmenting path
    vector<int> vis(m), pa(m, -1);
    int tt = 0;
    function<bool(int)> dfs = [&](int u) {
        vis[u] = tt;
        for (int v = 0; v < m; ++v) {
            if (edge[u][v] &&
                (pa[v] == -1 || (vis[pa[v]] != tt && dfs(pa[v])))) {
                pa[v] = u;
                return true;
            }
        }
        return false;
    };
    int ans = 0;
    for (int i = 0; i < m; ++i) {
        ++tt;
        if (dfs(i))
            ++ans;
    }

    cout << m - ans << '\n';

    return 0;
}



相關文章