CSUACM2024新生赛 - 第1场 题解
写在前面
比赛地址:https://www.luogu.com.cn/contest/210420。
鉴于出题人水平大部分是原。
A 英雄联盟世界赛 签到,模拟
首先要读懂题意,在要晋级和淘汰的关键场次中,也就是队伍有两场胜利或者两次失败,那么则需要通过三局两胜制来获得该场比赛的胜利,对于非关键场次,那么只需要一场决胜负,所以直接贪心模拟就可以了
#include<bits/stdc++.h>
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr); std::cout.tie(nullptr);
int x, y;
std::cin >> x >> y;
int ans[3][3] = { {4, 4, 6}, {3, 3, 4}, {2, 2, 2} };
std::cout << ans[x][y] << "\n";
return 0;
}
B Cut! Increasing! 结论
首先考虑,连续的 \(0/1\)是一定在一块的,这样我们便可以将 \(s\) 变成 \(010101\dots\) 或者 \(10101\dots\)
再考虑一下,一个漂亮的字符串是这样的形式的 $(00\dots11\dots) $,所以如果在原 \(s\) 中有 \(01\) 形式,那么我们便可以将这整个切出来,剩下的 \(0/1\) 切块
我们将 \(s\) 缩起来后,变成 \(s^{'}\)。 如果 \(s^{'}\)中有 \(01\) 子串,那么答案就是 \(|s^{'}| - 1\) ,否则为 $|s^{'}| $。
#include<bits/stdc++.h>
void solve() {
std::string s;
std::cin >> s;
std::vector<int> a;
for (int i = 0; i < s.size() - 1; i++) {
if (s[i] != s[i + 1]) a.push_back(s[i] - '0');
}
a.push_back(s.back() - '0');
bool flag = false;
for (int i = 0; i + 1 < a.size(); i++) {
if (!a[i] && a[i + 1]) flag = true;
}
std::cout << a.size() - flag << "\n";
}
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr); std::cout.tie(nullptr);
int t;
std::cin >> t;
while (t--) {
solve();
}
return 0;
}
C 小予老师请客吃饭 概率期望
样例分析
当吃一串能吃饱,而将要吃第一串时,每一串都有肉,则 \(100\%\) 能取到肉,故只用 \(1\) 次就能吃饱,答案为 \(1\)。
当吃零串能吃饱,则一串都不用取,故答案为 \(0\)。
当吃两串能吃饱,则第一串一定能吃到肉;取第二串时,有一串是空的,有可能要多取一次、两次、……、甚至无穷次,有:$2\cdot \frac{1}{2}+3\cdot \frac{1}{2} \frac{1}{2}+4\cdot \frac{1}{2}\frac{1}{2} \frac{1}{2}+\dots $。根据无穷级数相关理论(后续会给出推导),答案为 \(3\)。
方法一
若直接利用数学期望的公式求解,想一步登天,难度较大。于是,我们的思路是,设当前已经吃了 \(i\) 串,要吃第 \(i+1\) 串,求本轮取串需要多少次,然后将每轮取串次数期望求和,得到总期望。
设已经吃了 \(i\) 串,要取 \(X\) 次才能吃到吃第 \(i+1\) 串,则当前取一次取到肉串的概率是:
有:
根据无穷级数:
又 \(kq^{k-1}=(q^k)^{'}\),则:
于是:
故总期望为:
方法二
设 \(E_i\) 表示已经吃了 \(i\) 串,吃饱还需要取串次数的数学期望。则有:
化简为:
则:
详见本视频: https://www.bilibili.com/video/BV1Ar19YyERv。于是答案为:
代码
#include<bits/stdc++.h>
#define LL long long
#define LD long double
using namespace std;
const int N = 1e6+6;
LL n,m;
LD pr[N];
void work(){
cin>>n>>m;
LD res=n*(pr[n]-pr[n-m]);
printf("%.5Lf\n",res);
}
void pre(){
for(LL i=1;i<=1e6;++i){
LD ad=1.0/(LD)i;
pr[i]=pr[i-1]+ad;
}
}
int main(){
LL _=1;
cin>>_;
pre();
while(_--){
work();
}
}
D AtForces 大陆 二分答案,贪心
首先考虑到,先不杀史莱姆更优。
然后问题就转化为了原来最少有多少个史莱姆时,经过繁衍,到最后有至少 \(t\) 个史莱姆的问题
如果直接模拟去做,显然会超时。
我们这样考虑,如果能进行繁衍,那么可以繁衍的史莱姆数量是有个下界 \(m\) 的。然后每次是可以产生 \(k\) 个可以繁衍的史莱姆,就相当于少了 \(m - k\) 个可以繁衍的史莱姆,那么假设最开始有 $x (x \geq m) $ 个史莱姆,那么可以繁衍的次数就是 $ \lfloor \frac{x - m}{m - k} \rfloor + 1$ 次,所以最后的总史莱姆数就是 $ x + (\lfloor \frac{x - m}{m - k} \rfloor + 1) * k $
二分找到满足 $ x + (\lfloor \frac{x - m}{m - k} \rfloor + 1) * k \geq h$ 的最小的 \(x\) 即可
当然,这样的写法是需要特判的,如果 \(m = k\) ,那么取 \(min\{m, ~h\}\) ,如果 $h \leq m $,取 \(h\) 。
#include<bits/stdc++.h>
#define int long long
void solve() {
int m, k, h;
std::cin >> m >> k >> h;
if (m == k) {
std::cout << std::min(m, h) << "\n";
return;
}
int l = -1, r = h + 1, mid;
auto check = [&](int x) {
int t = (x - m) / (m - k) + 1;
if (x < m) t = 0;
if (k * t + x >= h) return true;
else return false;
};
while (l + 1 != r) {
mid = l + r >> 1;
if (check(mid)) r = mid;
else l = mid;
}
std::cout << r << "\n";
}
signed main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr); std::cout.tie(nullptr);
int t;
std::cin >> t;
while (t--) {
solve();
}
return 0;
}
E 立青连结 STL,set 启发式合并
multiset 使用入门题。
对于每一个可以相互到达的城市组成的连通块内,都使用两个 multiset 维护所有城市的编号以及权值,同时维护每个城市位于的连通块的编号。
对于操作 1,相当于将两个连通块 multiset 进行合并。如果直接枚举某一方的 multiset,并大力插入到另一集合中,显然复杂度是不对的,很容易被不断地向小 multiset 中插入较大的 multiset 的数据卡成 \(O(n^2\log n)\) 级别。但仅需要一个小优化,钦定每次枚举 size 较小的 multiset 中的元素,并向 size 较大的 multiset 中插入即可。这个技巧叫做启发式合并,其总时间复杂度为 \(O(n\log^2 n)\) 级别。
为什么总时间复杂度为 \(O(n\log^2 n)\)?因为钦定了每次将小的 set 合并到大的 set 中,则每次合并后的 set 大小一定不小于合并前较小的 set 的 size 的两倍。因此对于每一个 set 中的元素,其会在合并中被枚举到的次数一定不大于 \(O(\log_2 n)\) 次。
正确性显然,若其被枚举到了 \(O(\log_2 n)\) 次,考虑到每次 size 都会至少乘 2,则其所在 set 的 size 一定为 \(O(n)\) 级别。则可以保证上述枚举至多进行 \(O(n\log n)\) 次,再乘上 set 的复杂度,总时间复杂度为 \(O(n\log^2 n)\) 级别。
对于操作 2,相当于在维护权值的 multiset 修改一个权值,仅需删原权值,再新增一个权值即可。
对于操作 3,直接查询 multiset 的 size 即可。
对于操作 4,直接查询维护编号的 multiset 的最小值即可。
对于操作 5,查询维护点权值的 multiset 内大于 \(a_x\) 的最小的权值,使用 lower_bound
即可实现。
注意可能存在相同的点权值,需要使用 multiset,并且 erase 时需要特别注意写法,不能仅传入需要删除的权值,否则会把所有相同权值的数全删除。正确的写法是先进行 find 找到一个指向对应权值的迭代器,然后传入该迭代器进行删除。示例:s.erase(s.find(v));
。
总时间复杂度 \(O(n\log^2 n)\) 级别。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 1e5 + 10;
//=============================================================
int n, m;
int a[kN], bel[kN];
std::set<int> s1[kN];
std::multiset<int> s2[kN];
//=============================================================
void build(int x_, int y_) {
if (bel[x_] == bel[y_]) return ;
int bx = bel[x_], by = bel[y_];
if (s1[bx].size() > s1[by].size()) std::swap(bx, by); //启发式合并
for (auto node: s1[bx]) {
s1[by].insert(node), bel[node] = by;
}
for (auto node: s2[bx]) s2[by].insert(node);
s1[bx].clear(), s2[bx].clear();
}
void modify(int x_, int y_) {
s2[bel[x_]].erase(s2[bel[x_]].find(a[x_]));
a[x_] = y_;
s2[bel[x_]].insert(y_);
}
//=============================================================
int main() {
//freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
std::cin >> n >> m;
for (int i = 1; i <= n; ++ i) {
std::cin >> a[i];
bel[i] = i;
s1[i].insert(i), s2[i].insert(a[i]);
}
while (m --) {
std::string opt; std::cin >> opt;
if (opt == "build") {
int x, y; std::cin >> x >> y;
build(x, y);
} else if (opt == "modify") {
int x, y; std::cin >> x >> y;
modify(x, y);
} else if (opt == "size") {
int x; std::cin >> x;
std::cout << s1[bel[x]].size() << "\n";
} else if (opt == "query1") {
int x; std::cin >> x;
std::cout << *(s1[bel[x]].begin()) << "\n";
} else {
int x; std::cin >> x;
auto p = s2[bel[x]].lower_bound(a[x] + 1);
if (p == s2[bel[x]].end()) std::cout << -1 << "\n";
else std::cout << *p << "\n";
}
}
return 0;
}
F hcgg最讨厌的数学题 线性 DP
首先问题可以等价于在数列中选取一些数,这些数可以组成等差数列的方案数。
初看一眼不会写,没关系,看一下数据范围,\(n \leq 10 ^ 3, v \leq 2 * 10 ^ 4\) 。一看似乎可以 \(n ^ 2\) 大力去写。
尝试一下,我们可以考虑选择两项作为等差数列中相邻的两项,那么这个等差数列的样子就确定了。
我们设出该方程 \(f_{i, d}\) 作为以 \(a_i\) 为结尾,公差为 \(d\) 的方案数(这其实认为这个等差数列有两个以上的数) ,那么,$f_{i, d} $ 就可以这样转移: \(f_{i, d} ~ +=~ f_{j, d} + 1\) ,其中 \(d = a_i - a_j\) ,($ + 1$ 是因为可以在第 \(i\) 个数之前只选择第 \(j\) 个数)。对答案贡献就是 \(f_{j, d} + 1\)
所以直接 \(n^2\) 转移方程即可。
在实现上,由于可能 \(a_i - a_j < 0\) ,我们可以对公差加上一个 \(v\) ,使得 \(d + v \geq 0\) 。
#include<bits/stdc++.h>
using ll = long long;
const int mod = 998244353;
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr); std::cout.tie(nullptr);
int n;
std::cin >> n;
std::vector<std::vector<int>> f(n + 1, std::vector<int>(4e4 + 1, 0));
std::vector<int> a(n + 2);
for (int i = 1; i <= n; i++) {
std::cin >> a[i];
}
ll ans = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j < i; j++) {
int d = a[i] - a[j] + 2e4;
f[i][d] = (f[i][d] + f[j][d] + 1) % mod;
ans = (ans + f[j][d] + 1) % mod;
}
}
ans = (ans + n) % mod;
std::cout << ans << "\n";
return 0;
}
G 画壁 二进制,结论
原:CF1415D
为使得该数列 不单调不降,等价于经过若干次操作后,存在某个位置大于后一个位置。称该位置 \(p\) 为断点。显然被操作的部分,一定仅有以断点 \(p\) 为右端点的的一段区间 \([L, p]\),加上以 \(p+1\) 为左端点的一段区间 \([p+1, R]\)。\([L, p]\) 的操作结果构成较大的数,\([p+1, R]\) 的操作结果构成较小的数。该结论正确性显然。为了保证代价最小,断点不可能存在两个及以上。
为使断点位置满足条件,需要对它前后分别操作。考虑暴力枚举断点以及左右两区间的长度求最小代价,时间复杂度 \(O(n^3)\) 级别,无法通过本题。
再观察题目的特殊性质,从位运算的角度考虑,可以发现:若有三个连续的数的最高位相同,则可将后面两个数异或起来消去最高位,使得第一个数大于后面的数,此时仅需操作 1 次即可。又数列单调不降,且 \(a_i\le 10^9\),则最长的、使得三个连续的数最高位不同的数列长度不大于 \(2\times 30\)。
则加上特判 \(n>60\) 时直接输出 1,即可通过本题。
//知识点:结论
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kN = 60 + 10;
//=============================================================
int n, ans = kN, a[kN];
//=============================================================
inline int read() {
int f = 1, w = 0; char ch = getchar();
for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir, int sec) {
if (sec > fir) fir = sec;
}
void Chkmin(int &fir, int sec) {
if (sec < fir) fir = sec;
}
//=============================================================
int main() {
n = read();
if (n > 60) {
printf("1\n");
return 0;
}
for (int i = 1; i <= n; ++ i) a[i] = a[i - 1] ^ read();
for (int l = 1; l < n - 1; ++ l) {
for (int r = l; r < n; ++ r) {
for (int k = r + 1; k <= n; ++ k) {
if ((a[r] ^ a[l - 1]) > (a[k] ^ a[r])) {
Chkmin(ans, (r - l) + (k - r - 1));
}
}
}
}
printf("%d\n", (ans == kN) ? -1 : ans);
return 0;
}
写在最后
败犬女主真好看,这老八真倪玛是个后面忘了。