2024 暑假友谊赛 5
2024 暑假友谊赛 5
A - 喷泉
思路
方案只有三种,一种是都用金币,一种是都用钻石,还有一种是一座金币一座钻石。
可以先枚举采用金币的,一边枚举的过程中,用线段树维护剩下的金币数可以建造的温泉美观度最大值,注意要建造两座才可以算进答案中。
枚举完金币的可以求出金币数能建造的最大美观度,然后按照枚举金币的做法枚举钻石,这里可以把第三种方案一起更新,把剩下的钻石能买的最大美观度和前面求出的所有金币能买的最大美观求个最大值即可。
代码
#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 - 懒得起名了,就这样吧
思路
将 \(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 - 怎么你不服气?
思路
注意到要满足字符串 \(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 - 她真的不一样
思路
直接按顺序枚举离 \(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 - 数组分割
思路
题目说明不存在相同元素,即按照非递减顺序排序后其实就是一个递增数组,而这样的递增数组一定是唯一的,所以我们可以将原数组离散化后找连续的单调递增区间即可。
代码
#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 - 穿越火线
思路
由题意可知,需要找到第一个 \(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 - 船型假发
思路
模拟样例可以发现,奇数位的相加进位只会加到奇数位,偶数位同理,那么不妨将奇数位和偶数位单独拿出来,将 \(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 - 劫!我要打劫!啊啊啊啊啊啊啊啊
思路
要求选择一些物品满足价值达到至少为 \(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 - 等差数列
思路
显然可以先排序。
对于 \(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 - 超时空旅行
思路
对于每个点,肯定是越早走到它越好,所以在存边的时候可以把这个边集的序号存下来,然后对于每个时刻开放的边集,以边集为索引把该时刻存下来,跑 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 - 会赢的,对吗?
思路
先说结论:若 \((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 - 还会赢的,对吗?
思路
概率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;
}