1. 2024年昆明邀请赛题解(一)解析
作为一名参加过多次ICPC竞赛的选手,我深知比赛题解对于选手们的重要性。本文将详细解析2024年昆明邀请赛第一部分的五道题目,包括B题"金牌"、G题"乐观向上"、I题"左移2"、A题"两星级竞赛"和E题"学而时习之"。每道题我都会从题意理解、解题思路、代码实现和注意事项四个方面进行深入讲解。
2. B. 金牌题解
2.1 题目理解与分析
这道题目描述了一个奖牌分配的场景:有n场比赛,每场比赛有a_i支队伍参加。比赛规则是每k支队伍颁发一个奖牌,不足k支的不颁发。现在还有m支队伍没有分配到具体比赛,需要将它们分配到这些比赛中,使得最终颁发的奖牌总数最大。
关键点在于理解如何利用剩余的m支队伍来最大化奖牌数。我们需要考虑两点:
- 当前每场比赛已经能颁发的奖牌数(a_i/k)
- 通过分配剩余队伍,能否让更多比赛达到颁发奖牌的标准
2.2 解题思路与算法设计
采用贪心算法是最优解。具体步骤如下:
- 首先计算每场比赛当前的奖牌数(a_i/k)和剩余队伍数(a_i%k)
- 将剩余队伍数按从大到小排序
- 优先将m支队伍分配给剩余队伍数较多的比赛,使其达到k的倍数
- 如果分配完所有比赛后还有剩余队伍,则将这些队伍集中在一场比赛中计算奖牌数
这种贪心策略能确保我们用最少的队伍产生最多的额外奖牌。例如,如果一个比赛已经有k-1支剩余队伍,我们只需要分配1支就能多获得一个奖牌,这比分配给剩余队伍少的比赛更高效。
2.3 代码实现与优化
cpp复制#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 105;
int a[N];
int main() {
int T;
cin >> T;
while (T--){
int n, k;
cin >> n >> k;
ll ans = 0;
for (int i = 1; i <= n; i++) {
int x;
cin >> x;
ans += x / k;
a[i] = x % k;
}
int m;
cin >> m;
sort(a + 1, a + 1 + n, greater<int>());
for (int i = 1; i <= n; i++){
if (m + a[i] < k) break;
m -= k - a[i];
ans++;
}
ans += m / k;
cout << ans << endl;
}
return 0;
}
2.4 注意事项与边界条件
- 数据范围问题:结果可能超过int范围,必须使用long long
- 当k=1时的特殊情况:此时每支队伍都能获得奖牌,直接输出总队伍数
- 当m=0时的处理:只需计算当前奖牌数
- 时间复杂度:O(nlogn)主要来自排序,对于n≤100的数据范围完全足够
3. G. 乐观向上题解
3.1 题目理解与性质分析
题目要求构造一个0到n-1的排列,使得任意前缀的异或和都不为0。如果无法构造则输出"impossible"。
通过分析可以发现:
- 当n=1时,唯一排列[0]的前缀异或就是0,必然无解
- 当n是4的倍数时,所有数异或和为0,最后一个前缀就是整个排列,必然无解
- 其他情况下可以构造出符合条件的排列
3.2 构造方法与证明
有效的构造策略是:
- 预处理交换0和1的位置
- 对于所有4的倍数i,交换i和i-1的位置
这样构造的原因在于:
- 交换0和1避免了第一个元素就是0的情况
- 交换4的倍数与其前一个数打破了连续的异或模式,防止前缀异或为0
3.3 代码实现
cpp复制#include <iostream>
using namespace std;
const int N = 1e6 + 5;
int a[N];
int main() {
for (int i = 1; i <= 1e6; i++) a[i] = i;
a[0] = 1, a[1] = 0;
for (int i = 1; i <= 1e6; i++) if (i % 4 == 0) swap(a[i], a[i - 1]);
int T;
cin >> T;
while (T--){
int n;
cin >> n;
if (n == 1 || n % 4 == 0) {
puts("impossible");
continue;
}
for (int i = 0; i < n; i++) cout << a[i] << " ";
cout << endl;
}
return 0;
}
3.4 优化与扩展
- 预处理可以节省每次查询的时间
- 可以进一步研究其他可能的排列方式
- 对于大n值,可以优化预处理的空间
4. I. 左移2题解
4.1 题目理解与转化
题目给定一个字符串,可以通过旋转操作(左移d个字符)来改变字符串,然后需要通过最少次数的字符修改,使得字符串中没有两个相邻字符相同。
关键点在于:
- 旋转操作实际上是把字符串变成以任意位置开头的循环形式
- 我们需要找到最优的旋转位置,使得所需的修改次数最少
4.2 解题思路与算法
- 首先处理特殊情况:所有字符相同,直接返回n/2
- 将字符串复制一份接在后面,破环成链
- 找到首尾不同的旋转位置
- 计算当前字符串的连续相同字符段
- 对于每个偶数长度的连续段,可以节省一次修改
4.3 代码实现
cpp复制#include <iostream>
using namespace std;
int main() {
int T;
cin >> T;
while (T--){
string s;
cin >> s;
int n = s.size();
bool flag = true;
for (int i = 0; i < n; i++) if (s[i] != s[0]) {
flag = false;
break;
}
if (flag) {
cout << n / 2 << endl;
continue;
}
s = s + s;
int i = 0;
while (s[i] == s[i + n - 1]) i++;
int ans = 0;
bool foo = false;
int ed = i + n;
while (i < ed){
int j = 1;
while (s[i] == s[i + j]) j++;
if (j % 2 == 0) foo = true;
i += j;
ans += j / 2;
}
if (foo) ans--;
cout << ans << endl;
}
return 0;
}
4.4 复杂度分析与优化
- 时间复杂度:O(n)处理字符串
- 空间复杂度:O(n)用于存储扩展后的字符串
- 可以优化为不实际扩展字符串,而是使用模运算处理循环
5. A. 两星级竞赛题解
5.1 题目理解与数据结构设计
题目描述了一系列竞赛,每个竞赛有m个属性值和星级。某些属性值缺失(用-1表示),需要用不超过k的数字填充,要求满足星级低的竞赛属性值之和严格小于星级高的。
需要设计合适的数据结构来存储和处理这些竞赛信息。
5.2 解题算法与步骤
- 按星级排序竞赛
- 计算每个竞赛已知属性值和与缺失属性数量
- 从低星级到高星级处理:
- 计算需要比前一组大的最小和
- 检查当前竞赛能否通过填充达到要求
- 更新最大和供下一组使用
- 最后按原始顺序输出结果
5.3 代码实现
cpp复制#include <iostream>
#include <algorithm>
#include <vector>
#define endl "\n"
using namespace std;
typedef long long ll;
typedef vector<ll> vl;
const ll N = 4e5 + 10;
struct Cts {
ll s, id;
vl v;
ll su, ne, diff;
} cs[N];
bool cmp1(Cts &c1, Cts &c2) {
return c1.s < c2.s;
}
bool cmp2(Cts &c1, Cts &c2){
return c1.id < c2.id;
}
int main() {
int T;
cin >> T;
while (T--) {
ll n, m, k;
cin >> n >> m >> k;
for (int i = 1; i <= n; i++) {
cs[i].s = cs[i].id = 0;
cs[i].v.clear();
cs[i].su = cs[i].ne = cs[i].diff = 0;
}
for (int i = 1; i <= n; i++) {
cin >> cs[i].s;
cs[i].id = i;
cs[i].v.resize(m + 1);
cs[i].su = cs[i].ne = 0, cs[i].diff = 0;
for (ll j = 1; j <= m; j++) {
cin >> cs[i].v[j];
if (cs[i].v[j] != -1) cs[i].su += cs[i].v[j];
else cs[i].ne++;
}
}
sort(cs + 1, cs + n + 1, cmp1);
bool flag = true;
ll lst = 0, mx = 0;
for (int i = 1; i <= n; i++) {
ll need = max(lst - cs[i].su, 0ll);
if (need > cs[i].ne * k) {
flag = false;
break;
}
if (cs[i].su < lst) cs[i].diff = lst - cs[i].su;
else mx = max(mx, cs[i].su);
mx = max(mx, cs[i].su + min(cs[i].diff, cs[i].ne * k));
if (i == n || cs[i].s != cs[i + 1].s)
lst = mx + 1, mx = 0;
}
if (!flag) cout << "No" << endl;
else {
sort(cs + 1, cs + 1 + n, cmp2);
cout << "Yes" << endl;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (cs[i].v[j] != -1) cout << cs[i].v[j] << " ";
else {
cout << min(k, cs[i].diff) << " ";
cs[i].diff -= min(k, cs[i].diff);
}
}
cout << endl;
}
}
}
return 0;
}
5.4 复杂度分析与优化
- 时间复杂度:O(nm + nlogn)主要来自排序和遍历
- 空间复杂度:O(nm)存储竞赛数据
- 可以优化属性值的存储方式,使用更紧凑的数据结构
6. E. 学而时习之题解
6.1 题目理解与数学模型
给定n个正整数的序列和整数k,可以选择一个区间[l,r]将其中的数都加k,求操作后序列的最大公约数。
关键在于理解区间加k对序列GCD的影响,以及如何高效计算可能的最大GCD。
6.2 解题算法与数学原理
利用GCD的性质:
- gcd(a,b) = gcd(b, a-b)
- gcd(a+k, b+k) = gcd(a-b, b+k)
算法步骤:
- 预处理前缀和后缀GCD数组
- 枚举可能的分界点l
- 对于每个l,计算包含它的区间对GCD的影响
- 维护最大的GCD值
6.3 代码实现
cpp复制#include <iostream>
using namespace std;
typedef long long ll;
const int N = 3e5 + 5;
ll gcd(ll a, ll b) {
return b == 0 ? a : gcd(b, a % b);
}
ll a[N], pre[N], post[N];
int main() {
int T;
cin >> T;
while (T--) {
int n; ll k;
cin >> n >> k;
for (int i = 1; i <= n; i++) cin >> a[i];
pre[0] = 0, post[n + 1] = 0;
for (int i = 1; i <= n; i++) pre[i] = gcd(pre[i - 1], a[i]);
for (int i = n; i; i--) post[i] = gcd(post[i + 1], a[i]);
ll ans = pre[n];
for (int l = 1; l <= n; l++) {
if (pre[l - 1] != pre[l]){
ll g = 0;
for (int r = l; r <= n; r++) {
g = gcd(g, a[r] + k);
ll res = gcd(pre[l - 1], post[r + 1]);
res = gcd(res, g);
ans = max(ans, res);
}
}
}
cout << ans << endl;
}
return 0;
}
6.4 复杂度分析与数学证明
- 时间复杂度:O(nlogA)其中A是数组元素大小
- 空间复杂度:O(n)用于存储前缀后缀数组
- 正确性基于GCD的数学性质和单调性
7. 总结与竞赛建议
通过这五道题的解析,我们可以总结出一些ICPC竞赛的解题技巧:
- 贪心算法的应用场景识别
- 数学性质在解题中的关键作用
- 预处理和空间换时间的优化策略
- 边界条件的全面考虑
- 数据结构的选择与设计
在实际比赛中,建议选手:
- 仔细阅读题目,确保完全理解题意
- 先从简单的情况入手,寻找规律
- 合理分配时间,先解决有把握的题目
- 注意数据范围和特殊边界条件
- 编写清晰易读的代码,方便调试
这些题目虽然来自区域赛,但涉及的算法思想和解题技巧对提高编程竞赛能力非常有帮助。建议读者在理解这些题解后,尝试自己实现代码,并在类似的题目上练习应用这些技巧。