A - Count Takahashi (abc359 A)
題目大意
給定\(n\)個字串,問有多少個字串是Takahashi
解題思路
注意判斷比較即可。
神奇的程式碼
#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;
int ans = 0;
while (n--) {
string s;
cin >> s;
ans += s == "Takahashi";
}
cout << ans << '\n';
return 0;
}
B - Couples (abc359 B)
題目大意
給定\(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;
cin >> n;
n *= 2;
vector<int> a(n);
for (auto& x : a)
cin >> x;
int ans = 0;
for (int i = 1; i < n - 1; ++i) {
ans += a[i - 1] == a[i + 1];
}
cout << ans << '\n';
return 0;
}
C - Tile Distance 2 (abc359 C)
題目大意
給定一個座標系,有格子,如下:
給定起點和終點,問從起點到終點,要穿過多少次藍線。
解題思路
觀察上述格子,可以發現在\(y\)軸移動,每移動一次,必定穿過一次藍線。
由於每行格子交錯排列的,每往上走一個,我左右可走的區間都擴大了\(1\)。比如我在\((5,0)\),我可以左邊往上走到\((3,1) \to (5,1)\)的格子,也可以右邊往上走到\((5,1) \to (7,1)\)。
這樣,原本我左右走的橫座標區間是\([4,6)\),往上走一格後,橫座標區間擴大為\([3,7)\),往上走\(n\)格,可到達的橫座標區間範圍為\([4-n, 6+n)\),只要我終點的橫座標在這區間,那我就可以只花費\(y\)軸移動的代價就抵達終點了。而如果不在這個區間,那就再左右移動,每移動一次,橫座標區間就變動\(2\)。
容易發現這樣移動一定是最優的。\(y\)軸移動的藍線穿過不可避免,然後\(x\)軸的藍線穿過已經儘可能在移動\(y\)軸時避免了。
神奇的程式碼
#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 sx, sy, tx, ty;
cin >> sx >> sy >> tx >> ty;
LL dy = abs(sy - ty);
LL odd = (sx & 1);
LL l = sx - odd + (sy & 1) * (odd ? 1 : -1);
LL r = l + 2;
l -= dy, r += dy;
LL ans = dy + max(0ll, l - tx + 1) / 2 + max(0ll, tx - r + 2) / 2;
cout << ans << '\n';
return 0;
}
D - Avoid K Palindrome (abc359 D)
題目大意
給定一個包含AB?
的字串\(s\),將?
變成A
或B
,問有多少種情況,使得\(s\)沒有長度為\(k\)的迴文子串。
解題思路
注意\(k \leq 10\)
從左到右考慮每個字元,如果當前是?
,則考慮其變為A
,B
,是否出現長度為\(k\)的迴文串。
我們需要知道該?
前\(k-1\)位的情況,加上該字母,就可以判斷出新增的子串是不是迴文串。
即設\(dp[i][j]\)表示考慮前\(i\)位字元,其中?
都已經替換成A
或B
後,且後\(9\)位的字元狀態為\(j\)(因為只有AB
兩種,可以編碼成01
,用二進位制壓縮表示)。
然後考慮當前位的情況,如果取值為A
即\(0\),則判斷\(j << 1\)狀態是不是迴文串,不是的話則有\(dp[i+1][(j<<1) \& mask] += dp[i][j]\),否則就狀態非法,不轉移。因為\(j<<1\)是後\(10\)個字元的狀態資訊,而\(j\)的含義是後\(9\)位,所以\(\& mask\)是把第\(10\)位去掉。
同理,取值為\(B\)的話,即\(1\),則判斷\((j << 1) | 1\)是不是迴文串,不是的話就轉移,否則不轉移。
可以事先預處理每個狀態是否是迴文串,然後當\(i \geq k\)時再考慮轉移的合法性。
神奇的程式碼
#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, k;
string s;
cin >> n >> k >> s;
int up = (1 << k);
vector<int> p(up);
for (int i = 0; i < up; i++) {
vector<int> bit(k);
int num = i;
for (int j = 0; j < k; j++) {
bit[j] = (num & 1);
num >>= 1;
}
auto rev = bit;
reverse(rev.begin(), rev.end());
p[i] = rev == bit;
}
up = 1 << (k - 1);
int mask = up - 1;
vector<int> dp(up, 0);
dp[0] = 1;
for (int i = 0; i < n; ++i) {
int chr = s[i];
vector<int> dp2(up, 0);
for (int j = 0; j < up; j++) {
if (chr == '?') {
if (i + 1 < k || !p[j << 1]) {
int nxt = (j << 1) & mask;
dp2[nxt] = (dp2[nxt] + dp[j]) % mo;
}
if (i + 1 < k || !p[j << 1 | 1]) {
int nxt = (j << 1 | 1) & mask;
dp2[nxt] = (dp2[nxt] + dp[j]) % mo;
}
} else {
if (i + 1 < k || !p[j << 1 | (chr - 'A')]) {
int nxt = (j << 1 | (chr - 'A')) & mask;
dp2[nxt] = (dp2[nxt] + dp[j]) % mo;
}
}
}
dp.swap(dp2);
}
LL ans = 0;
for (int i = 0; i < up; i++) {
ans = (ans + dp[i]) % mo;
}
cout << ans << '\n';
return 0;
}
E - Water Tank (abc359 E)
題目大意
給定柱子長度。然後如下如所示。
每一時刻,\(0\)位會多一高度的水,如果該水高度高過柱子,且高過\(1\)位的水高度,則該高度的水會跑到\(1\)位,同理繼續判斷\(1\)位,該水是否跑到\(2\)位。
問每一位出現水的最早時刻。
解題思路
-
考慮\(1\)位,其答案就是第一根柱子高度\(3(a_1)+1\)
-
考慮\(2\)位,需要\(0\)位水高\(3\),\(1\)位水高\(1\),答案就是\(3+1+1\)
-
考慮\(3\)位,則需要\(0,1,2\)的水高均為\(4\),答案就是\(4+4+4+1\)
-
考慮\(4\)位,則需要\(0,1,2\)水高\(4\),\(3\)位水高\(1\),答案就是\(4+4+4+1+1\)
-
考慮\(5\)位,則需要\(0,1,2,3,4,\)位水高\(5\),答案就是\(5+5+5+5+5+1\)。
觀察上述例子的求解過程,如果要求第\(i\)位的答案,則要求第\(i-1\)位裝滿
,裝滿的意思就是和柱子\(a_i\)高度同高,而同高會連帶著\(i-2,i-3,...\)位同高,但需要多少位呢?觀察上述會發現,假設前面比柱子\(a_i\)還高的柱子是\(a_j\),那麼\(j,j+1,...,i-1\)位的水高都必須是\(a_i\)。
因此,求解第\(i\)位的答案,則需要\(i-1,i-2,...,j\)位與\(a_i\)同高,然後\(j-1,j-2,...,k\)與\(a_j\)同高,然後\(k-1,k-2,...\)與\(a_k\)同高,其中\(a_i \leq a_j \leq a_k\),也就是說需要維護一個從左到右,柱子高度單調遞減的序列,這可以用棧維護,即單調棧,棧底是最左邊,棧頂是最右邊。在維護遞減高度時,順帶維護每個遞減區間的水高度總和即可。
神奇的程式碼
#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> h(n + 1);
for (int i = 1; i <= n; ++i)
cin >> h[i];
h[0] = 1e9 + 8;
vector<int> hei;
hei.push_back(0);
LL ans = 0;
for (int i = 1; i <= n; ++i) {
while (!hei.empty() && h[hei.back()] <= h[i]) {
ans -= h[hei.back()] * (LL)(hei.back() - hei[hei.size() - 2]);
hei.pop_back();
}
ans += h[i] * (LL)(i - hei.back());
hei.push_back(i);
cout << ans + 1 << " \n"[i == n];
}
return 0;
}
F - Tree Degree Optimization (abc359 F)
題目大意
給定\(n\)個點的點權\(a_i\),構造一棵樹,使得\(\sum_{i=1}^{n} d_i^2a_i\)最小,其中\(d_i\)表示點\(i\)的度。
解題思路
由於是一棵樹,則有\(\sum d_i = 2n-2, 1 \leq d_i \leq n - 1\)。
對於任意滿足上述條件的\(d_i\),都可以構造出對應的樹,使得每個點的度數都是\(d_i\)。(構造方法為,每次選擇度數為1
和非1
的點連邊,然後更新剩餘度數,歸納可證)
那剩下就是如何分配這些度數。
如果給點\(1\)分配一個度,是其\(d_1 = 1 \to 2\),則代價是\(4a_1 - a_1\),而如果是\(d_1 = 2 \to 3\),則代價是\(9a_1 - 4a_1\)。
這可以把問題抽象成每個點起始度數為\(1\),然後把剩下的\(n-2\)個度分配給每個點,使得代價最小,每次僅分配\(1\)的度,那我肯定是貪心的分配給代價最小的點。
用優先佇列維護上述代價即可。
神奇的程式碼
#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;
priority_queue<pair<LL, int>, vector<pair<LL, int>>, greater<pair<LL, int>>>
q;
LL ans = accumulate(a.begin(), a.end(), 0ll);
for (int i = 0; i < n; i++) {
q.push({a[i] * 3ll, 2});
}
for (int i = 0; i < n - 2; i++) {
auto [x, y] = q.top();
q.pop();
ans += x;
if (y < n - 1) {
LL ori = x / (2 * y - 1);
LL nxt = ori * (2 * y + 1);
q.push({nxt, y + 1});
}
}
cout << ans << '\n';
return 0;
}
G - Sum of Tree Distance (abc359 G)
題目大意
給定一棵樹,點有點權\(a_i\)。求\(\sum_i \sum_j f(i,j)\),其中\(a_i == a_j\)。\(f(i,j)\)表示點\(i \to j\)的距離,邊權為\(1\)。
解題思路
距離的最終來源是邊數,考慮每條邊被算入了多少次,即對答案貢獻的次數。
即\(\sum_i \sum_j f(i,j) = \sum_e sum_e\),其中\(sum_e\)表示邊\(e\)對答案貢獻的次數,考慮該次數怎麼算。
考慮邊\((u,v)\),將該樹分成了兩個連通塊,如果這兩個連通塊各有一點\(i,j\),其\(a_i == a_j\),那麼從點\(i \to j\)必定經過該邊,因此需要統計每個點權,在兩個連通塊的出現次數,其乘積的和則是該邊的貢獻。
問題就變成了統計一個子樹裡,各個點權的出現次數\(cc_i\),事先預處理每個點權的出現次數\(cnt_i\),對點權求和,即\(\sum cc_i \times (cnt_i - cc_i)\)就是該邊對答案的貢獻。
由於點權是稀疏的,用map
來維護出現次數,合併兒子之間的map
,採用啟發式合併
,即用數量少的合併到數量大的,這樣每次合併最壞的複雜度是\(O(\frac{n}{2})\),而最壞的情況最多隻有\(O(\log n)\)次(每一次最壞情況,合併後的點數會翻倍,最多翻倍\(O(\log n)\)次。
合併的時候,計算邊貢獻的式子,\(\sum cc_i \times (cnt_i - cc_i)\)只有一項發生變化,可以動態\(O(1)\)維護出更新後的貢獻\(sum\)。
最終的時間複雜度就是\(O(n \log^2 n)\),一個\(\log\)是啟發式合併,另一個\(\log\)是map
。
程式碼裡的\(sum\)是考慮父親邊\(u \to fa\)對答案的貢獻。由於返回型別是\(pair\),用\(map\)構造\(pair\)會複製構造造成巨大的效能損失,用\(move\)函式進行移動構造。或者返回值僅為\(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;
cin >> n;
vector<vector<int>> edge(n);
for (int i = 0; i < n - 1; i++) {
int u, v;
cin >> u >> v;
u--;
v--;
edge[u].push_back(v);
edge[v].push_back(u);
}
vector<int> a(n);
vector<int> cnt(n);
for (auto& x : a) {
cin >> x;
--x;
cnt[x]++;
}
LL ans = 0;
auto dfs = [&](auto& dfs, int u, int fa) -> pair<map<int, int>, LL> {
map<int, int> cc;
LL sum = 0;
for (auto v : edge[u]) {
if (v == fa)
continue;
auto&& [son_ret, son_sum] = dfs(dfs, v, u);
if (son_ret.size() > cc.size()) {
swap(son_ret, cc);
swap(son_sum, sum);
}
for (auto& [k, v] : son_ret) {
sum -= 1ll * cc[k] * (cnt[k] - cc[k]);
cc[k] += v;
sum += 1ll * cc[k] * (cnt[k] - cc[k]);
}
}
sum -= 1ll * cc[a[u]] * (cnt[a[u]] - cc[a[u]]);
cc[a[u]]++;
sum += 1ll * cc[a[u]] * (cnt[a[u]] - cc[a[u]]);
ans += sum;
return {move(cc), sum};
};
dfs(dfs, 0, 0);
cout << ans << '\n';
return 0;
}