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\) 串,则当前取一次取到肉串的概率是:

\[p=\frac{n-i}{n},\ q=1-p \]

有:

\[E(X)=p+2pq+3pq^2+4pq^3+\dots =p\sum_{k=1}^{\infin}{kq^{k-1}} \]

根据无穷级数:

\[\sum_{k=0}^\infin{q^k}=\lim_{n\rightarrow\infin}\frac{1-q^n}{1-q}=\frac{1}{1-q} \]

\(kq^{k-1}=(q^k)^{'}\),则:

\[\sum_{k=1}^{\infin}{kq^{k-1}}=\frac{1}{(1-q)^2}-0=\frac{1}{p^2} \]

于是:

\[E(X)=p\cdot\frac{1}{p^2}=\frac{1}{p}=\frac{n}{n-i} \]

故总期望为:

\[E=n\sum_{i=1}^m\frac{1}{m-i} \]

方法二

\(E_i\) 表示已经吃了 \(i\) 串,吃饱还需要取串次数的数学期望。则有:

\[E_i=\frac{C_i^1}{C_n^1}E_i+(1-\frac{C_i^1}{C_n^1})E_{i+1}+1 \]

化简为:

\[E_i\begin{cases} E_{i+1}+\frac{n-i}{n} &(i\not= n)\\ 0 &(i=n) \end{cases}\]

则:

\[E_0=n(1+\frac{1}{2}+\frac{1}{3}+\dots+\frac{1}{n}) \]

详见本视频: https://www.bilibili.com/video/BV1Ar19YyERv。于是答案为:

\[E=E_0-E_m=n\sum_{i=1}^m\frac{1}{m-i} \]

代码

#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;
}

写在最后

败犬女主真好看,这老八真倪玛是个后面忘了。

posted @ 2024-11-01 09:03  Luckyblock  阅读(184)  评论(0编辑  收藏  举报