- 题解
CWNU算协挑战赛 Round 1 题解
- @ 2026-9-27 21:30:07
A - 第 67 个整数问题
思路
无论江月诗选择什么 ,都有
因此答案对应的目标值不可能超过 。直接令 ,便有
恰好达到上界。
C++示例代码
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
using uint = unsigned int;
using ll = long long;
using ull = unsigned long long;
using lll = __int128;
using ulll = unsigned __int128;
const int N = 1e6 + 10;
void solve(){
int val;
cin >> val;
cout << val << endl;
}
int main(){
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr); std::cout.tie(nullptr);
int _ = 1;
cin >> _;
while (_--) solve();
return 0;
}
B - 甜品
思路
江月诗每走一次长步,只会给一个坐标增加 ,不会改变这个坐标的奇偶性。
起点为 ,两个坐标都是偶数。短步每次只给一个坐标增加 ,会改变这一个坐标的奇偶性;而短步最多只能使用一次。
因此,最终最多只能有一个奇数坐标。判断条件为
也就是:只有 和 都是奇数时输出 NO,其余情况输出 YES。
C++示例代码
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
using uint = unsigned int;
using ll = long long;
using ull = unsigned long long;
using lll = __int128;
using ulll = unsigned __int128;
const int N = 1e6 + 10;
void solve(){
int x, y;
cin >> x >> y;
cout << (x % 2 == 1 && y % 2 == 1 ? "NO" : "YES") << endl;
}
int main(){
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr); std::cout.tie(nullptr);
int _ = 1;
cin >> _;
while (_--) solve();
return 0;
}
C - 第 67 个“6–7”整数问题
思路
江月诗需要让恰好 个数取反,等价于选择一个数保留,其余全部取反。
设原数组总和为
若保留 ,新总和为
不会随着选择改变。因此,只需要让保留的 尽可能大。
最终答案为
读入时同时维护总和与最大值即可,不需要排序或保存数组。
C++示例代码
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
using uint = unsigned int;
using ll = long long;
using ull = unsigned long long;
using lll = __int128;
using ulll = unsigned __int128;
const int N = 1e6 + 10;
void solve(){
int sum = 0, mx = INT_MIN;
for(int i = 0; i < 7; i++){
int x;
cin >> x;
sum += x;
mx = max(mx, x);
}
cout << 2 * mx - sum << endl;
}
int main(){
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr); std::cout.tie(nullptr);
int _ = 1;
cin >> _;
while (_--) solve();
return 0;
}
D - 派对图案
思路
江月诗进行调整时,选择的子串可以是整个字符串。
删除整个字符串后,剩下空串;再将所有字符逐个插回任意位置,就可以得到这些字符的任意排列。这里的多次“插回字符”属于同一次操作,不是多次操作。
于是,问题转化为:这些左括号和右括号能否重新排列成合法括号序列?
操作不会改变两种括号的数量。设左括号数为 ,右括号数为 。答案为
统计左括号数量 cnt,直接判断 2 * cnt == n 即可。
C++示例代码
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
using uint = unsigned int;
using ll = long long;
using ull = unsigned long long;
using lll = __int128;
using ulll = unsigned __int128;
const int N = 1e6 + 10;
void solve(){
int n;
string str;
cin >> n >> str;
int cnt = 0;
for(char& it : str){
cnt += it == '(';
}
cout << (2 * cnt == n ? "YES" : "NO") << endl;
}
int main(){
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr); std::cout.tie(nullptr);
int _ = 1;
cin >> _;
while (_--) solve();
return 0;
}
E - 降雪
关键观察:只关心因子 和
因为 ,一个子数组的乘积能够被 整除,当且仅当乘积同时具有因子 和因子 。
因此,按整除性把元素分为四组:
| 分组 | 条件 | 示例 |
|---|---|---|
| 能被 整除 | ||
| 能被 整除,但不能被 整除 | ||
| 既不能被 整除,也不能被 整除 | ||
| 能被 整除,但不能被 整除 |
只要一个子数组含有 元素,其乘积一定能被 整除。不含 时,则需要同时出现 与 元素。
构造方案
江月诗只需将卡片按以下四组的顺序重新排列:
各组内部的顺序不影响结果,可以保留读入顺序。
直观上,把 集中放在一端,避免它们分散影响更多区间;把 与 分开放,中间用不会贡献因子 或 的 隔开。
下面通过计数上界证明这个构造确实最优,而不仅是直觉上合理。
证明:对“不满足条件”的子数组计数
设
表示长度为 的数组的非空连续子数组总数。
记 、、,以及 。定义
即乘积不能被 整除的子数组数量。最小化 等价于最大化 。
第一步:给任意排列的 建立上界。
一个计入 的子数组不可能含有 元素,因此其左右端点必须都来自其余 个位置。从这 个位置中选择左、右端点,按实际位置满足左端点不晚于右端点,总共只有 种选择。
这些端点选择不一定都有效:区间内部可能出现 ,也可能同时出现因子 、。因此, 只是候选数量的上界。
其中,只要两个端点分别选自 与 ,区间乘积就一定能被 整除。这样的端点组合恰好有 个:每选一个 位置和一个 位置,就唯一确定一个区间,无论哪个位置在前。
所以对任意排列,都有
从而
第二步:证明构造达到这个界。
在构造 中,不包含 的部分是一个长度为 的连续后缀。
这个后缀共有 个子数组。其中,乘积能被 整除的子数组,必须左端点位于 、右端点位于 ,恰好有 个。其他子数组全部计入 。
因此,此构造满足
恰好达到任意排列都不能超过的上界。所以它使 达到最小值,构造正确。
C++示例代码
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
using uint = unsigned int;
using ll = long long;
using ull = unsigned long long;
using lll = __int128;
using ulll = unsigned __int128;
const int N = 1e6 + 10;
void solve(){
int n;
cin >> n;
vector<vector<int>> a(4);
for(int i = 0; i < n; i++){
int x;
cin >> x;
if(x % 6 == 0) a[0].push_back(x);
else if(x % 2 == 0) a[1].push_back(x);
else if(x % 3 == 0) a[3].push_back(x);
else a[2].push_back(x);
}
int cnt = 0;
for(auto& it1 : a){
for(int it2 : it1){
cout << it2 << (++cnt == n ? '\n' : ' ');
}
}
}
int main(){
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr); std::cout.tie(nullptr);
int _ = 1;
cin >> _;
while (_--) solve();
return 0;
}
F - 第 67 个异或问题
关键观察:两个未被删除元素的异或值不会改变
设江月诗当前数组中两个元素的值为 。如果某次操作选择的值是 ,且这两个元素都没有被删除,那么它们的新值分别为 与 。
根据异或的结合律、交换律以及 ,有
$$(u\oplus x)\oplus(v\oplus x) =u\oplus v\oplus x\oplus x =u\oplus v.$$因此,只要两个元素都还在数组中,它们当前值的异或,就始终等于它们在原数组中的值的异或。
这里把元素看作有固定身份的对象:即使数组下标随删除发生变化,也能追踪到它原来的位置。
把整个操作过程转化为选择两个原数组位置
在最后一次操作之前,数组中恰好剩下两个元素。设它们原来位于位置 ,当前值分别为 。
最后一次操作无论删除哪个元素,最终留下的值都是
所以,任意合法操作序列的最终结果,都一定是原数组中两个不同位置的元素的异或值。
反过来,任意选择两个不同的原数组位置 ,先按任意顺序删除其余 个元素,最后再删除这两个元素中的一个,就可以得到 。当 时,直接进行最后一次操作即可。
因此,问题严格等价于求
这里要求位置不同,但不要求数值不同。
C++示例代码
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
using uint = unsigned int;
using ll = long long;
using ull = unsigned long long;
using lll = __int128;
using ulll = unsigned __int128;
const int N = 1e6 + 10;
void solve(){
int n;
cin >> n;
vector<int> a(n);
for(auto& it : a) cin >> it;
int ans = 0;
for(int i = 0; i < n; i++){
for(int j = i + 1; j < n; j++){
ans = max(ans, a[i] ^ a[j]);
}
}
cout << ans << endl;
}
int main(){
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr); std::cout.tie(nullptr);
int _ = 1;
cin >> _;
while (_--) solve();
return 0;
}
G - 回文 mex
关键观察:最优子数组一定包含
江月诗可以只取任意一个 ,得到的单元素数组是回文,且其 。因此答案至少为 。
不包含 的子数组,其 ,不可能比上述方案更优。所以只需要考虑包含 的回文子数组。
题目又保证 恰好出现两次。设这两个 的下标为 ,其中 。下面统一使用从 开始的下标,与代码一致。
为什么只需要三个候选中心
情况一:回文子数组中只有一个 。
在回文中,除奇数长度回文的中心外,每个位置都对应一个值相同的镜像位置。若唯一的 不在中心,就必须有另一个 与它配对,产生矛盾。
因此,唯一的 必须是中心,候选中心只能是 或 。
情况二:回文子数组中包含两个 。
这两个 必须关于中心对称。否则,其中一个 还需要第三个 作为镜像,违背整个数组中 只有两次的条件。
所以中心必须为
由此,所有可能最优的回文子数组,其中心都属于
统一处理奇数和偶数长度回文
用一对初始指针 表示中心:当 时表示以某个元素为中心;当 时表示以两个相邻元素之间为中心。
三个候选的起始指针为
$$(p,p),\qquad(q,q),\qquad \left(\left\lfloor\frac{p+q}{2}\right\rfloor, \left\lceil\frac{p+q}{2}\right\rceil\right).$$第三对指针在代码中写作
(lef + rig) / 2
(lef + rig + 1) / 2
因为下标非负,整数除法正好实现所需的向下取整。当 为偶数时,两式相同;为奇数时,两式相差 。
从中心向外扩展
对每个候选中心,建立一个出现标记数组 seen。当左右指针都未越界,且对应元素相等时,将两个端点的值标记为出现,然后令左指针减一、右指针加一。
遇到不相等或越界后停止,得到该中心能够扩展出的最长回文区间。随后从 开始扫描出现标记,找到第一个没有出现的整数,就是其 。
为什么只检查该中心的最长回文,而不检查所有较短的回文?因为扩展只会增加元素,不会删除已出现的元素,故 不会减小。最长回文的 一定不小于同中心任何较短回文的 。
C++示例代码
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
using uint = unsigned int;
using ll = long long;
using ull = unsigned long long;
using lll = __int128;
using ulll = unsigned __int128;
const int N = 1e6 + 10;
void solve(){
int n;
cin >> n;
int len = 2 * n;
vector<int> arr(len), zero;
for(int i = 0; i < len; i++){
cin >> arr[i];
if(arr[i] == 0) zero.push_back(i);
}
auto calc = [&](int lef, int rig){
vector<int> seen(n + 1, 0);
while(lef >= 0 && rig < len && arr[lef] == arr[rig]){
seen[arr[lef]] = 1;
seen[arr[rig]] = 1;
lef--;
rig++;
}
int mex = 0;
while(mex < n && seen[mex]) mex++;
return mex;
};
int lef = zero[0], rig = zero[1];
int ans = max(calc(lef, lef), calc(rig, rig));
ans = max(ans, calc((lef + rig) / 2, (lef + rig + 1) / 2));
cout << ans << endl;
}
int main(){
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr); std::cout.tie(nullptr);
int _ = 1;
cin >> _;
while (_--) solve();
return 0;
}