A - Welcome to AtCoder Land (abc358 A)
題目大意
給定兩個字串,問是否是AtCoder Land
解題思路
讀取後判斷即可。
神奇的程式碼
#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 (s == "AtCoder" && t == "Land")
cout << "Yes" << '\n';
else
cout << "No" << '\n';
return 0;
}
B - Ticket Counter (abc358 B)
題目大意
售票廳,\(n\)個人來買票,每個人買票耗時\(a\)。第 \(i\)個人 \(t_i\)時刻來 ,如果此時沒人買票則可以立刻買票,否則要排隊等買票。
問每個人最終買到票的時間。
解題思路
維護隊伍無人的時間\(time\),當第\(i\)個人來時,
- \(time \leq t_i\),則其可以立刻買票,買完票時間為 \(t_i + a\),此時隊伍無人的時間變為 \(time = t_i + a\)。
- \(time > t_i\),則其需要等待至隊伍無人時間\(time\),買完票時間為 \(time + a\),此時隊伍無人的時間變為 \(time = time + a\)。
按照上述方式模擬即可。
神奇的程式碼
#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, a;
cin >> n >> a;
int time = 0;
for (int i = 0; i < n; i++) {
int x;
cin >> x;
if (time <= x) {
time = x + a;
} else
time = time + a;
cout << time << '\n';
}
return 0;
}
C - Popcorn (abc358 C)
題目大意
給定\(n\)個小攤售賣的爆米花種類。
問選擇的最少的小攤數量,可以買到所有爆米花種類。
解題思路
由於\(n \leq 10\),直接 \(O(2^n)\)列舉選擇的小攤,看是否覆蓋所有的爆米花種類。
神奇的程式碼
#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, m;
cin >> n >> m;
vector<int> a(n, 0);
for (auto& x : a) {
string s;
cin >> s;
for (auto c : s) {
x = x * 2 + (c == 'o');
}
}
int ans = n, up = (1 << n);
for (int i = 0; i < up; i++) {
int cnt = 0;
for (int j = 0; j < n; j++) {
if (i & (1 << j)) {
cnt |= a[j];
}
}
if (cnt == (1 << m) - 1) {
ans = min(ans, __builtin_popcount(i));
}
}
cout << ans << '\n';
return 0;
}
D - Souvenirs (abc358 D)
題目大意
\(n\)個盒子,第 \(i\)個盒子價格 \(a_i\),有 \(a_i\)個糖果。
買 \(m\)個盒子給 \(m\)個人,其中第 \(i\)個人的盒子的糖果數至少有 \(b_i\)個。
問花費價格的最少值,或告知不可行。
解題思路
考慮每個人,買哪個盒子給他。
由於盒子的糖果數和價格是相當的,那對於每個人的\(b_i\),肯定是選擇\(\geq b_i\)的最小的 \(a_i\),二分查詢即可。由於每個盒子只能買一次,因此得將其刪去,用 multiset
維護即可。
下述程式碼可以解決糖果數與價格不相當的情況,按照\(b_i\)從大到小考慮,那我肯定是貪心地選最小价格的盒子,滿足 \(a_i \geq b_i\)。用優先佇列維護這個最小值即可,而剩下未選擇的盒子都可以滿足後續的 \(b_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, m;
cin >> n >> m;
vector<int> a(n), b(n);
for (auto& x : a)
cin >> x;
for (auto& x : b)
cin >> x;
sort(a.begin(), a.end(), greater<int>());
sort(b.begin(), b.end(), greater<int>());
priority_queue<int, vector<int>, greater<int>> q;
bool ok = true;
LL ans = 0;
for (int i = 0, j = 0; i < m; i++) {
while (j < n && a[j] >= b[i]) {
q.push(a[j]);
j++;
}
if (q.empty()) {
ok = false;
break;
}
ans += q.top();
q.pop();
}
if (!ok)
ans = -1;
cout << ans << '\n';
return 0;
}
E - Alphabet Tiles (abc358 E)
題目大意
給定\(k\)和\(26\)個 \(c_i\),問字串數量,其 長度在\(1 \sim k\)之間,且第 \(i\)個字母的出現次數不超過 \(c_i\)。
解題思路
先列舉長度\(len\),然後考慮 \(len\)個字母分別是什麼字母。
考慮每個字母使用的數量,可以發現我們只需知道此時剩餘字母數,就可以作出轉移。
設 \(dp[i][j]\)表示使用了前 \(i\)類字母,填了\(j\) 個空位的方案數。
列舉當前字母使用的數量\(k\),轉移則為\(dp[i][j + k] += dp[i - 1][j] \times C_{len - j}^{k}\),即要從當前剩餘的\(len - j\)個空位選 \(k\)個作為當前的字母。
狀態數是 \(O(26n)\),轉移是 \(O(n)\),加上我們一開始列舉的長度複雜度 \(O(n)\),總的時間複雜度是 \(O(26n^3)\),由於 \((n \leq 10^3\),會超時。
考慮最佳化,容易發現列舉不同的長度計算每個\(dp[i][j]\),會有很多重複的計算。但是轉移代價會依賴總長度\(len\)。
考慮我們先計算 \(len = k\)的 \(dp[i][j]\),即長度為 \(k\)的符合條件的字串數量,看看能否得到其餘 \(len\)的數量。
很顯然\(dp[26][k]\)是長度為 \(k\)的字串數量,而 \(dp[26][k-1],dp[26][k-2],...\)同樣是長度為 \(k\)的字串,但有 \(1,2,...\)個空位上的字母是未確定的,其中空位的位置也有很多種情況。
以 \(dp[26][k-1]\)為例,它表示考慮了前 \(26\)個字母,填了 \(k-1\)個空位的方案數,此時還有一個空位。它相當於是,原先\(k-1\)個字母的方案數,再插入一個空位。 而空位的位置數量有 \(C_{k}^{1}\)種, 考慮 \(\frac{dp[26][k-1]}{C_{k}^{1}}\),即將空位的情況數去掉,其值就變成了長度為 \(k-1\)的符合條件的字串數量。
其餘情況同理。這就把列舉長度的複雜度最佳化掉了。
因此答案就是 \(\sum_{i=1}{k} \frac{dp[26][k - i]}{C_{n}^{i}}\),總的時間複雜度是 \(O(26n^2)\)。
神奇的程式碼
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int mo = 998244353;
long long qpower(long long a, long long b) {
long long qwq = 1;
while (b) {
if (b & 1)
qwq = qwq * a % mo;
a = a * a % mo;
b >>= 1;
}
return qwq;
}
long long inv(long long x) { return qpower(x, mo - 2); }
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n;
cin >> n;
array<int, 26> cnt;
for (auto& x : cnt)
cin >> x;
vector<int> dp(n + 1);
dp[0] = 1;
vector<int> fac(n + 1, 1), ifac(n + 1, 1);
for (int i = 1; i <= n; ++i) {
fac[i] = 1ll * fac[i - 1] * i % mo;
}
ifac[n] = inv(fac[n]);
for (int i = n - 1; i >= 0; --i) {
ifac[i] = 1ll * ifac[i + 1] * (i + 1) % mo;
}
auto C = [&](int n, int m) -> int {
if (n < m)
return 0;
return 1ll * fac[n] * ifac[m] % mo * ifac[n - m] % mo;
};
for (int i = 0; i < 26; ++i) {
vector<int> dp2(n + 1, 0);
for (int j = 0; j <= n; ++j) {
for (int k = 0; k <= cnt[i] && j + k <= n; ++k) {
dp2[j + k] = (dp2[j + k] + 1ll * dp[j] * C(n - j, k) % mo) % mo;
}
}
dp.swap(dp2);
}
int ans = 0;
for (int i = 1; i <= n; ++i) {
ans = (ans + 1ll * dp[i] * inv(C(n, n - i)) % mo) % mo;
}
cout << ans << '\n';
return 0;
}
F - Easiest Maze (abc358 F)
題目大意
給定\(n,m,k\),構造一個 \(n \times m\)的迷宮,從右上走到右下,路徑唯一,長度為 \(k\)。
解題思路
<++>
神奇的程式碼
G - AtCoder Tour (abc358 G)
題目大意
給定一個二維網格,格子上有數。
給定起點,重複以下操作\(k\)次。
- 每次操作,要麼不動,要麼上下左右四個方向選一個移動一格。操作完後,獲得收益,收益為格子上的數。
問最優操作下的收益和的最大值。
解題思路
觀察最優情況的特點,一定是從起點出發,到達某一個點,然後一直停留直到操作次數到達\(k\)。
因此首先列舉終點\(a_{ij}\),然後計算從起點到終點,怎樣走收益最大。
我從起點到達終點時,路徑有很多,不同路徑下,最終收益不一樣,而最終收益關係到兩個狀態:已經獲得的收益值\(presum\),還剩下的操作次數\(cnt\)。
收益最大,則要求\(presum + cnt \times a_{ij}\)最大。
觀察上述式子,它並不意味著我越短路徑到達\(a_{ij}\)是最優的。
考慮一極端情況, \(x \to y\)耗時三步,收益為 \(1,1\) ,\(a_y=10^6\),而另一個方案,耗時五步,收益為\(10^6-1, 10^6-1, 10^6 - 1,10^6-1\),兩種方案,後面的 \(k-4\)操作的收益相同,而前 \(4\)次的收益,顯然是後者高:雖然後者耗時5步,但損失很少:只有\(4\),而前者雖然耗時 \(3\)步,但每步的損失高達 \(10^6\)。這啟示我們要以最小損失代價
到達終點。
換句話說,對上述式子變形,注意到 \(presum = \sum_{k - cnt} a_x\),即 \(k-cnt\)個 \(a_x\)的和,我們將式子改寫成 \(k \times a_{ij} - (k - cnt) a_{ij} + \sum_{k - cnt} a_x\)。注意到後兩項的項數相同,合併一下,得到
前一項是一個定值,而後一項可以假象在一個新的網格圖上走,格子數變為\(b_x = a_{ij} - a_x\)(上述的損失代價
),問 從起點到終點的最短路徑(損失代價最小)。 這麼操作其實就相當於把步數損失
放到每一步的計算裡,從而消去了棘手的\(cnt\)(這個平均值的處理技巧差不多)
但有個問題是,最短路徑裡,邊權有負(\(x \to y\),邊權是 \(b_y\) ),則求起點到終點的最短路,不能用\(dijkstra\),但這是網格圖, \(SPFA\)會被卡死了。怎麼辦呢。
可以先考慮終點取 \(a_{ij}\)最大的,那麼 \(b_x\)都是正的,此時可以用 \(dijkstra\)求最短路。然後考慮次大的 \(a_{ij}\),則原本 最大的格子的 \(b_x\)變成負的,此時求最短路怎麼辦呢?細想會發現,如果最短路會經過這個負格子,那我可以就停留在這個格子上(這樣我的損失代價會不斷減小),這樣最終收益比到達終點更大。
因此會發現這個最短路徑中,它不會經過 \(b_x\)是負數的格子。由此實際上就是一個正權的 \(dijkstra\)最短路問題。
由此求出最短路徑,即最小損失代價,從而就知道此時終點的最優收益。對所有終點的最優收益取個最大值即為答案。
列舉終點的複雜度是\(O(hw)\), \(dijkstra\)的複雜度是 \(O(hw \log hw)\),因此總的時間複雜度為\(O((hw)^2 \log hw)\)。
有點傻傻了,好像一個樸素的\(DP\)就解決了。
首先還是注意到最優情況下,一定是從起點出發,到達某一個點,然後一直停留直到操作次數到達\(k\)。
而到達某個格子時,並不一定是最短距離到達最好,因為最後的收益包含兩部分,\(presum\)和 \(cnt\)(和上述同意義):\(presum + cnt \times a_{ij}\)最大,而每個\(cnt\)都可能作為最終的答案。
而 \(cnt\)的範圍最大就是 \(O(hw)\),因此我們可以保留這個狀態,即設 \(dp[c][i][j]\)表示走 \(c\)步到達 \(a_{ij}\)的最大收益,即\(presum\) 。那最終的答案就是\(\max_{i, j, c} dp[c][i][j] + (k - c) \times a_{ij}\)。
狀態數是\(O((hw)^2)\),轉移是 \(O(1)\),因此總的時間複雜度為 \(O((hw)^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 h, w, k, sx, sy;
cin >> h >> w >> k >> sx >> sy;
--sx, --sy;
vector<vector<int>> a(h, vector<int>(w));
for (auto& i : a)
for (auto& j : i)
cin >> j;
LL ans = 0;
array<int, 4> dx = {0, 0, 1, -1}, dy = {1, -1, 0, 0};
auto solve = [&](int ex, int ey) {
LL sum = 1ll * a[ex][ey] * k;
vector<vector<int>> cost(h, vector<int>(w));
for (int i = 0; i < h; ++i)
for (int j = 0; j < w; ++j)
cost[i][j] = a[ex][ey] - a[i][j];
priority_queue<pair<LL, pair<int, int>>> team;
vector<vector<LL>> dis(h, vector<LL>(w, numeric_limits<LL>::max()));
team.push({0, {sx, sy}});
dis[sx][sy] = 0;
while (!team.empty()) {
auto [d, p] = team.top();
team.pop();
auto [x, y] = p;
if (dis[x][y] < -d)
continue;
for (int i = 0; i < 4; ++i) {
int nx = x + dx[i], ny = y + dy[i];
if (nx < 0 || nx >= h || ny < 0 || ny >= w)
continue;
if (cost[nx][ny] < 0)
continue;
if (dis[nx][ny] > -d + cost[nx][ny]) {
dis[nx][ny] = -d + cost[nx][ny];
team.push({-dis[nx][ny], {nx, ny}});
}
}
}
return sum - dis[ex][ey];
};
for (int i = 0; i < h; ++i) {
for (int j = 0; j < w; ++j) {
if (a[i][j] >= a[sx][sy]) {
LL ret = solve(i, j);
ans = max(ans, ret);
}
}
}
cout << ans << '\n';
return 0;
}