AtCoder Beginner Contest 349

~Lanly~發表於2024-04-13

A - Zero Sum Game (abc349 A)

題目大意

\(n\)個人遊戲,每局有一人 \(+1\)分,有一人 \(-1\)分。

給定最後前 \(n-1\)個人的分數,問第 \(n\)個人的分數。

解題思路

零和遊戲,所有人總分是 \(0\),因此最後一個人的分數就是前 \(n-1\)個人的分數和的相反數。

神奇的程式碼
n = input()
print(-sum([int(i) for i in input().split()]))


B - Commencement (abc349 B)

題目大意

對於一個字串,如果對於所有 \(i \geq 1\),都有恰好 \(0\)\(2\) 個自負出現\(i\)次,則該串是好串。

給定一個字串\(s\),問它是不是好串。

解題思路

\(|s|\)只有 \(100\),統計每個字元的出現次數,再列舉 \(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);
    string s;
    cin >> s;
    bool ok = true;
    map<char, int> cnt;
    for (auto c : s)
        cnt[c]++;
    auto check = [&](int c) {
        int cc = 0;
        for (auto& [k, v] : cnt) {
            cc += (v == c);
        }
        return cc == 0 || cc == 2;
    };
    for (int i = 1; i <= s.size(); ++i) {
        ok &= check(i);
    }
    cout << (ok ? "Yes" : "No") << endl;

    return 0;
}



C - Airport Code (abc349 C)

題目大意

給定一個字串\(s\)和字串 \(t\),問字串 \(t\)能否從字串 \(s\)得到。操作為:

  • \(s\)挑三個字母,不改變順序變成 \(t\)
  • \(s\)挑兩個字母,加上\(X\),不改變順序變成 \(t\)

解題思路

就子序列匹配問題。就近匹配原則即可。

神奇的程式碼
#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);
    string s, t;
    cin >> s >> t;
    if (t.back() == 'X')
        t.pop_back();
    auto pos = s.find_first_of(tolower(t[0]));
    for (int i = 1; i < t.size() && pos < s.size(); ++i) {
        pos = s.find_first_of(tolower(t[i]), pos + 1);
    }
    if (pos < s.size())
        cout << "Yes" << endl;
    else
        cout << "No" << endl;

    return 0;
}



D - Divide Interval (abc349 D)

題目大意

給定\([l,l+1,...,r-1,r)\)序列,拆分成最少的序列個數,使得每個序列形如 \([2^ij, 2^i(j+1))\)

給出拆分方案。

解題思路

拆分的序列個數最小,那肯定想讓一個序列儘可能的長,而長的話,就是讓\(2^i\)儘可能大。

因此就貪心地讓\(2^i\)儘可能大,即 \(l=2^i * j\),這裡的\(i\)是最大的 \(i\)(這意味著 \(j\)是奇數),並且 \(r \leq 2^i(j + 1)\),如果 \(r > 2^i(j + 1)\),那說明 \(2^i\)太大了,就變成 \(2^(i-1) 2j\)來試試。

神奇的程式碼
#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 l, r;
    cin >> l >> r;
    vector<array<LL, 2>> ans;
    while (l < r) {
        LL p2 = 1;
        LL bl = l;
        while (bl % 2 == 0 && l + p2 <= r) {
            bl >>= 1;
            p2 <<= 1;
        }
        while (l + p2 > r) {
            p2 >>= 1;
            bl <<= 1;
        }
        ans.push_back({l, l + p2});
        l += p2;
    }
    cout << ans.size() << '\n';
    for (auto& i : ans) {
        cout << i[0] << ' ' << i[1] << '\n';
    }

    return 0;
}



E - Weighted Tic-Tac-Toe (abc349 E)

題目大意

給定\(3 \times 3\)的網格,高橋和青木畫 \(OX\)

每個格子有分數。

若存在同行同列或同對角線,則對應方贏,否則全部畫滿後,所畫格子的分數和較大者贏。

問最優策略下,誰贏。

解題思路

樸素的博弈\(dp\),狀態即為當前的棋盤樣子,總狀態數為 \(3^9=2e4\),直接搜尋即可。

即設 \(dp[i]\)表示當前局面 \(i\)的當前操作者(稱為先手)的必贏\((dp[i] = 1)\)或必輸 \((dp[i] = 0)\)

轉移,則列舉當前操作者的行為,即選擇哪個格子,進入後繼狀態。

如果所有後繼狀態都是(先手)必贏,那麼當前狀態則是(先手)必輸,即\(dp[i] = 0\)如果所有\(dp[j] = 1\)\(j\)是後繼狀態。

否則,如果有一個後繼狀態是先手必輸,那麼當前狀態的先手就可以控制局面走向該狀態,使得當前狀態是必勝態,即\(dp[i] = 1\)如果存在一個\(dp[j] = 0\)

轉移顯而易見是\(O(9)\),總狀態數只有 \(O(2e4)\),因此樸素搜尋就可以透過了。

神奇的程式碼
#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);
    typedef array<array<LL, 3>, 3> tu;
    tu a;
    for (auto& x : a)
        for (auto& y : x)
            cin >> y;
    map<tu, int> dp;
    auto check_end = [&](tu& pos) -> int {
        LL p1 = 0, p2 = 0;
        int left = 0;
        for (int i = 0; i < 3; ++i) {
            for (int j = 0; j < 3; ++j) {
                left += !pos[i][j];
                p1 += (pos[i][j] == 1) * a[i][j];
                p2 += (pos[i][j] == 2) * a[i][j];
            }
        }
        if (left == 0) {
            if (p1 > p2)
                return 0;
            else
                return 1;
        }

        for (int i = 0; i < 3; ++i) {
            if (pos[i][0] == pos[i][1] && pos[i][1] == pos[i][2] &&
                pos[i][0] != 0) {
                if (pos[i][0] == 1)
                    return 0;
                else
                    return 1;
            }
            if (pos[0][i] == pos[1][i] && pos[1][i] == pos[2][i] &&
                pos[0][i] != 0) {
                if (pos[0][i] == 1)
                    return 0;
                else
                    return 1;
            }
        }

        if (pos[0][0] == pos[1][1] && pos[1][1] == pos[2][2] &&
            pos[0][0] != 0) {
            if (pos[0][0] == 1)
                return 0;
            else
                return 1;
        }

        if (pos[0][2] == pos[1][1] && pos[1][1] == pos[2][0] &&
            pos[0][2] != 0) {
            if (pos[0][2] == 1)
                return 0;
            else
                return 1;
        }

        return -1;
    };

    auto dfs = [&](auto self, tu& pos, int role) -> bool {
        int status = check_end(pos);
        if (status != -1) {
            return dp[pos] = status == role;
        }
        if (dp.find(pos) != dp.end())
            return dp[pos];
        bool lose = false;
        for (auto& i : pos)
            for (auto& j : i) {
                if (j)
                    continue;
                j = (role + 1);
                lose |= (!self(self, pos, role ^ 1));
                j = 0;
            }
        return dp[pos] = lose ? 1 : 0;
    };

    tu ini{};
    bool win = dfs(dfs, ini, 0);
    cout << (win ? "Takahashi" : "Aoki") << endl;

    return 0;
}



F - Subsequence LCM (abc349 F)

題目大意

給定一個序列\(a\)\(m\),問子序列的數量,使得其\(lcm\)(最小公倍數)為 \(m\)

子序列之間的不同,當且僅當有元素在原位置不一樣,即使它們的數字可能是一樣的。

解題思路

我們可以依次考慮每個\(a\),選或不選,很顯然是\(O(2^n)\)

我們不能維護每個數選擇的狀態,但是要維護怎樣的狀態呢?是怎樣的中間狀態能夠匯出它們的最小公倍數呢。

給定\(n\)個數,考慮怎麼求它們的最小公倍數。

根據其定義,假設最小公倍數是\(m\),那就意味著 \(m\)是每個數的倍數。

從質因數的角度來思考倍數,那就是 \(m\)的每個質因子的冪 \(\geq\)每個數對應質因子的冪。

因此 \(m\)的每個質因子的冪就是這些數對應質因子冪的最大值。(最大公因數對應的其實就是冪的最小值)

從上述求最小公倍數的做法,可以引出維護的中間狀態。

即設\(dp[i][j]\)表示考慮前 \(i\)個數,選擇若干個後,每個質數冪的值的狀態為 \(j\)的方案數。這樣透過狀態 \(j\)就可以知道最後的最小公倍數是不是 \(m\)。但很顯然這一狀態數是比較大的,稍加思考會發現我們不需要保留當前的冪值是多少,因為求最小公倍數時,我們是 取最大值,我們只想讓它最後為\(m\)對應的值即可,因此這裡的數量狀態可以變成二元狀態。

\(dp[i][j]\)表示考慮前 \(i\)個數,選擇若干個後,其質數冪的最大值是否達到 \(m\)對應的質數冪的值的狀態為 \(j\)(對於每個 \(m\)的質數,都有 未達到達到這一\(01\) 狀態,因此\(j\)是一個二進位制壓縮的狀態)的選擇方案數。初始條件是 \(dp[0][0]=0\)

轉移就考慮,選擇當前數後,狀態 \(j\)是否會變化。因此要事先預處理每個數選擇後對這一狀態的影響。即事先對\(m\)質因數分解,再預處理每個 \(a_i\)對轉移的影響。

考慮時間複雜度, \(m\)\(10^{16}\),至多可能有15+個質數,總的複雜度是 \(O(2^15 10^5)\),粗略算也有 \(1e9\)了。當前的 \(dp\)還過不了。考慮進一步最佳化。

現在的問題已經變成,給定若干個 \(010101\)數,要求選若干個數,使得其與結果\(11111\)的方案數。上述的 \(dp\)方式複雜度是 \(1e9\),會超時。

超時的1e9
#include <bits/stdc++.h>
using namespace std;
using LL = long long;

const int mo = 998244353;

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n;
    LL m;
    cin >> n >> m;
    bool one = (m == 1);
    vector<pair<LL, int>> fac;
    int up = 1e8;
    for (int i = 2; i <= up; ++i) {
        if (m % i == 0) {
            fac.push_back({i, 0});
            while (m % i == 0) {
                m /= i;
                fac.back().second++;
            }
        }
    }
    if (m != 1) {
        fac.push_back({m, 1});
    }

    vector<int> a;
    for (int i = 0; i < n; ++i) {
        LL x;
        cin >> x;
        bool ok = true;
        int sign = 0;
        for (int j = 0; j < fac.size(); ++j) {
            auto& [p, v] = fac[j];
            int cnt = 0;
            while (x % p == 0) {
                x /= p;
                ++cnt;
            }
            ok &= (cnt <= v);
            if (cnt == v)
                sign |= (1 << j);
        }
        ok &= (x == 1);
        if (ok)
            a.push_back(sign);
    }

    vector<int> dp(1 << fac.size(), 0);
    dp[0] = 1;
    for (int x : a) {
        vector<int> dp2 = dp;
        for (int i = 0; i < (1 << fac.size()); ++i) {
            dp2[i | x] = (dp2[i | x] + dp[i]);
            if (dp2[i | x] >= mo)
                dp2[i | x] -= mo;
        }
        dp.swap(dp2);
    }
    int ans = dp.back();
    if (one) {
        ans = (ans - 1 + mo) % mo;
    }
    cout << ans << '\n';

    return 0;
}



欲知後事如何,且等作業寫完後再寫

G - Palindrome Construction (abc349 G)

題目大意

<++>

解題思路

<++>

神奇的程式碼