2024 CSU寒假集训新生赛题解
写在前面
整活整了个爽。
如果你发现题目背景里有怪话那么这题大概就是我搬的哈哈。
以下按个人向难度排序。
T429867 A 我是一个一个签到题
签到 600
位运算除左右移外,在二进制表示下不进位,两个数取 or 后不可能变得更小。
把所有数全或起来得到的数的 \(n\) 倍即为答案。
T429822 I 最简单的题
签到 800
可证明最大匹配数为 \(n\),输出 \(n\) 即可。
证明赛后讲了,因为挺麻烦的略,问问过了的大神吧!
T429677 E 清点人数
枚举 900
怎么那么多写数据结构的、、、
保证询问位置是递增的,于是仅需维护变量 \(p\) 表示当前询问的位置,与变量 \(s\) 表示当前询问的答案。
对于修改操作,直接单点修改数组的值即可。若修改的位置不大于 \(p\) 则在 \(s\) 上加减影响。
对于查询操作,不断自增 \(p\) 直至等于当前查询位置,将经过的位置的贡献统计到 \(s\) 中即可。
复杂度 \(O(n+m)\) 级别。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 5e5 + 10;
//=============================================================
int n, k, pos, a[kN];
LL s = 0;
//=============================================================
//=============================================================
int main() {
//freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
std::cin >> n >> k;
while (k --) {
std::string opt; std::cin >> opt;
if (opt[0] == 'A') {
int m; std::cin >> m;
while (pos < m) ++ pos, s += a[pos];
std::cout << s << "\n";
} else if (opt[0] == 'B') {
int m, p; std::cin >> m >> p;
a[m] += p;
if (pos >= m) s += p;
} else {
int m, p; std::cin >> m >> p;
a[m] -= p;
if (pos >= m) s -= p;
}
}
return 0;
}
T429669 L 色彩入侵
CF1415B
枚举,贪心 1100
发现颜色种类数很少,暴力枚举最后数列的形态,然后枚举数列中的每个位置,有颜色不同的位置则贪心地以该位置为左端点修改即可。
//知识点:枚举,贪心
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kN = 1e5 + 10;
//=============================================================
int n, m, ans, 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() {
int t = read();
while (t --) {
n = read(), m = read();
ans = kN;
for (int i = 1; i <= n; ++ i) a[i] = read();
for (int i = 1; i <= 100; ++ i) {
int sum = 0;
for (int j = 1; j <= n; ++ j) {
if (a[j] != i) {
sum ++;
j = j + m -1;
}
}
Chkmin(ans, sum);
}
printf("%d\n", ans);
}
return 0;
}
T428499 B 物理学的终结
递推,枚举 1600
定义 \(f(l,r)\) 表示子串 \([l,r]\) 组成的十进制数。考虑枚举删除的子串的右端点 \(x\),则删去后可得到的所有十进制数的和为:
预处理出后缀表示的十进制数与 10 的幂,枚举 \(x\) 的时候维护前缀十进制数的和即可求解,时间复杂度 \(O(n)\) 级别。
//知识点:递推,枚举
/*
By:Luckyblock
*/
#include <cctype>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define LL long long
const int kN = 2e6 + 10;
const LL mod = 1e9 + 7;
//=============================================================
int n;
char s[kN];
LL pow10[kN], suf[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;
}
//=============================================================
int main() {
scanf("%s", s + 1);
n = strlen(s + 1);
pow10[0] = 1;
for (int i = 1; i <= n; ++ i) {
pow10[i] = pow10[i - 1] * 10ll % mod;;
}
for (int i = n; i >= 1; -- i) {
suf[i] = suf[i + 1] + (s[i] - '0') * pow10[n - i] % mod;
suf[i] %= mod;
}
LL sum = 0, val = 0, ans = 0;
for (int i = 1; i <= n; ++ i) {
ans += sum * pow10[n - i] % mod + i * suf[i + 1] % mod;
ans %= mod;
val = (10ll * val % mod + s[i] - '0') % mod;
sum = (sum + val) % mod;
}
printf("%lld\n", ans);
return 0;
}
T430968 C 很多正方形(正确版本)
CF1811D
构造,1600
江队在这里给大家拜年了!!!扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通扑通
我没写,略。见:https://www.luogu.com.cn/problem/solution/CF1811D。
T428496 D 爱如往昔
P3558 [POI2013] BAJ-Bytecomputer
DP 1800
发现一些结论:
- 显然应该从前往后操作。
- 若某位置为 \(1\),则经过若干次操作后它之后的位置都可以 \(\ge 1\)。
- 若某位置为 \(-1\),则经过若干次操作后它之后的位置都可以 \(\le -1\)。
- 由上述结论可知,若出现:\(0,\cdots, 0, -1,\cdots\) 的情况,则一定无解,否则一定有解。
- 显然最终答案一定是 \(-1\cdots, -1,0,\cdots,0,1\) 的形式,不可能出现 \(|a_i|\ge 2\) 的情况。 因为 \(-2\) 只能通过 \(-1-1\) 得到,此时已经单调不降了,变为 \(-2\) 一定不优。\(2\) 同理。
根据结论 4 判下无解,由结论 5,考虑 DP。
设 \(f_{i,-1/0/1}\) 表示当前操作到第 \(i\) 个数,第 \(i\) 个数变为 \(-1/0/1\) 时,令前 \(i\) 个数单调不降的最小操作次数。
初始化 \(f_{1,a_1} = 0\),转移时分类讨论:
答案即 \(\min(f_{n,-1},f_{n,0},f_{n,1})\)。
总时间复杂度 \(O(n)\)。
//知识点:线性 DP
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kN = 1e6 + 10;
//=============================================================
int n, ans, a[kN], f[kN][3];
//=============================================================
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();
int fir1 = 0, firm1 = 0;
for (int i = 1; i <= n; ++ i) {
a[i] = read();
if (!fir1 && a[i] == 1) fir1 = i;
if (!firm1 && a[i] == -1) firm1 = i;
}
if (1 < firm1 && firm1 < fir1) {
printf("-1\n");
return 0;
}
memset(f, 63, sizeof (f));
f[1][a[1] + 1] = 0;
for (int i = 2; i <= n; ++ i) {
f[i][0] = f[i - 1][0] + a[i] + 1;
if (a[i] == 0) {
f[i][1] = std::min(f[i - 1][0], f[i - 1][1]);
} else if (a[i] == 1) {
f[i][1] = f[i - 1][0] + 1;
}
if (a[i] != 1) {
f[i][2] = f[i - 1][2] + 1 - a[i];
} else {
Chkmin(f[i][2], f[i - 1][0]);
Chkmin(f[i][2], f[i - 1][1]);
Chkmin(f[i][2], f[i - 1][2]);
}
}
ans = f[n + 1][0];
Chkmin(ans, f[n][0]);
Chkmin(ans, f[n][1]);
Chkmin(ans, f[n][2]);
if (ans == f[n + 1][0]) {
printf("-1\n");
} else {
printf("%d\n", ans);
}
return 0;
}
T428337 K KFC 三人篮球赛
枚举 1800
CF1826D
感谢 xinhuo 同学查错,给你磕一个了呜呜呜
实际上超级好写的题。
一个显然的结论,对于最优的区间,其两个端点上一定是前三大的数,否则将区间缩短会更优。
问题变为最大化:
于是考虑枚举中间的值 \(a_i\),则最优的左端点上的贡献一定是 \(\max_{j<i} (a_j + j)\),最优的右端点的贡献一定是 \(\max_{i<j} (a_j - j)\)。对 \(a_j + j\) 与 \(a_j - j\) 分别维护前后缀最大值即可快速求得左右端点。
总时间复杂度 \(O(n)\) 级别。
有复杂度同阶的 DP 解法。
//枚举
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 1e5 + 10;
const int kInf = 1e9;
//=============================================================
int n, b[kN], b1[kN], b2[kN];
//=============================================================
void Solve() {
std::cin >> n;
b1[0] = b2[n + 1] = -kInf;
for (int i = 1; i <= n; ++ i) {
std::cin >> b[i];
b1[i] = b[i] + i, b2[i] = b[i] - i;
b1[i] = std::max(b1[i], b1[i - 1]);
}
for (int i = n; i; -- i) {
b2[i] = std::max(b2[i], b2[i + 1]);
}
int ans = 0;
for (int i = 2; i < n; ++ i) {
ans = std::max(ans, b1[i - 1] + b[i] + b2[i + 1]);
}
std::cout << ans << "\n";
}
//=============================================================
int main() {
//freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
int T; std::cin >> T;
while (T --) {
Solve();
}
return 0;
}
T429692 F 梦违科学世纪
数论 1800
显然 \(p \bmod q \not= 0\),输出 \(p\)。
当 \(p \bmod q = 0\) 时,想到让 \(p\) 除以某些 \(q\) 的质因子直到 \(p\) 不能被 \(q\) 整除。\(p\) 中比 \(q\) 大的质因子没有影响,于是仅考虑 \(q\) 的质因子,并且仅削除一种质因子仅可。
于是枚举 \(q\) 的每一种质因子令 \(p\) 除以该质因子,直到 \(p\) 中该质因子的次数比 \(q\) 中的次数小即为可行的 \(x\) 的取值,取最大值即为答案。
总复杂度 \(O\left(t\sqrt{q}\right)\) 级别。
//֪
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <cmath>
#define LL long long
//=============================================================
//=============================================================
inline LL read() {
LL 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() {
int T = read();
while (T --) {
LL p = read(), q = read(), orip = p, x = 1;
if (p < q) {
printf("%lld\n", p);
continue ;
}
if (p % q != 0) {
printf("%lld\n", p);
continue ;
}
for (LL i = 2, lim = q; i * i <= lim; ++ i) {
if (q % i == 0ll) {
int cntq = 0, cntp = 0;
while (q % i == 0ll) {
q /= i;
++ cntq;
}
while (p % i == 0ll) {
p /= i;
++ cntp;
}
if (cntp < cntq) continue ;
LL tmp = orip;
for (int lim = cntp - cntq + 1; lim; -- lim) {
tmp /= i;
}
if (tmp > x) x = tmp;
}
}
if (q != 1) {
int cntp = 0;
while (p % q == 0ll) {
p /= q;
++ cntp;
}
if (cntp < 1) continue ;
LL tmp = orip;
for (int lim = cntp - 1 + 1; lim; -- lim) {
tmp /= q;
}
if (tmp > x) x = tmp;
}
printf("%lld\n", x);
}
return 0;
}
T429675 G 画壁
CF1415D
结论,大范围结论小范围暴力 1900
有一些显然的性质。
- 使得该数列 不单调不降,即存在合并后的两个位置,使得前一个 大于 后一个,以下称它为断点。
- 被操作的位置一定是包含上述断点的 一段 区间。断点前的部分构成较大的数,断点后的部分构成较小的数。
正确性显然。为了保证代价最小,断点不可能存在两个或两个以上。为使断点位置满足条件,需要对它前后分别操作。
于是可以考虑暴力枚举断点以及左右的区间,求得最小代价,复杂度 \(O(n^3)\)。
再观察题目的特殊性质,从位运算的角度考虑,可以发现:
若有三个连续的数的最高位相同,则可将后面两个异或起来消去最高位,使得第一个数大于后面的数。仅需操作 1 次即可。
又数列单调不降,则最长的使得三个连续的数最高位不同的数列长度为 \(2\times 30\)。
特判 \(n>60\) 时输出 1 即可通过本题。
总时间复杂度 \(O(60^3)\)。
//知识点:结论
/*
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;
}
T428497 H 请不要抢走爱丽丝的工作!
[ABC181F] Silver Woods
二分答案,并查集 2000
考虑二分答案,检查某直径 \(d\) 的圆是否可以无伤通过。发现无法通过当且仅当存在一组钉子,使得这组钉子加上上下界可以构成AT力场把圆挡住。更加形式化地,直径 \(d\) 不合法当前仅当存在一组钉子:\(\{(x_1, y_1), (x_2, y_2), \dots, (x_k, y_k)\}(y_1\le y_2\le \dots\le y_k)\),满足:
考虑将 check
过程转换成图论问题。新建两个虚拟节点分别代表上下界,若某节点满足与上下界距离不大于 \(d\) 则连边;然后枚举所有点对,若距离不大于 \(d\) 则连边。若代表上下界的节点连通说明直径为 \(d\) 的圆不合法,否则合法。并查集维护即可。
总复杂度 \(O(n^2\alpha(n)\log v)\) 级别。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 100 + 10;
const double eps = 1e-9;
//=============================================================
int n, nodenum;
int fa[kN];
struct Node {
int x, y;
} node[kN << 2];
double ans;
//=============================================================
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;
}
int find(int x_) {
return x_ == fa[x_] ? x_ : fa[x_] = find(fa[x_]);
}
void merge(int x_, int y_) {
int fx = find(x_), fy = find(y_);
if (fx == fy) return ;
fa[fx] = fy;
}
double dis2(int i_, int j_) {
return (node[i_].x - node[j_].x) * (node[i_].x - node[j_].x) +
(node[i_].y - node[j_].y) * (node[i_].y - node[j_].y);
}
bool check(double d_) {
for (int i = 1; i <= nodenum; ++ i) fa[i] = i;
for (int i = 3; i <= nodenum; ++ i) {
if (node[i].y + d_ + eps >= 100.0) merge(1, i);
if (node[i].y - d_ - eps <= -100.0) merge(2, i);
for (int j = i + 1; j <= nodenum; ++ j) {
if (dis2(i, j) - eps <= d_ * d_) merge(i, j);
}
}
return find(1) != find(2);
}
//=============================================================
int main() {
//freopen("1.txt", "r", stdin);
n = read();
nodenum = 2;
for (int i = 1; i <= n; ++ i) {
int x_ = read(), y_ = read();
node[++ nodenum] = (Node) {x_, y_};
}
for (double l = 0, r = 200; r - l > eps; ) {
double mid = (l + r) / 2.0;
if (check(mid)) {
l = mid;
ans = mid;
} else {
r = mid;
}
}
printf("%lf", ans / 2.0);
return 0;
}
T428498 J 燕石博物志
建图,最短路 2200
算法一
我会暴力建图!
每个点向上下左右的点连权值为 1 的双向边。每个关键点向同行同列的点连权值为 1 的双向边。然后跑 Dijkstra。
然而点数边数是 \(O(n^2)\) 级别的,时间复杂度 \(O(n^2\log (n^2))\) 级别,太菜了。
算法二
考虑从起点到终点的最短路径。若不经过任何一个关键点,最短路即为两点曼哈顿距离,可以直接算出,否则可以把最短路看成:起点 \(\rightarrow\) 关键点 \(\rightarrow\) 终点。于是将关键点作为中继点,改变连边方式:
- 起点向关键点连边,权值为 \(\min(|sx-x|, |sy-y|)\)。
- 关键点与关键点之间连 双向 边,权值为 \(\min(|x_1-x_2|, |y_1-y_2|)\)。
- 关键点向终点连边,权值为 \(\min(|tx-x|, |ty-y|)\)。
再跑 Dijkstra,点数边数变为 \(O(m^2)\) 级别,时间复杂度 \(O(m^2 \log (m^2))\) 级别,还是菜。
算法三
为表达方便,以下钦定两关键点间的距离为 \(\min(|x_1-x_2|, |y_1-y_2|)\)。
考虑三个关键点之间的连边,如果出现下图情况:
显然 \(A\rightarrow C\) 的距离不小于 \(A\rightarrow B\) 与 \(B\rightarrow C\) 的距离之和。 因此可以不连 \(A\rightarrow C\) 的边,不会影响 \(A\rightarrow C\) 的最短路,可以删除这条边。
再考虑更一般的情况,如果有下图:
\(A\rightarrow C\) 的距离仍然不小于 \(A\rightarrow B\) 与 \(B\rightarrow C\) 的距离之和。因此可以不连 \(A\rightarrow C\) 的边,不会影响 \(A\rightarrow C\) 的最短路。 但注意到 \(A\rightarrow C\) 的边会对 \(A\rightarrow B\) 的最短路作出贡献,这条边不能删除。
于是得到一个对算法二的优化:先把关键点按 \(x\) 坐标排序,在排序后相邻两个点连 双向边。再把关键点按 \(y\) 坐标排序,在排序后相邻两点连 双向边。
跑出来的最短路与之前的相等,但点数边数仅为 \(O(m)\) 级别,时间复杂度 \(O(m\log m)\) 级别,可以通过。
注意空间大小。
//知识点:建图,最短路
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <queue>
#define pr std::pair
#define mp std::make_pair
#define LL long long
const int kM = 1e5 + 10;
const int kE = 6e5 + 10;
//=============================================================
struct Node {
int x, y, id;
} a[kM];
int n, m, sx, sy, tx, ty;
int e_num, head[kM], v[kE], w[kE], ne[kE];
LL dis[kM];
bool vis[kM];
//=============================================================
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_;
}
bool CMPx(Node fir_, Node sec_) {
return fir_.x < sec_.x;
}
bool CMPy(Node fir_, Node sec_) {
return fir_.y < sec_.y;
}
void AddEdge(int u_, int v_, int w_) {
v[++ e_num] = v_;
w[e_num] = w_;
ne[e_num] = head[u_];
head[u_] = e_num;
}
void Dijkstra(int s_) {
std::priority_queue <pr <LL, int> > q;
memset(dis, 63, sizeof (dis));
memset(vis, 0, sizeof (vis));
dis[s_] = 0;
q.push(mp(0, s_));
while (! q.empty()) {
int u_ = q.top().second;
q.pop();
if (vis[u_]) continue ;
vis[u_] = true;
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i], w_ = w[i];
if (dis[u_] + w_ < dis[v_]) {
dis[v_] = dis[u_] + w_;
q.push(mp(-dis[v_], v_));
}
}
}
}
//=============================================================
int main() {
n = read(), m = read();
sx = read(), sy = read();
tx = read(), ty = read();
AddEdge(0, m + 1, abs(tx - sx) + abs(ty - sy));
for (int i = 1; i <= m; ++ i) {
int x = read(), y = read();
a[i] = (Node) {x, y, i};
AddEdge(0, i, std::min(abs(sx - x), abs(sy - y)));
AddEdge(i, m + 1, abs(tx - x) + abs(ty - y));
}
std::sort(a + 1, a + m + 1, CMPx);
for (int i = 2; i <= m; ++ i) {
LL val = std::min(abs(a[i].x - a[i - 1].x), abs(a[i].y - a[i - 1].y));
AddEdge(a[i - 1].id, a[i].id, val);
AddEdge(a[i].id, a[i - 1].id, val);
}
std::sort(a + 1, a + m + 1, CMPy);
for (int i = 2; i <= m; ++ i) {
LL val = std::min(abs(a[i].x - a[i - 1].x), abs(a[i].y - a[i - 1].y));
AddEdge(a[i - 1].id, a[i].id, val);
AddEdge(a[i].id, a[i - 1].id, val);
}
Dijkstra(0);
printf("%lld\n", dis[m + 1]);
return 0;
}
写在最后
学到了什么:
- 不要一直正序开题。
- 前期卡题了请考虑换题。
- 记得看榜。
- 如果你现在没题可做想跟榜,而且某道题榜上过得人不多,跟榜不一定是最优的,有可能只是大神一眼把金牌题秒了。