A - 369 (abc369 A)
題目大意
給定兩個數\(a,b\),問有多少個整數\(x\),使得 \(a,b,x\)經過某種排列後成為等差數列,
解題思路
就三種情況:\(xab\),\(axb\), \(abx\),三種情況都求出來,然後放到 set
去重即為答案。中間的情況要判斷是否是實數。
神奇的程式碼
#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 a, b;
cin >> a >> b;
if (a > b)
swap(a, b);
set<int> ans{a - (b - a), b + (b - a)};
if ((b - a) % 2 == 0)
ans.insert(a + (b - a) / 2);
cout << ans.size() << '\n';
return 0;
}
B - Piano 3 (abc369 B)
題目大意
談鋼琴,給出左右手依次要彈奏的鍵,問左右手移動的距離數。
解題思路
模擬即可,用一個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;
int ans = 0;
map<char, int> pos;
while (n--) {
int p;
string s;
cin >> p >> s;
if (pos.find(s[0]) != pos.end()) {
ans += abs(pos[s[0]] - p);
}
pos[s[0]] = p;
}
cout << ans << '\n';
return 0;
}
C - Count Arithmetic Subarrays (abc369 C)
題目大意
給定一個陣列\(a\),問有多少個 \(l,r\),使得 \(a[l..r]\)是一個等差數列。
解題思路
等差數列即公差相等。從\(a\)的差分陣列\(b\)來看, \(a[l..r]\)是等差數列,意味著差分陣列的對應區間的數是相等的,那就是說,對於\(a[l..r]\)是等差數列的 \(l,r\)對數,等價於 \(b[i..j]\)是相同數的對數。(特判下\(a[l..r]\)長度是 \(1\)的情況)
那先求 \(a\)的差分陣列\(b\),然後對該差分陣列的相同數的區間
,比如 \(b[i..j] = c\),那麼對於\(a\)陣列符合條件的 \(l,r\) 就有 \(\frac{(j - i + 1)(j - 1)}{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 n;
cin >> n;
vector<int> a(n);
for (auto& i : a)
cin >> i;
vector<int> l(n);
int la = -1;
int cnt = 0;
LL ans = n;
for (int i = 1; i < n; ++i) {
if (a[i] - a[i - 1] != la) {
ans += 1ll * cnt * (cnt - 1) / 2;
cnt = 2;
la = a[i] - a[i - 1];
} else {
++cnt;
}
}
ans += 1ll * cnt * (cnt - 1) / 2;
cout << ans << '\n';
return 0;
}
D - Bonus EXP (abc369 D)
題目大意
\(n\)個怪獸,你要依次打他們。
對於第 \(i\)只怪獸,要麼與它戰鬥,要麼放走他。
如果與它戰鬥,你會獲勝,且會獲得 \(x_i\)經驗。如果它是你第偶數只打敗的怪獸,則還可以額外獲得 \(x_i\)經驗,即共獲得雙倍經驗。
問獲得的經驗數的最大值。
解題思路
比較樸素的\(dp\),很顯然對於每隻怪獸考慮打或不打,如果選擇打,其結果會受到 是否是第偶數只打敗
這一狀態的影響,因此我們的\(dp\)狀態,除了包含基本狀態 考慮前$i$只怪獸
外,還要加上狀態打敗了奇數/偶數只怪獸
這一\(0/1\)狀態。
有了這一狀態後,就可以寫出關於經驗的轉移式子了。即 \(dp[i][0/1]\)表示考慮前 \(i\)只怪獸,已經打敗了偶數只/奇數只怪獸時,獲得的最大經驗值。
然後考慮第\(i\)只打或不打 ,得到對應的經驗值,轉移到後續狀態即可。
神奇的程式碼
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
LL inf = 1e18;
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;
array<LL, 2> dp = {0, -inf};
for (int i = 0; i < n; i++) {
array<LL, 2> dp2 = {0, 0};
dp2[0] = max(dp[0], dp[1] + a[i] + a[i]);
dp2[1] = max(dp[1], dp[0] + a[i]);
dp2.swap(dp);
}
cout << max(dp[0], dp[1]) << '\n';
return 0;
}
E - Sightseeing Tour (abc369 E)
題目大意
給定一張無向圖,邊有邊權。
回答\(q\)個詢問。
每個詢問給定 \(k \leq 5\)條邊,表示從\(1 \to n\),必須經過至少一次這些邊,的最短路徑。
解題思路
這裡給的邊數很少。
考慮最簡單的情況,即\(k=1\),給的邊是\(u,v\),那麼很顯然答案就是 \(1 \to u \to v \to n\)或者 \(1 \to v \to u \to n\), 即考慮從\(1\)節點出發,以最短路先到 \(u\)還是先到 \(v\)。
這裡 \(k=5\),但情況數仍然不多,我們仍然列舉中途經過的點,共有\(O(k! 2^k)\)種情況(列舉遍歷邊的順序,對於每條邊再列舉訪問端點的順序), \(k=5\)的話就是 \(3e3\),情況數不大,有了經過的點之後,剩下的就是以最短路徑依次遍歷每個點。由於 \(n\leq 400\),可以事先用 \(floyd\)求出任意兩點的距離,然後對於每個詢問,花費 \(O(k! 2^k)\)列舉遍歷點的順序,然後用 \(O(2k)\)計算該順序對應的最短路長度,所有情況取最小即為答案。
總的時間複雜度為\(O(n^3 + q(k! 2^k + k))\)
神奇的程式碼
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const LL inf = 1e18;
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n, m;
cin >> n >> m;
vector<array<int, 3>> edge(m);
vector<vector<LL>> dis(n, vector<LL>(n, inf));
for (int i = 0; i < n; i++)
dis[i][i] = 0;
for (int i = 0; i < m; i++) {
int u, v, w;
cin >> u >> v >> w;
--u, --v;
dis[u][v] = min(dis[u][v], (LL)w);
dis[v][u] = min(dis[v][u], (LL)w);
edge[i] = {u, v, w};
}
for (int k = 0; k < n; ++k) {
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
}
}
}
int q;
cin >> q;
auto calc = [&](vector<int>& p) -> LL {
LL sum = 0;
int st = 0;
for (int i = 0; i < p.size(); i += 2) {
int u = p[i], v = p[i + 1];
sum += dis[st][u];
st = v;
}
sum += dis[st][n - 1];
return sum;
};
while (q--) {
int k;
cin >> k;
vector<int> b(k);
for (auto& x : b) {
cin >> x;
--x;
}
int up = (1 << k);
LL ans = inf;
do {
for (int i = 0; i < up; ++i) {
vector<int> p;
LL sumb = 0;
for (int j = 0; j < k; ++j) {
auto [u, v, w] = edge[b[j]];
sumb += w;
if ((i >> j) & 1) {
swap(u, v);
}
p.push_back(u);
p.push_back(v);
}
LL sum = calc(p) + sumb;
ans = min(ans, sum);
}
} while (next_permutation(b.begin(), b.end()));
cout << ans << '\n';
}
return 0;
}
F - Gather Coins (abc369 F)
題目大意
\(h\times w\)網格,有些格子有金幣。
從左上走到右下,只能向右走和向下走。
問取得金幣的最大值。
解題思路
樸素\(dp\)就是 \(dp[i][j] = \max(dp[i - 1][j], dp[i][j - 1]) + (coin[i][j] == 1)\),但\(h \times w\)可達 \(1e10\),整不動。但金幣數最多隻有 \(10^5\),我們知道 \(dp\)的值只有在有金幣的格子才會變動,實際有效的格子只有 \(10^5\)個。我們僅考慮這些格子的 \(dp\)值怎麼計算。
考慮 \(dp[i][j]\)表示當前處於有金幣的格子 \((i,j)\)時的最大金幣數,考慮能夠轉移到此的狀態,即 $dp[i][j] = \max_{x \leq i, y \leq j, coin[i][j]}(dp[x][y]) + 1。
這個轉移條件其實就是個二維偏序,因此對金幣的位置\((x,y)\)從小到大排序,然後依次列舉這些金幣,當考慮到第 \(i\)個金幣時, \(j \leq i\)的金幣 一定滿足\(x_j \leq x_i\),因此我們只需找到 \(y_j \leq y_i\)的最大的 \(dp[x_j][y_j]\)值即可,這是一個區間最值查詢,用線段樹維護即可。
即對金幣的位置\((x,y)\)從小到大排序,然後依次列舉這些金幣,用線段樹維護列舉過的金幣關於y_j下標的dp最大值
。考慮上述的轉移條件,線上段樹查詢時,由於列舉順序的緣故,天然滿足\(x \leq i\)的條件,而線段樹的區間查詢找到滿足 \(y < j\)的 \(\max(dp[x][y])\),因此上述的二維偏序的最值問題就可以用線段樹解決了。
神奇的程式碼
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int N = 2e5 + 8;
const int inf = 1e9 + 7;
class segment {
#define lson (root << 1)
#define rson (root << 1 | 1)
public:
int val[N << 2];
int id[N << 2];
void pushup(int root) {
if (val[lson] > val[rson]) {
val[root] = val[lson];
id[root] = id[lson];
} else {
val[root] = val[rson];
id[root] = id[rson];
}
}
void build(int root, int l, int r) {
if (l == r) {
val[root] = -inf;
return;
}
int mid = (l + r) >> 1;
build(lson, l, mid);
build(rson, mid + 1, r);
pushup(root);
}
void update(int root, int l, int r, int pos, int v, int i) {
if (l == r) {
if (val[root] < v) {
val[root] = v;
id[root] = i;
}
return;
}
int mid = (l + r) >> 1;
if (pos <= mid)
update(lson, l, mid, pos, v, i);
else
update(rson, mid + 1, r, pos, v, i);
pushup(root);
}
pair<int, int> query(int root, int l, int r, int L, int R) {
if (L <= l && r <= R) {
return {val[root], id[root]};
}
int mid = (l + r) >> 1;
pair<int, int> resl{}, resr{};
if (L <= mid)
resl = query(lson, l, mid, L, R);
if (R > mid)
resr = query(rson, mid + 1, r, L, R);
return max(resl, resr);
}
} seg;
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int h, w, n;
cin >> h >> w >> n;
vector<array<int, 2>> pos(n + 2);
for (int i = 1; i <= n; ++i)
cin >> pos[i][0] >> pos[i][1];
pos[0] = {1, 1};
pos[n + 1] = {h, w};
sort(pos.begin(), pos.end());
seg.build(1, 1, w);
seg.update(1, 1, w, 1, 0, 0);
vector<int> tr(n);
for (int i = 1; i <= n + 1; ++i) {
auto& [x, y] = pos[i];
auto res = seg.query(1, 1, w, 1, y);
int dp = res.first + 1;
tr[i] = res.second;
seg.update(1, 1, w, y, dp, i);
}
auto [ans, p] = seg.query(1, 1, w, w, w);
cout << ans - 1 << '\n';
string op;
while (p != 0) {
auto [x1, y1] = pos[p];
p = tr[p];
auto [x2, y2] = pos[p];
auto dx = abs(x1 - x2);
auto dy = abs(y1 - y2);
if (dx) {
op += string(dx, "UD"[x1 > x2]);
}
if (dy) {
op += string(dy, "LR"[y1 > y2]);
}
}
reverse(op.begin(), op.end());
cout << op << '\n';
return 0;
}
G - As far as possible (abc369 G)
題目大意
給定一棵樹,邊有邊權。
對於\(k=1,2,...,n\),要求選 \(k\)個點,使得從 \(1\)號點出發,遍歷每個點,最終回到 \(1\)號點的距離的最小值最大。
解題思路
如果我給定了\(k\)個點,怎麼求這個的最小值呢。
容易發現答案其實就是這 \(k\)個點到根的路徑的並的長度的兩倍。
當 \(k=1\)時,很顯然我們選擇距離根最遠的點。
然後當 \(k=2\)時,由於先前的選擇,會導致一些點對答案的貢獻發生了變化——其到根的路徑有一部分與之前選擇的點到根的路徑有交集,那交集的部分不會有額外的貢獻。因此當我們選擇一個點後,除了一路沿父親節點更新貢獻外,還要更新父親兄弟節點及其子樹的貢獻改變。這個貢獻改變自然是一棵子樹,透過樹的\(dfs\) 序來維護這個貢獻改變,其實就是一個區間操作,可以用線段樹維護,其複雜度只有\(O(\log)\),而貢獻改變會發生多少次呢?一個點最多隻會帶來一次貢獻改變,因此最多區間操作 \(O(n)\)次,因此總的複雜度只有 \(O(n \log n)\)次。
即\(val[i]\)表示\(dfs\)序裡的第 \(i\)個節點,如果我選擇它,它對答案貢獻(增加)了多少。每次我們肯定選擇最大的 \(val\),選擇這個 \(val\)後,會使得一些子樹內的節點對答案的貢獻減少(減去交集路徑長度),每個子樹內的節點在 \(dfs\)序裡面對應了一個區間,因此我們用線段樹維護這個 \(val\)陣列,每次查詢就是個區間最值,每次更新貢獻就是個區間操作。
但如果從另一個角度來看,考慮對這棵樹進行長鏈剖分,容易發現答案就是最長的 \(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<vector<array<int, 2>>> edge(n);
for (int i = 0; i < n - 1; ++i) {
int u, v, w;
cin >> u >> v >> w;
--u, --v;
edge[u].push_back({v, w});
edge[v].push_back({u, w});
}
vector<int> mxson(n, -1);
vector<LL> deep(n, 0), maxdeep(n, 0);
function<void(int, int)> dfs1 = [&](int u, int fa) {
maxdeep[u] = deep[u];
for (auto [v, w] : edge[u]) {
if (v == fa)
continue;
deep[v] = deep[u] + w;
dfs1(v, u);
maxdeep[u] = max(maxdeep[u], maxdeep[v]);
if (mxson[u] == -1 || maxdeep[mxson[u]] < maxdeep[v]) {
mxson[u] = v;
}
}
};
dfs1(0, -1);
vector<LL> lian;
function<void(int, int, LL)> dfs2 = [&](int u, int fa, LL dis) {
for (auto [v, w] : edge[u]) {
if (v == fa)
continue;
if (v == mxson[u])
dfs2(v, u, dis + w);
else
dfs2(v, u, w);
}
if (mxson[u] == -1) {
lian.push_back(dis);
}
};
dfs2(0, -1, 0);
sort(lian.begin(), lian.end(), greater<LL>());
int up = 0;
LL ans = 0;
for (int i = 0; i < lian.size(); ++i) {
ans += lian[i];
cout << ans * 2 << '\n';
}
for (int i = lian.size(); i < n; ++i) {
cout << ans * 2 << '\n';
}
return 0;
}