2025牛客寒假算法基础集训营1补题笔记
题目难度顺序大致为:A D B G M H E J C F L K I
头疼的思维+模拟。
前 \(4\) 题写得挺顺,但 \(D\) 题没看清是两种元素出现次数相同wa了一发,\(M\) 题其实一开始没有思路但暴力写了一波奇迹的过了,赛后果然被hack数据太水,\(H\) 卡了4个钟。。。\(E\) 题明显的贪心结论没有套上,歪到平均数去了,然后就一直在 \(H\) 钻牛角尖。
A.茕茕孑立之影
题意
给定一个数组,找到一个正整数 \(x\),使得 \(x\) 和数组中的元素互不为倍数关系。
思路
- 首先 \(1\) 是任何数的因数,所以有 \(1\) 的时候没有答案。
- 然后考虑没有 \(1\) 的情况,可以发现只需要找到一个比数组中的元素都大的质数就可以,因为数组元素都不超过 \(10^9\) ,直接输出 \(1000000007\) 即可。
代码
点击查看代码
#include <iostream>
using namespace std;
const int P = 1e9 + 7;
int n;
void solve()
{
bool flag = 0;
cin >> n;
for (int i = 1; i <= n; i ++) {
int x;
cin >> x;
if (x == 1) flag = 1;
}
if (flag) cout << -1 << '\n';
else cout << P << '\n';
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int t = 1;
cin >> t;
while (t --) solve();
return 0;
}
D.双生双宿之决
题意
给定一个数组,判断是否为双生数组,即元素种类数为 \(2\)、且出现次数相同。
思路
按题意模拟即可。用 \(set\) 来筛选种类个数,用 \(map\) 来记录每个数出现次数。
也可以排序,检查前半部分和后半部分数是否相等即可。
代码
点击查看代码
#include <iostream>
#include <algorithm>
#include <map>
#include <set>
#define si(x) int(x.size())
#define fi first
#define se second
using namespace std;
int n;
void solve()
{
cin >> n;
set<int> v;
map<int, int> mp;
for (int i = 0; i < n; i ++) {
int x;
cin >> x;
mp[x] ++;
v.insert(x);
}
if (n % 2 || si(v) != 2) cout << "No" << '\n';
else {
int num = 0;
for (auto it : mp) {
if (num == 0) num = it.se;
else if (num != it.se) {
cout << "No" << '\n';
return ;
}
}
cout << "Yes" << '\n';
}
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int t = 1;
cin >> t;
while (t --) solve();
return 0;
}
代码2
点击查看代码
void solve()
{
cin >> n;
for (int i = 1; i <= n; i ++) cin >> a[i];
sort(a + 1, a + 1 + n);
if (n % 2 || a[1] == a[n]) return void(cout << "No" << '\n');
if (a[1] == a[n / 2] && a[n / 2 + 1] == a[n]) cout << "Yes" << '\n';
else cout << "No" << '\n';
}
B.一气贯通之刃
题意
给一棵树,找到一条路径经过所有节点。
思路
自己手动画几棵树可以发现:如果一棵树的某个节点出度超过 \(2\) ,即这个节点与至少 \(3\) 个节点有连边,那么就不存在有简单路径是经过所有节点的,所以我们只需要去遍历一遍所有节点的出度就可以了。而起点、终点,则明显是两个叶子节点,出度为 \(1\)。
题外知识:一颗树的最长简单路径就是这棵树的直径。可以用树形 \(dp\) 来解决。
代码
点击查看代码
#include <iostream>
using namespace std;
const int N = 1e6 + 10;
int n, u, v;
int a[N];
void solve()
{
cin >> n;
for (int i = 1; i < n; i ++) {
cin >> u >> v;
a[u] ++, a[v] ++;
}
int sd = -1, ed = -1;
for (int i = 1 ; i <= n; i ++) {
if (a[i] > 2) return void(cout << -1 << '\n');
if (a[i] == 1)
if (sd == -1) sd = i;
else ed = i;
}
cout << sd << ' ' << ed;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int t = 1;
while (t --) solve();
return 0;
}
G.井然有序之衡
题意
给一个数组,每次操作可以使一个元素加 \(1\),另一个元素减 \(1\) ,问变成排列的最小操作次数。
思路
首先,一个元素加 \(1\), 一个元素减 \(1\),对于数组总和是不变,所以数组是否可以构造成排列,在于数组总和和排列总和是否相等。然后是计算最小操作数。
贪心的方法解决最小操作数。
将数组进行升序排序,然后按 \(1 \sim n\) 的排列顺序计算操作个数。
代码
点击查看代码
#include <iostream>
#include <algorithm>
#define ll long long
using namespace std;
const int N = 1e6 + 10;
ll n;
ll a[N];
void solve()
{
cin >> n;
ll sum = 0;
for (int i = 1; i <= n; i ++) {
cin >> a[i];
sum += a[i];
}
ll num = (n + 1) * n / 2;
if (num != sum) cout << -1;
else {
sort(a + 1, a + 1 + n);
ll res = 0;
for (int i = 1; i <= n; i ++)
res += abs(i - a[i]);
cout << res / 2;
}
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int t = 1;
while (t --) solve();
return 0;
}
M.数值膨胀之美
题意
给定一个数组,可以选择一个区间将所有元素乘 \(2\),问操作后的最小极差。
思路
赛后重新思考,想到可以从第一个最小值开始维护区间,到最后包括所有最小值。
如何维护呢?
-
首先存下所有元素的值和下标,升序排序。
-
然后从第一个最小值下标开始,按区间右端点增大方向操作,到达下一个最小值的位置,区间内的数都要乘2,直到包括所有的最小值后结束。
最后这个思路只过了86.11%,看完题解才知道还要继续考虑次小值直到最大值。
(其实赛时已经发现假设选取所有元素乘2可能比选取子区间要更优,但赛后忘了。。。)
代码
点击查看代码
#include <iostream>
#include <algorithm>
#define fi first
#define se second
using namespace std;
typedef pair<int, int> PII;
const int N = 1e5 + 10;
int n, b[N];
PII a[N];
int main()
{
cin >> n;
for (int i = 1; i <= n; i ++) {
cin >> b[i];
a[i] = {b[i], i};
}
sort(a + 1, a + 1 + n);
int res = 0x3f3f3f3f;
a[n + 1].fi = res;
int maxv = a[n].fi, l = a[1].se, r = a[1].se;
for (int i = 1; i <= n; i ++) {
while (a[i].se <= l) maxv = max(maxv, b[l --] * 2);
while (a[i].se >= r) maxv = max(maxv, b[r ++] * 2);
res = min(res, maxv - min(a[1].fi * 2, a[i + 1].fi));
}
cout << res;
return 0;
}
H.井然有序之窗
题意
构造一个排列,满足每个元素都在一个指定的区间内。
思路
一个经典的贪心题吧,居然在这跑dfs,感觉我赛时一定是脑子抽风了。
先说结论:第 \(i\) 个位置如果多个选择,那么选择区间右端点最小的那个数,结果一定不会更劣。
为什么呢?可以自己模拟一下:
假设现在要选择一个数填入第5的位置,有3种选择:3[3, 7]、6[4, 5]、8[5, 6]。
首先我们得知道既然已经到填入第5的位置了,那么 \(1\sim4\) 的位置都已经完成填入了,所以对于这4种选择,可以发现3和6的区间是要更小的:3[5, 7]、6[5, 5]。
所以这个位置如果先选3或8填入,那么6就无法填入了,而如果每个位置的多种选案都选右端点最小的填入,那么对后面的位置影响是最小的。
实现用优先队列来维护右端点的小根堆,枚举 \(1 \sim n\)的位置,将在这个位置下的所有未选区间放入队列中,如果没有或队首的右端点小于当前位置,就没有方案可行。
代码
点击查看代码
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 1e6 + 10;
int n;
struct node {
int val;
int l, r;
bool operator < (const node& b) const {
return r > b.r;
}
} a[N];
priority_queue<node> pq;
int ans[N];
bool cmp(node aa, node bb) {
if (aa.l == bb.l) return aa.r < bb.r;
return aa.l < bb.l;
}
void solve()
{
cin >> n;
for (int i = 1; i <= n; i ++) {
int l, r;
cin >> l >> r;
a[i] = {i, l, r};
}
sort(a + 1, a + 1 + n, cmp);
for (int i = 1, j = 1; i <= n; i ++) {
while (j <= n && a[j].l <= i) pq.push(a[j ++]);
if (pq.empty() || pq.top().r < i) return void(cout << -1);
ans[pq.top().val] = i;
pq.pop();
}
for (int i = 1; i <= n; i ++) cout << ans[i] << ' ';
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
solve();
return 0;
}
E.双生双宿之错
题意
给定一个数组,每次操作可以使得一个元素加1或者减1,问最小操作几次可以变成双生数组,即元素种类数为2、且出现次数相同。
思路
\(D\) 题的扩展,其实是一个贪心结论题,参考货仓选址。叫中位数定理,这个今天才知道。
先说结论:求一个数 \(x\),让一组元素与 \(x\) 的差的绝对值的和最小,那么 \(x\) 是这组元素的中位数,结果不会更劣。
我们依旧先举例模拟:有两个数3、5,中位数可以是3或5,那么差值和就是2,假如我们在大于5或小于3的范围内选一个数,比如7,那么差值和就是4+2=6比2大。
其实我们可以发现选择的那个数可以让大于它和小于它的数相抵消,如果某方有多出的数就会多增加差值,就上面的例子:5 - 3 = (4 - 3) + (5 - 4)= |3 - 5|,中间的|3 - 4| + (5 - 4)其实就是5-3,-4和+4相抵消了,如果是7变为:|3 - 7| + |5 - 7|相对于4多加了两个(7 - 5)。
有了上面的结论,解决这道题就很容易了,先将数组排序,找出前后两部分的中位数,然后求差的绝对值之和。但要处理两个中位数相等的特殊情况,可以枚举四种情况:假设前半部分的中位数为lmid,后半部分中位数为rmid,那么算出(lmid-1,rmid)、(lmid+1,rmid)、(lmid,rmid-1)、(lmid,rmid+1)的结果然后取最小值。
代码
点击查看代码
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 1e5 + 10;
int n;
int a[N];
void solve()
{
cin >> n;
int m = n / 2;
for (int i = 1; i <= n; i ++) cin >> a[i];
sort(a + 1, a + 1 + n);
if (a[1] == a[n]) return void(cout << m << '\n');
int midl = a[(m + 1) >> 1], midr = a[(m + 1 + n) >> 1];
bool flag = 0;
if (midl == midr) midl --, flag = 1;
ll ans = 0;
for (int i = 1; i <= m; i ++) ans += abs(a[i] - midl);
for (int i = m + 1; i <= n; i ++) ans += abs(a[i] - midr);
if (flag) {
midl ++, midr ++;
ll sum = 0;
for (int i = 1; i <= m; i ++) sum += abs(a[i] - midl);
for (int i = m + 1; i <= n; i ++) sum += abs(a[i] - midr);
ans = min(ans, sum);
midl ++, midr --;
sum = 0;
for (int i = 1; i <= m; i ++) sum += abs(a[i] - midl);
for (int i = m + 1; i <= n; i ++) sum += abs(a[i] - midr);
ans = min(ans, sum);
midl --, midr ++;
sum = 0;
for (int i = 1; i <= m; i ++) sum += abs(a[i] - midl);
for (int i = m + 1; i <= n; i ++) sum += abs(a[i] - midr);
ans = min(ans, sum);
}
cout << ans << '\n';
}
int main()
{
int t;
cin >> t;
while (t --) solve();
return 0;
}
J.硝基甲苯之袭
题意
给定一个数组,问有多少对元素满足它们的gcd等于xor。
思路
一个很有趣的题,涉及到数论,我赛后写了一下发现不难。
首先
然后,假设 \(i = x \oplus y = gcd(x, y)\),根据异或的性质有
此时可以发现,\(i\) 是整除 \(y\) 的,我们可以枚举 \(i\)时,处理 \(i\) 的所有倍数,将符合上述等式且是给出的数组中的元素,那么就是一对方案,最后求和结果要除以2,因为 \(i \oplus y\) 和 \(y\) 都是数组中的元素那么就会重复算两遍。
然后是关于枚举的双重循环
for (int i = 1; i < N; i ++)
for (int j = i; j < N; j += i)
这其实是一个和调和级数有关的时间复杂度。
即:
代码
点击查看代码
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 2e5 + 10;
int n, x;
ll cnt[N];
int main()
{
cin >> n;
for (int i = 0; i < n; i ++) {
cin >> x;
cnt[x] ++;
}
ll ans = 0;
for (int i = 1; i < N; i ++)
for (int j = i; j < N; j += i)
if ((i ^ j) < N && __gcd(i ^ j, j) == i)
ans += cnt[i ^ j] * cnt[j];
cout << ans / 2;
return 0;
}
C.兢兢业业之移
题意
01矩阵,将所有1移动到矩阵左上角的四分之一区域。
思路
一道模拟题。
- 首先,我们可以从 \((0, 0)\)开始向y轴正方向枚举,到边界后回到下行首即 \((1, 0)\) 重新向y轴正方向枚举。
- 然后枚举中遇到1就将它移动到目标位置,目标位置用 \((x, y)\) 来表示。
- 移动过程中,注意移动的顺序,假设要移动的点在目标位置的上方,我们就要先移动x轴,再移动y轴,因为我这默认目标位置是一行行完成放置的,就意味着小于x的位置都已经放置了1,如果先移动y轴就会导致已放置好的1会被移出目标位置。
如下图,红色1为已放置完成的1,深蓝色线为先移动y轴的情况,会导致同行红色的1被整体向右移动一格,而橙色路线则不会对左上角目标区域造成影响。

- 接着,再考虑一个问题,如果上图的蓝色的1下一格刚好存在一个1,那么会导致这个1被移动到蓝色的1的位置上,所以,枚举时要判定这个格子在移动后是变为0的。
这个思路在最坏情况下,假设 $ \frac{n^2}{4} $ 个 1 都移动步数为 \(2n\),即 \(\frac{n^2}{4} \times 2n = \frac{n^3}{2}\),所以一定是可行的。
代码
点击查看代码
#include <iostream>
#include <cstring>
#include <vector>
#include <array>
using namespace std;
const int N = 100 + 10;
int n;
string g[N];
vector<array<int, 4>> ans;
void to_x(int i, int j, int x, int y)
{
while (i < x) {
ans.push_back({i, j, i + 1, j});
swap(g[i][j], g[i + 1][j]);
i ++;
}
while (i > x) {
ans.push_back({i, j, i - 1, j});
swap(g[i][j], g[i - 1][j]);
i --;
}
}
void to_y(int i, int j, int x, int y) {
while (j < y) {
ans.push_back({i, j, i, j + 1});
swap(g[i][j], g[i][j + 1]);
j ++;
}
while (j > y) {
ans.push_back({i, j, i, j - 1});
swap(g[i][j], g[i][j - 1]);
j --;
}
}
void move(int i, int j, int x, int y)
{
if (i < x) {
to_x(i, j, x, y);
to_y(x, j, x, y);
}
else if (i > x) {
to_y(i, j, x, y);
to_x(i, y, x, y);
}
else {
to_y(i, j, x, y);
}
}
void solve()
{
ans.clear();
cin >> n;
for (int i = 0; i < n; i ++) cin >> g[i];
int x = 0, y = 0;
for (int i = 0; i < n; i ++)
for (int j = 0; j < n; j ++)
while (g[i][j] == '1') {
move(i, j, x, y);
g[x][y] = '2';
y ++;
if (y == n / 2) x ++, y = 0;
}
cout << ans.size() << '\n';
for (auto [i, j, x, y] : ans)
cout << i + 1 << ' ' << j + 1 << ' ' << x + 1 << ' ' << y + 1 << '\n';
}
int main()
{
int t;
cin >> t;
while (t --) solve();
return 0;
}
F.双生双宿之探
题意
给定一个数组,问有多少连续子数组是双生数组,即元素种类数为2、且出现次数相同。
思路
求连续子数组的题一般会涉及到双指针、前缀和、差分之类的算法。
首先,我们可以用双指针来维护选择的子数组区间,维护的标准是最长的恰好包含两个元素的子数组区间,我称为类双生数组。
有了上面的选择,我们接下只需要确定这个类双生数组里面有多少个双生数组。
根据双生数组的定义,我们只需要找到类双生数组中有多少个元素x和元素y的个数相等的子区间即可。
求解方法就是做前缀和:让元素x贡献为+1,元素y贡献为-1,然后做前缀和。那么怎么确定子区间是双生数组呢?就是前缀和的值相等的区间就是双生数组。
比如:
你会发现,第一个1和第二个1之间存在一个双生数组“x y”,第一个1和第二个1之间存在一个双生数组“x y x y”,第一个2和第二个2之间存在一个双生数组“y x”等等。
由此就可以得到,遍历前缀和数组,统计每个数出现次数,将每个数在此之前出现次数求和就是答案。特殊的前缀和为0时就是一个双生数组,所以要再加上本身出现的次数。
代码
点击查看代码
#include <iostream>
#include <set>
#include <map>
using namespace std;
typedef long long ll;
const int N = 1e5 + 10;
int n;
int a[N];
ll get(int l, int r, int x, int y)
{
ll res = 0, sum = 0;
map<int, ll> s;
for (int i = l; i <= r; i ++) {
if (a[i] == x) sum ++;
else sum --;
s[sum] ++;
if (sum == 0) res += s[sum];
else res += s[sum] - 1;
}
return res;
}
void solve()
{
cin >> n;
for (int i = 1; i <= n; i ++) cin >> a[i];
a[n + 1] = -1;
set<int> v;
map<int, ll> mp;
ll ans = 0;
for (int i = 1, j = 1; i <= n; i ++)
{
v.insert(a[i]);
mp[a[i]] ++;
while (v.size() > 2 && j <= i) {
mp[a[j]] --;
if (mp[a[j]] == 0) v.erase(a[j]);
j ++;
}
while (v.size() <= 2 && i <= n) {
i ++;
v.insert(a[i]);
mp[a[i]] ++;
}
if (v.size() > 2 || i > n) {
v.erase(a[i]);
mp[a[i]] --;
i --;
}
if (v.size() != 2) break;
int x = -1, y = -1;
for (auto it : v)
if (x == -1) x = it;
else y = it;
ans += get(j, i, x, y);
}
cout << ans << '\n';
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int t;
cin >> t;
while (t --) solve();
return 0;
}

浙公网安备 33010602011771号