2024 暑假友誼賽 5
A - 噴泉
CodeForces - 799C
思路
方案只有三種,一種是都用金幣,一種是都用鑽石,還有一種是一座金幣一座鑽石。
可以先列舉採用金幣的,一邊列舉的過程中,用線段樹維護剩下的金幣數可以建造的溫泉美觀度最大值,注意要建造兩座才可以算進答案中。
列舉完金幣的可以求出金幣數能建造的最大美觀度,然後按照列舉金幣的做法列舉鑽石,這裡可以把第三種方案一起更新,把剩下的鑽石能買的最大美觀度和前面求出的所有金幣能買的最大美觀求個最大值即可。
程式碼
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
template<class Node>
struct SegmentTree {
#define lc u<<1
#define rc u<<1|1
const int n, N;
vector<Node> tr;
SegmentTree(): n(0) {}
SegmentTree(int n_): n(n_), N(n * 4 + 10) {
tr.reserve(N);
tr.resize(N);
build(1, 1, n);
}
void build(int u, int l, int r) {
tr[u].l = l, tr[u].r = r;
if (l == r) {
tr[u] = {l, r, 0};
return ;
}
i64 mid = (l + r) >> 1;
build(lc, l, mid);
build(rc, mid + 1, r);
pushup(tr[u], tr[lc], tr[rc]);
};
void pushup(Node& U, Node& L, Node& R) { //上傳
U.l = L.l, U.r = R.r;
U.Max = max(L.Max, R.Max);
}
void modify(int u, int pos, i64 k) {
if (tr[u].l >= pos && tr[u].r <= pos) {
tr[u].Max = max(tr[u].Max, k);
return ;
}
int mid = (tr[u].l + tr[u].r) >> 1;
if (pos <= mid)
modify(lc, pos, k);
else
modify(rc, pos, k);
pushup(tr[u], tr[lc], tr[rc]);
}
Node query(int u, int l, int r) { //區查
if (l <= tr[u].l && tr[u].r <= r)
return tr[u];
i64 mid = tr[u].l + tr[u].r >> 1;
if (r <= mid)
return query(lc, l, r);
if (l > mid)
return query(rc, l, r);
Node U;
Node L = query(lc, l, r), R = query(rc, l, r);
pushup(U, L, R);
return U;
}
};
struct Node { //線段樹定義
i64 l, r;
i64 Max;
};
constexpr int N = 1e5 + 10, M = N - 10;
SegmentTree<Node> tr1(N), tr2(N);
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, a[2];
cin >> n >> a[0] >> a[1];
vector w(2, vector<array<int, 2>>());
for (int i = 0; i < n; i ++) {
int b, p;
char c;
cin >> b >> p >> c;
w[c - 'C'].push_back({b, p});
}
int ans = 0;
for (auto [val, cost] : w[0]) {
if (a[0] >= cost) {
int t = a[0] - cost > 0 ? tr1.query(1, 1, a[0] - cost).Max : 0;
if (t) {
ans = max(ans, val + t);
}
}
tr1.modify(1, cost, val);
}
int x = a[0] > 0 ? tr1.query(1, 1, a[0]).Max : 0;
for (auto [val, cost] : w[1]) {
if (a[1] >= cost) {
int t = a[1] - cost > 0 ? tr2.query(1, 1, a[1] - cost).Max : 0;
t = max(t, x);
if (t) {
ans = max(ans, val + t);
}
}
tr2.modify(1, cost, val);
}
cout << ans << '\n';
return 0;
}
B - 懶得起名了,就這樣吧
CodeForces - 1520D
思路
將 \(a_j-a_i=j-i\) 移項可得 \(a_i-i=a_j-j\) ,所以從左往右列舉的過程中,用 map 維護一下 \(a_i-i\) 的個數即可。
程式碼
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
void solve() {
i64 ans = 0;
map<int, i64> mp;
int n;
cin >> n;
for (int i = 1; i <= n; i ++) {
int x;
cin >> x;
ans += mp[x - i];
mp[x - i] ++;
}
cout << ans << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
solve();
}
return 0;
}
C - 怎麼你不服氣?
CodeForces - 510C
思路
注意到要滿足字串 \(s\) 小於字串 \(t\) ,則 \(s\) 與 \(t\) 的第一個不同字元的位置應該滿足 \(s_i<t_i\) ,所以我們根據這個建立單向關係,然後跑拓撲序求出可行的方案,如果存在環則說明存在 \(s_i<t_i \land t_i<s_i\),顯然這是不存在可行方案的。
另外如果存在 \(i<j\) ,而 \(t_j\) 是 \(s_i\) 的字首也是不可以的,因為題目要求字首相同時,較短的字串更小。
程式碼
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<string> s(n);
for (auto &i : s) {
cin >> i;
}
vector<int> in(30);
vector g(30, vector<int>());
set<string> ok;
for (auto c : s) {
if (ok.count(c)) {
cout << "Impossible\n";
return 0;
}
for (int i = 0; i < c.size(); i ++) {
ok.insert(c.substr(0, i));
}
}
for (int i = 0; i + 1 < n; i ++) {
for (int j = 0; j < s[i].size(); j ++) {
if (s[i][j] == s[i + 1][j]) continue;
int u = s[i][j] - 'a', v = s[i + 1][j] - 'a';
g[u].push_back(v);
in[v]++;
break;
}
}
queue<int> Q;
for (int i = 0; i < 26; i ++) {
if (!in[i]) {
Q.push(i);
}
}
vector<int> ans;
while (Q.size()) {
auto u = Q.front();
Q.pop();
ans.push_back(u);
for (auto v : g[u]) {
if (!--in[v]) {
Q.push(v);
}
}
}
if (ans.size() != 26) {
cout << "Impossible\n";
} else {
for (auto i : ans) {
cout << (char)(i + 'a');
}
}
return 0;
}
D - 她真的不一樣
CodeForces - 796A
思路
直接按順序列舉離 \(m\) 最近的位置即可。
程式碼
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m, k;
cin >> n >> m >> k;
vector<int> a(n + 1);
for (int i = 1; i <= n; i ++) {
cin >> a[i];
}
i64 ans = n * 10;
for (int i = 1; i <= n; i ++) {
if (!a[i] || a[i] > k) continue;
ans = min(ans, abs(i - m) * 10ll);
}
cout << ans << '\n';
return 0;
}
E - 陣列分割
CodeForces - 1557B
思路
題目說明不存在相同元素,即按照非遞減順序排序後其實就是一個遞增陣列,而這樣的遞增陣列一定是唯一的,所以我們可以將原陣列離散化後找連續的單調遞增區間即可。
程式碼
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
void solve() {
int k, n;
cin >> n >> k;
vector<int> a(n + 1);
for (int i = 1; i <= n; i ++) {
cin >> a[i];
}
auto b = a;
sort(b.begin() + 1, b.end());
for (int i = 1; i <= n; i ++) {
a[i] = lower_bound(b.begin() + 1, b.end(), a[i]) - b.begin();
}
int res = 0;
for (int i = 1; i <= n; i ++) {
int j = i;
while (j + 1 <= n && a[j + 1] == a[j] + 1 ) {
j ++;
}
res ++;
i = j;
}
cout << (res <= k ? "Yes" : "No") << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
solve();
}
return 0;
}
F - 穿越火線
AtCoder - abc292_h
思路
由題意可知,需要找到第一個 \(m\) 使得 \(\frac 1m (\sum\limits_{i=1}^mP_i)\ge K\),轉化式子可得:
設 \(A_i=P_i - K\),那麼就是找到使 \(\sum A_i\ge 0\) 的第一個 \(i\) 的位置,也就是 \(A_i\) 的字首和,結合將 \(P_c\) 修改成 \(x\) 可以想到單點修改,於是我們可以用線段樹維護最大字首和,透過線段樹二分找到該位置即可。
程式碼
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
template<class Node>
struct SegmentTree {
#define lc u<<1
#define rc u<<1|1
const int n, N;
vector<Node> tr;
SegmentTree(): n(0) {}
SegmentTree(int n_): n(n_), N(n * 4 + 10) {
tr.reserve(N);
tr.resize(N);
}
SegmentTree(vector<int> init) : SegmentTree(init.size() - 1) {
function<void(int, int, int)> build = [&](int u, int l, int r) {
tr[u].l = l, tr[u].r = r;
if (l == r) {
tr[u] = {l, r, init[l], init[l]};
return ;
}
i64 mid = (l + r) >> 1;
build(lc, l, mid);
build(rc, mid + 1, r);
pushup(tr[u], tr[lc], tr[rc]);
};
build(1, 1, n);
}
void pushup(Node& U, Node& L, Node& R) { //上傳
U.l = L.l, U.r = R.r;
U.sum = L.sum + R.sum;
U.presum = max(L.presum, L.sum + R.presum);
}
void modify(int u, int pos, int k) {
if (tr[u].l >= pos && tr[u].r <= pos) {
tr[u].sum = k;
tr[u].presum = k;
return ;
}
int mid = (tr[u].l + tr[u].r) >> 1;
if (pos <= mid)
modify(lc, pos, k);
else
modify(rc, pos, k);
pushup(tr[u], tr[lc], tr[rc]);
}
Node query(int u, int l, int r, i64 k) { //區查
if (tr[u].l == tr[u].r ) {
auto res = tr[u];
res.presum += k;
return res;
}
i64 mid = tr[u].l + tr[u].r >> 1;
if (k + tr[lc].presum >= 0)
return query(lc, l, r, k);
else
return query(rc, l, r, k + tr[lc].sum);
}
};
struct Node { //線段樹定義
i64 l, r;
i64 presum, sum;
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, k, q;
cin >> n >> k >> q;
vector<int> a(n + 1);
for (int i = 1; i <= n; i ++) {
cin >> a[i];
a[i] -= k;
}
SegmentTree<Node> tr(a);
while (q--) {
int c, x;
cin >> c >> x;
tr.modify(1, c, x - k);
auto res = tr.query(1, 1, n, 0);
long double ans = k + 1.L * res.presum / res.l;
cout << fixed << setprecision(15) << ans << '\n';
}
return 0;
}
G - 船型假髮
CodeForces - 1567C
思路
模擬樣例可以發現,奇數位的相加進位只會加到奇數位,偶數位同理,那麼不妨將奇數位和偶數位單獨拿出來,將 \(n\) 拆成 \(x\) 和 \(y\) ,例如 \(114514\),可以拆分成 \(154\) 和 \(141\),那麼拆分後的兩個數就會滿足正常的進位加法,接下來只要找到有多少種方案組成 \(x\) 和 \(y\) 即可。顯然,從 \((0,x),(1,x-1),\dots ,(x-1,1),(x,0)\) 一共有 \(x+1\) 種方案組成 \(x\),同理有 \(y+1\) 種方案組成 \(y\),所以最終有 \((x+1)\times (y+1)\) 種方案,但是題目要求 \(a,b\) 都要是正整數,所以 \((0,x),(0,y)\) 和 \((x,0),(y,0)\) 這兩組需要去掉。
即最終答案為 \((x+1)\times (y+1) -2\)。
程式碼
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
void solve() {
int n;
cin >> n;
i64 x = 0, y = 0;
string s = to_string(n);
for (int i = 0; i < s.size(); i ++) {
int t = s[i] - '0';
if (i & 1) x = x * 10 + t;
else y = y * 10 + t;
}
cout << (x + 1) * (y + 1) - 2 << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
solve();
}
return 0;
}
H - 劫!我要打劫!啊啊啊啊啊啊啊啊
AtCoder - abc322_e
思路
要求選擇一些物品滿足價值達到至少為 \(P\) 的最小花費,很顯然的揹包問題,只不過這裡是 \(K\) 維,由於 \(1\le K\le 5\),索性直接就開 \(5\) 維了。
需要注意一些細節的處理。
程式碼
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
constexpr int N = 7;
i64 dp[N][N][N][N][N];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, k, p;
cin >> n >> k >> p;
vector<int> a(k, p);
a.resize(5);
vector<int> cost(n + 1);
vector val(n + 1, vector<int>(5));
for (int i = 1; i <= n; i ++) {
cin >> cost[i];
for (int j = 0; j < k; j ++) {
int x;
cin >> x;
val[i][j] = x;
}
}
memset(dp, 62, sizeof dp);
dp[0][0][0][0][0] = 0;
for (int i = 1; i <= n; i ++) {
for (int j = a[0]; j >= 0; j--)
for (int j1 = a[1]; j1 >= 0; j1--)
for (int j2 = a[2]; j2 >= 0; j2--)
for (int j3 = a[3]; j3 >= 0; j3--)
for (int j4 = a[4]; j4 >= 0; j4--) {
auto& t = dp[min(a[0], j + val[i][0])][min(a[1], j1 + val[i][1])][min(a[2] , j2 + val[i][2])][min(a[3], j3 + val[i][3])][min(a[4], j4 + val[i][4])];
t = min(t , dp[j][j1][j2][j3][j4] + cost[i]);
}
}
if (dp[a[0]][a[1]][a[2]][a[3]][a[4]] != 4485090715960753726) {
cout << dp[a[0]][a[1]][a[2]][a[3]][a[4]] << '\n';
return 0;
}
cout << -1 << '\n';
return 0;
}
I - 等差數列
CodeForces - 1185D
思路
顯然可以先排序。
對於 \(n < 4\) ,那隻要刪掉任意一個即可滿足要求。
可以將排序後 \(a_2\sim a_{n-1}\) 之間的長度存下,如果最大長度等於最小長度,說明中間已經是等差數列了, 此時只需特判下 \(a_2-a_1\) 和 \(a_n-a_{n-1}\) 的長度即可。
考慮去掉一個元素後,會有三段長度產生影響,列舉中間的元素,假設有 \(a_{i-1},a_i,a_{i+1}\) ,而 \(a_i\) 是我們準備刪除的元素,當刪掉 \(a_i\) 後,那麼 \(a_i-a_{i-1}\) 和 \(a_{i+1}-a_i\) 就會消失,產生新的差分長度 \(a_{i+1}-a_{i-1}\),那此時只要看新加入 \(a_{i+1}-a_{i-1}\) 後,我們維護的長度最大最小值是否相等即可,相等則說明刪掉 \(a_i\) 後數列變成了等差數列,否則我們就把刪掉的 \(a_i-a_{i-1},a_{i+1}-a_i\) 加回到我們維護的長度中,繼續往後列舉。
程式碼
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
if (n < 4) {
cout << 1 << '\n';
return 0;
}
vector<int> a(n + 1);
vector<array<i64, 2>> b(n + 1);
for (int i = 1; i <= n; i ++) {
cin >> a[i];
b[i] = {a[i], i};
}
sort(b.begin() + 1, b.end());
multiset<i64> s;
for (int i = 2; i + 2 <= n; i ++) {
s.insert(b[i + 1][0] - b[i][0]);
}
i64 x = b[2][0] - b[1][0], y = b[n][0] - b[n - 1][0];
if (*s.begin() == *s.rbegin()) {
i64 t = *s.begin();
if (x == t) {
cout << b[n][1] << '\n';
return 0;
} else if (y == t) {
cout << b[1][1] << '\n';
return 0;
}
}
s.insert(x), s.insert(y);
for (int i = 2; i + 1 <= n; i ++) {
i64 x = b[i][0] - b[i - 1][0], y = b[i + 1][0] - b[i][0], z = b[i + 1][0] - b[i - 1][0];
s.erase(s.lower_bound(x));
s.erase(s.lower_bound(y));
s.insert(z);
if (*s.begin() == *s.rbegin()) {
cout << b[i][1] << '\n';
return 0;
}
s.erase(s.lower_bound(z));
s.insert(x);
s.insert(y);
}
cout << "-1\n";
return 0;
}
J - 超時空旅行
CodeForces - 1887B
思路
對於每個點,肯定是越早走到它越好,所以在存邊的時候可以把這個邊集的序號存下來,然後對於每個時刻開放的邊集,以邊集為索引把該時刻存下來,跑 Dijkstra 鬆弛邊的時候透過二分找到這條邊最早開放的時刻去更新即可。
程式碼
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
constexpr int N = 2e5 + 10;
vector<int> pos[N];
struct DIJ {
using i64 = long long;
using PII = pair<i64, i64>;
vector<i64> dis;
vector<vector<PII>> G;
DIJ() {}
DIJ(int n) {
dis.assign(n + 1, -1);
G.resize(n + 1);
}
void add(int u, int v, int w) {
G[u].emplace_back(v, w);
}
void dijkstra(int s) {
priority_queue<PII, vector<PII>, greater<PII>> que;
que.push({0, s});
while (!que.empty()) {
auto [_, u] = que.top();
que.pop();
if (dis[u] != -1) continue;
dis[u] = _;
for (auto [v, i] : G[u]) {
auto w = lower_bound(pos[i].begin(), pos[i].end(), dis[u]);
if (w != pos[i].end()) {
que.push({*w + 1, v});
}
}
}
}
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, t;
cin >> n >> t;
DIJ dij(n);
for (int i = 1; i <= t; i ++) {
int m;
cin >> m ;
for (; m; m--) {
int u, v;
cin >> u >> v;
dij.add(u, v, i);
dij.add(v, u, i);
}
}
int k;
cin >> k;
for (int i = 0; i < k; i ++) {
int x;
cin >> x;
pos[x].emplace_back(i);
}
dij.dijkstra(1);
cout << dij.dis[n] << '\n';
return 0;
}
K - 會贏的,對嗎?
HDU - 7216
思路
先說結論:若 \((a-1)\otimes(b-1)\otimes (c-1)\ne 0\) 則先手必勝(\(\otimes\) 代表異或)。
假設 \(a\le b \le c\),那麼有 \(a+b\ge c\),則有 \((a-1)+(b-1)\ge (c-1)\)。
令 \(x = (a-1)\otimes (b-1)\otimes (c-1):\)
當 \(x=0:\)
-
\(a=1\),那麼此時有 \((b-1)\otimes (c-1)=0\to a<b=c\),\(a\) 已經不能再減小了,此時無論減少 \(b\) 還是 \(c\) 都無發構成三角形。
-
\(a>1\),假設將 \(a\to a'\),此時有 \((a'-1)\ne (a-1)\to (a'-1)\otimes (b-1)\otimes (c-1)\ne 0\),即轉變到 \(x\ne 0\) 情況。
當 $x\ne 0: $
- 以下三個不等式必有一個成立 \((a-1)\otimes x < (a-1),(b-1)\otimes x < (b-1),(c-1)\otimes x < (c-1).\)
- 因為 \(x\) 中的 \(1\) 是 \((a-1),(b-1),(c-1)\) 由三者異或得到的,也就是一定是奇數次的 \(1\) 的個數,那麼不妨假設最高位的 \(1\) 是由 \((a-1)\) 提供的,(另外兩個即便也有 \(1\),也會因為異或偶數次而被抵消 ),那麼我們 \(x\) 的這一位上也一定有 \(1\),讓 \(x\otimes (a-1)\),就可以抵消最高位的 \(1\) ,使得其小於 \((a-1)\)。所以一定會存在由 \(x\ne 0\to x=0\) 的情況,也就是由必勝態轉向必敗態。
在兩人都採取最優策略的情況下,那麼先手就可以決定勝敗。
程式碼
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
void solve() {
int a, b, c;
cin >> a >> b >> c;
cout << ((a - 1) ^ (b - 1) ^ (c - 1) ? "Win" : "Lose") << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
solve();
}
return 0;
}
L - 還會贏的,對嗎?
CodeForces - 148D
思路
機率dp。
設 \(dp_{i,j}\) 為 \(i\) 只白老鼠,\(j\) 只黑老鼠中獲勝的機率。
顯然,當 \(j=0\) 時,有 \(dp_{i,0}=1\),當 \(j=1\) 時,有 \(dp_{i,1}=\frac{i}{i+1}\).
接下來分類討論:
- 先手抓到白老鼠,有 \(dp_{i,j}=\frac{i}{i+j}.\)
- 先手抓到黑老鼠,後手抓到白老鼠,有 \(dp_{i,j}=0.\)
- 先手抓到黑老鼠,嗎後手抓到黑老鼠,跑了一隻白老鼠,有 \(dp_{i,j}=\frac{j}{i+j}\times \frac{j-1}{i+j-1}\times \frac{i}{i+j-2}\times dp_{i-1,j-2}.\)
- 先手抓到黑老鼠,後手抓到黑老鼠,跑了一隻黑老鼠,有 \(dp_{i,j}=\frac{j}{i+j}\times \frac{j-1}{i+j-1}\times \frac{j-2}{i+j-2}\times dp_{i,j-3}.\)
程式碼
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int w, b;
cin >> w >> b;
vector dp(w + 10, vector<double>(b + 10));
for (int i = 1; i <= w; i ++) {
dp[i][0] = 1.0;
dp[i][1] = 1.0 * i / (i + 1);
}
for (int i = 1; i <= w; i ++) {
for (int j = 2; j <= b; j ++) {
dp[i][j] += 1.0 * i / (i + j);
dp[i][j] += 1.0 * j / (i + j) * (j - 1) / (i + j - 1) * i / (i + j - 2) * dp[i - 1][j - 2];
if (j > 2)
dp[i][j] += 1.0 * j / (i + j) * (j - 1) / (i + j - 1) * (j - 2) / (i + j - 2) * dp[i][j - 3];
}
}
cout << fixed << setprecision(10) << dp[w][b] << '\n';
return 0;
}