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)
題目大意
<++>
解題思路
<++>
神奇的程式碼