2025牛客寒假算法基础集训营1
A. 茕茕孑立之影
题意:给你\(n\)个数,你要找一个数使得这个数和数组的任意一个数都不成倍数关系。
如果数组里有\(1\)肯定不行,\(1\)是所有数的因子。其他情况我们只需要找一个大质数就行,因为值域只有\(1e9\),可以输出\(1e9+7\)。
点击查看代码
void solve() {
int n;
std::cin >> n;
std::vector<int> a(n);
int ans = 1e9 + 7;
for (int i = 0; i < n; ++ i) {
std::cin >> a[i];
}
std::sort(a.begin(), a.end());
if (a[0] == 1) {
std::cout << -1 << "\n";
} else {
std::cout << ans << "\n";
}
}
B. 一气贯通之刃
题意:给你一颗树,要你找一条简单路径经过所有点。
如果这颗树不是一条链的话,不可能找到一条路径经过所有点。所以判断是不是链,然后找链的两端就行。
可以用度数判断,度数为一个点连的边数量。一条链上的点度数都小于等于\(2\),并且两端的点度数是\(1\)。
点击查看代码
void solve() {
int n;
std::cin >> n;
std::vector<int> deg(n);
for (int i = 1; i < n; ++ i) {
int u, v;
std::cin >> u >> v;
-- u, -- v;
++ deg[u];
++ deg[v];
}
std::vector<int> ans;
int cnt = 0;
for (int i = 0; i < n; ++ i) {
if (deg[i] == 1) {
ans.push_back(i);
}
cnt += deg[i] == 2;
}
if (cnt != n - 2 || ans.size() != 2) {
std::cout << -1 << "\n";
} else {
std::cout << ans[0] + 1 << " " << ans[1] + 1 << "\n";
}
}
C. 兢兢业业之移
题意:给你一个\(01\)矩阵,你要把所有的\(1\)都移动到左上部分。给出方案。
直接枚举所有左上部分的点,我们按行从上到下,按列从左到右枚举。那么如果我们枚举到了\((i,j)\),则所有\(1 <=x < i, 1 <= y <= n / 2\)的地方以及\(x=i, 1 <= y < j\)的地方全都是\(1\),如果这个\((i, j)\)是\(0\),我们找一个不在已经操作好的位置的\(1\)的位置移过来就行了。
点击查看代码
void solve() {
int n;
std::cin >> n;
std::vector<std::string> s(n);
for (int i = 0; i < n; ++ i) {
std::cin >> s[i];
}
std::vector<std::array<int, 4> > ans;
for (int i = 0; i < n / 2; ++ i) {
for (int j = 0; j < n / 2; ++ j) {
if (s[i][j] == '0') {
int x = -1, y = -1;
for (int l = 0; l < n && x == -1; ++ l) {
for (int r = 0; r < n && x == -1; ++ r) {
if ((l < i && r < n / 2) || (l == i && r < j)) {
continue;
}
if (s[l][r] == '1') {
x = l, y = r;
break;
}
}
}
while (x < i) {
std::swap(s[x][y], s[x + 1][y]);
ans.push_back({x, y, x + 1, y});
++ x;
}
while (y < j) {
std::swap(s[x][y], s[x][y + 1]);
ans.push_back({x, y, x, y + 1});
++ y;
}
while (y > j) {
std::swap(s[x][y], s[x][y - 1]);
ans.push_back({x, y, x, y - 1});
-- y;
}
while (x > i) {
std::swap(s[x][y], s[x - 1][y]);
ans.push_back({x, y, x - 1, y});
-- x;
}
}
}
}
std::cout << ans.size() << "\n";
for (auto & [a, b, c, d] : ans) {
std::cout << a + 1 << " " << b + 1 << " " << c + 1 << " " << d + 1 << "\n";
}
}
D. 双生双宿之决
题意:一个数组是双生数组,那么它恰好有两种元素,并且每一种元素的个数是\(n/2\)。判断给你的数组是不是双生数组。
统计判断即可。
点击查看代码
void solve() {
int n;
std::cin >> n;
std::map<int, int> mp;
for (int i = 0; i < n; ++ i) {
int x;
std::cin >> x;
++ mp[x];
}
std::vector<int> a;
for (auto & [x, y] : mp) {
a.push_back(y);
}
if (a.size() != 2 || a[0] != a[1]) {
std::cout << "No\n";
} else {
std::cout << "Yes\n";
}
}
E. 双生双宿之错
题意:一个数组是双生数组,那么它恰好有两种元素,并且每一种元素的个数是\(n/2\),你每次可以让数组一个元素加一或者减一,求让数组变成双生数组的最小操作数。
先排序,因为只有两个数组并且每个都是一半,那么我们分成左半和右半来操作。每一边都要变成一个相同的数。变成两边的中位数是最优的。但可能两边中位数一样,那么我们枚举两边中位数减少或增大就行了。
为什么中位数最优?具体的来说,我们设在中间位置左边的所有点,到中位数的差之和为\(p\),右边的差之和则为\(q\)那么我们就必须让\(p+q\)的值尽量小。当位置向左移动的话,\(p\)会减少\(x\)但是\(q\)会增加\(n−x\).所以说当为数组中位数的时候,\(p+q\)最小。
点击查看代码
void solve() {
int n;
std::cin >> n;
std::vector<i64> a(n + 1);
for (int i = 1; i <= n; ++ i) {
std::cin >> a[i];
}
if (n == 2) {
if (a[1] == a[2]) {
std::cout << 1 << "\n";
} else {
std::cout << 0 << "\n";
}
return;
}
std::sort(a.begin(), a.end());
if (a[1] == a[n]) {
std::cout << n / 2 << "\n";
return;
}
i64 midl = a[n / 2 / 2], midr = a[n / 2 + n / 2 / 2];
i64 ans = 1e18;
for (i64 x = midl - 10; x <= midl + 10; ++ x) {
for (i64 y = midr - 10; y <= midr + 10; ++ y) {
if (x == y) {
continue;
}
i64 sum = 0;
for (int i = 1; i <= n; ++ i) {
if (i <= n / 2) {
sum += std::abs(a[i] - x);
} else {
sum += std::abs(a[i] - y);
}
}
ans = std::min(ans, sum);
}
}
midl = a[n / 2 / 2 + 1];
midr = a[n / 2 + n / 2 / 2 + 1];
for (i64 x = midl - 10; x <= midl + 10; ++ x) {
for (i64 y = midr - 10; y <= midr + 10; ++ y) {
if (x == y) {
continue;
}
i64 sum = 0;
for (int i = 1; i <= n; ++ i) {
if (i <= n / 2) {
sum += std::abs(a[i] - x);
} else {
sum += std::abs(a[i] - y);
}
}
ans = std::min(ans, sum);
}
}
std::cout << ans << "\n";
}
F. 双生双宿之探
题意:双生数组的定义和\(E\)题一样。求有多少子数组是双生数组。
考虑双指针枚举每个只有两个数的区间。那么我们确定了双生数组的两个数。那么就可以单独求这个区间的贡献。设两个数是\(x,y\),当\(a_i\)等于\(x\)时加\(1\),否则减\(1\)。那么就变成求一个前缀和和当前相等。
点击查看代码
void solve() {
int n;
std::cin >> n;
std::vector<int> a(n);
for (int i = 0; i < n; ++ i) {
std::cin >> a[i];
}
i64 ans = 0;
for (int i = 0; i < n; ++ i) {
int j = i + 1;
std::map<int, int> mp;
++ mp[a[i]];
int x = a[i], y;
while (j < n) {
mp[a[j]] ++ ;
if (mp.size() > 2) {
break;
}
if (a[j] != x) {
y = a[j];
}
++ j;
}
if (mp.size() == 1) {
break;
}
std::map<int, int> cnt;
++ cnt[0];
int sum = 0;
for (int k = i; k < j; ++ k) {
if (a[k] == x) {
++ sum;
} else {
-- sum;
}
ans += cnt[sum];
++ cnt[sum];
}
for (int k = j - 1; k >= i; -- k) {
if (a[k] != a[j - 1]) {
i = k;
break;
}
}
}
std::cout << ans << "\n";
}
G. 井然有序之衡
题意:给你一个数组,你每次可以给一个数加一同时给另一个数减一。问能不能变成一个排列,求最小操作数。
将数组排序后,那么应该时最小的变成\(1\),第二小的变成\(2\) ... 最大的变成\(n\)。那么模拟即可。
因为每次操作不会改变数组总和,那么一个排列的总和位\(\frac{n(n+1)}{2}\),判断数组总和是不是这个数就行了。不是则不可能变成。
点击查看代码
void solve() {
int n;
std::cin >> n;
std::vector<int> a(n);
for (int i = 0; i < n; ++ i) {
std::cin >> a[i];
}
if (std::accumulate(a.begin(), a.end(), 0ll) != (i64)n * (n + 1) / 2) {
std::cout << -1 << "\n";
return;
}
std::sort(a.begin(), a.end());
i64 ans = 0;
for (int i = 0, j = n - 1; i < j;) {
if (a[i] == i + 1) {
++ i;
} else if (a[j] == j + 1) {
-- j;
} else {
i64 t = std::min(i + 1 - a[i], a[j] - (j + 1));
ans += t;
a[i] += t;
a[j] -= t;
}
}
std::cout << ans << "\n";
}
H. 井然有序之窗
题意:有一个排列,现在告诉你每个位置数的范围,你要构造一个合适的排列。
我们按照右端点从大到小给,每次尽量给最小的。因为如果有一个区间\(l\)更小并且长度小于当前区间但在它后面,那么他可选的数更多,我们不应该先关心它。否则在前面,已经考虑过了。用一个\(set\)维护没选过的数即可。
点击查看代码
void solve() {
int n;
std::cin >> n;
std::vector<std::array<int, 3> > a(n);
for (int i = 0; i < n; ++ i) {
int l, r;
std::cin >> l >> r;
a[i] = {r, l, i};
}
std::sort(a.begin(), a.end());
std::vector<int> ans(n);
std::set<int> s;
for (int i = 1; i <= n; ++ i) {
s.insert(i);
}
for (auto & [r, l, i] : a) {
auto it = s.lower_bound(l);
if (it == s.end() || *it > r) {
std::cout << -1 << "\n";
return;
}
ans[i] = *it;
s.erase(it);
}
for (int i = 0; i < n; ++ i) {
std::cout << ans[i] << " \n"[i == n - 1];
}
}
I. 井然有序之桠
题意:给你一个排列,你要构造一个排列,使得\(\sum gcd(a_i, p_i) = k\)。
这种构造题真的是靠智商吧。。。
看\(jiangly\)的视频时很震撼,秒出思路就算了,居然还可以直接想到两个特殊情况。不过\(jiangly\)的递归写法确实非常好,这里我们先不管怎么想特殊情况,讲讲一些我们能想到的思路。
首先我们不关注\(a\)是怎么排列的,因为两个排列的数是一一对应的,所以我们考虑排列为\(\{1, 2, ... , n - 1, n\}\)时该怎么构造。
我们知道,使用分治法的前提时我们可以把原问题分成多个和原问题等价的规模更小的问题,那么我们将问题改为求\(\sum gcd(p_i, i) = k\)的解。\(1\)到\(i\)的排列能产生的最小值是\(i\),那么只要\(i-1\)大于等于\(k-i\),我们给\(i\)这个位置填\(i\),就变成一个可行的更小的问题:求\(\sum gcd(p_{i-1}, {i-1}) = k - i\)的解,然后结尾加上一个\(i\)就行;否则,我们可以求\(\sum gcd(p_{i-2}, {i-2}) = k - 2\)的解,在末尾加上\(i, i - 1\)就行。这样就变成更小的子问题,当\(n=0\)或者\(n=1\)的时候我们就可以轻松解决。
这个思路似乎很好,但这个题还有两个特殊情况,如果不通过大量试样例或者打表找规律,我肯定是想不到的。如果我们遇到我们想出来一个很好的方法还是无法通过的情况,可以打表找特例。
这里,用下面的代码发现好像只有两个样例,如果没发现特例是有规律的,那就直接特判再交一发看看。
点击查看代码
for (int i = 1; i <= 5000; ++ i) {
for (int j = i; j < i + i - 1; ++ j) {
if (j - 2 > (i - 2) * (i - 1) / 2) {
std::cout << i << " " << j << "\n";
}
}
}
下面是参考\(jiangly\)代码的递归写法。
点击查看代码
std::vector<int> get(int n, i64 k) {
std::vector<int> p;
if (n == 0) {
} else if (n == 1) {
p = {1};
} else if (k >= n + n - 1) {
p = get(n - 1, k - n);
p.push_back(n);
} else if (n == 3 && k == 4) {
p = {3, 2, 1};
} else if (n == 4 && k == 6) {
p = {3, 4, 1, 2};
} else {
p = get(n - 2, k - 2);
p.push_back(n);
p.push_back(n - 1);
}
return p;
}
void solve() {
int n;
i64 k;
std::cin >> n >> k;
std::vector<int> a(n);
for (int i = 0; i < n; ++ i) {
std::cin >> a[i];
}
if (k < n) {
std::cout << -1 << "\n";
return;
}
auto p = get(n, k);
for (int i = 0; i < n; ++ i) {
std::cout << p[a[i] - 1] << " \n"[i == n - 1];
}
}
J. 硝基甲苯之袭
题意:给你一个数组,有多少对\((i, j)\)满足\(gcd(a_i, a_j) = a_i \oplus a_j\)。
因为值域很小,那么我们枚举每个数的约数\(d\),然后判断\(gcd(a_i, a_i \oplus d)\)是不是等于\(d\)就行了。从左到右计算,记录前面每个数的数量。
点击查看代码
void solve() {
int n;
std::cin >> n;
std::vector<int> a(n);
for (int i = 0; i < n; ++ i) {
std::cin >> a[i];
}
const int N = 2e5 + 5;
std::vector<std::vector<int> > factor(N);
for (int i = 1; i < N; ++ i) {
for (int j = i; j < N; j += i) {
factor[j].push_back(i);
}
}
std::sort(a.begin(), a.end());
std::map<int, int> cnt;
i64 ans = 0;
for (int i = 0; i < n; ++ i) {
for (auto & j : factor[a[i]]) {
if (std::gcd(a[i], a[i] ^ j) == j) {
ans += cnt[a[i] ^ j];
}
}
cnt[a[i]] += 1;
}
std::cout << ans << "\n";
}
K. 硝基甲苯之魇
题意:给你一个数组,求有多少区间满足区间\(gcd\)等于区间异或和。
因为\(gcd\)最大操作\(log\)次,所以如果固定右端点\(r\),那么左边最多有\(log\)个\(gcd\)不同的区间\([l_i, r]\)。
并且连续做\(gcd\)的值时递减的,这提示我们按\(gcd\)去找左端点,如果\(l_i\)是第一个使得\([l_i, r]\)的区间\(gcd\)为\(x\)的位置,那么可以二分向左找最左边等于区间和等于\(x\)。假设得到\([p, l_i]\)这些点作为左端点到\(r\)的区间和都是\(x\),记\(sum_i\)为\(1\)到\(i\)的异或和,那么我们要找\(sum_{j-1} = sum_i \oplus x\)并且在\([p, l_i]\)之间的位置。区间\(gcd\)可以用线段树维护,记录每个前缀异或和的位置可以用\(map\)记录。
因为最多跳\(log\),所以加上线段树的\(log\)时间复杂度就是\(O(nlog^2n)\)。
点击查看代码
#define ls (u << 1)
#define rs (u << 1 | 1)
#define umid (tr[u].l + tr[u].r >> 1)
struct Node {
int l, r;
int d;
};
struct SegmentTree {
std::vector<Node> tr;
SegmentTree(int _n) {
tr.assign(_n << 2, {});
build(1, _n);
}
void pushup(int u) {
tr[u].d = std::gcd(tr[ls].d, tr[rs].d);
}
void build(int l, int r, int u = 1) {
tr[u] = {l, r};
if (l == r) {
return;
}
int mid = l + r >> 1;
build(l, mid, ls); build(mid + 1, r, rs);
}
void modify(int p, int x) {
int u = 1;
while (tr[u].l != tr[u].r) {
int mid = umid;
if (p <= mid) {
u = ls;
} else {
u = rs;
}
}
tr[u].d = x;
u >>= 1;
while (u) {
pushup(u);
u >>= 1;
}
}
int query(int l, int r, int u = 1) {
if (l <= tr[u].l && tr[u].r <= r) {
return tr[u].d;
}
int mid = umid;
if (r <= mid) {
return query(l, r, ls);
} else if (l > mid) {
return query(l, r, rs);
}
return std::gcd(query(l, r, ls), query(l, r, rs));
}
};
void solve() {
int n;
std::cin >> n;
std::vector<int> a(n);
std::vector<int> sum(n + 1);
SegmentTree tr(n);
std::map<int, std::vector<int> > pos;
pos[0].push_back(0);
for (int i = 0; i < n; ++ i) {
std::cin >> a[i];
sum[i + 1] = sum[i] ^ a[i];
pos[sum[i + 1]].push_back(i + 1);
tr.modify(i + 1, a[i]);
}
i64 ans = 0;
for (int i = 1; i <= n; ++ i) {
int j = i - 1;
while (j) {
int d = tr.query(j, i);
int l = 1, r = j;
while (l < r) {
int mid = l + r >> 1;
if (tr.query(mid, i) == d) {
r = mid;
} else {
l = mid + 1;
}
}
int t = sum[i] ^ d;
int L = std::lower_bound(pos[t].begin(), pos[t].end(), l - 1) - pos[t].begin();
int R = std::upper_bound(pos[t].begin(), pos[t].end(), j - 1) - pos[t].begin() - 1;
if (L <= R) {
ans += R - L + 1;
}
j = l - 1;
}
}
std::cout << ans << "\n";
}
L. 一念神魔之耀
题意:\(n\)盏灯,每次可以选择连续的\(x\)或\(y\)盏灯切换他们的开关,求一个方案使得最后所有灯都是打开的。
我们可以操作任何长度为\(d = gcd(x, y)\)的区间,这里假设\(x > y\)。那么如果我们想操作\([l, l + d - 1]\),就先操作\([l, l + x - 1]\),这样就多出来\(x - d\)个额外被翻转的,我们要把它们翻回来,那么一直翻转右边的长度为\(y\)的区间,最后如果翻转到一个都不剩,就完成了,如果还有剩下一点长度\(k(k < y)\),使得无法翻转,那么就操作\([l + d + k, l + d + k + x]\),这样相当于加上去了\(2x\)减去了若干个\(y\),一直重复这个操作,其实就是求需要减去多少\(y\)和加上几个\(x\)可以正好使得多出来的翻转回来,这就是求\(ax+by=d\),根据裴蜀定理,这是一定有解的。那么对于\([1, n - y + 1]\)这一段,如果是\(0\),就直接翻转就行,最后只剩下,\([n - y, n]\)这一段,按照操作\(gcd\)的方案模拟即可。
点击查看代码
void solve() {
int n, x, y;
std::cin >> n >> x >> y;
std::string s;
std::cin >> s;
if (x < y) {
std::swap(x, y);
}
std::vector<std::array<int, 2> > ans;
auto op = [&](int l, int r) {
ans.push_back({l, r});
for (int i = l; i <= r; ++ i) {
s[i] ^= 1;
}
};
int d = std::gcd(x, y);
for (int i = 0; i + y - 1 < n; ++ i) {
if (s[i] == '0') {
op(i, i + y - 1);
}
}
for (int i = n - 1; i + y - 1 >= n; -- i) {
if (s[i] == '0') {
op(i - x + 1, i);
int l = i - x + 1;
while (1) {
int j = l;
while (j + y - 1 <= i - d) {
op(j, j + y - 1);
j += y;
}
if (j == i - d + 1) {
break;
}
op(j - x, j - 1);
l = j - x;
}
}
}
for (auto & c : s) {
if (c == '0') {
std::cout << -1 << "\n";
return;
}
}
std::cout << ans.size() << "\n";
for (auto & [l, r] : ans) {
std::cout << l + 1 << " " << r + 1 << "\n";
}
}
M. 数值膨胀之美
题意:给你一个数组,你要恰好执行一次操作,选择一段区间让这个区间的数都乘2。最小化极差。
要让影响极差,那么我们肯定要改最大值和最小值,发现改最大值只会变大。那么我们应该操作最小值。随便找一个最小值的位置,然后两边扩展,看是不是最小值然后乘\(2\)即可。要用\(set\)实时维护最大最小值。
点击查看代码
void solve() {
int n;
std::cin >> n;
std::vector<i64> a(n);
std::multiset<i64> s;
for (int i = 0; i < n; ++ i) {
std::cin >> a[i];
s.insert(a[i]);
}
i64 ans = 1e18;
for (int i = 0; i < n; ++ i) {
if (a[i] == *s.begin()) {
s.extract(a[i]);
s.insert(a[i] * 2);
ans = std::min(ans, *s.rbegin() - *s.begin());
int l = i - 1, r = i + 1;
while (l >= 0 || r < n) {
if (l >= 0 && a[l] == *s.begin()) {
s.extract(a[l]);
s.insert(a[l] * 2);
ans = std::min(ans, *s.rbegin() - *s.begin());
-- l;
} else if (r < n && a[r] == *s.begin()) {
s.extract(a[r]);
s.insert(a[r] * 2);
ans = std::min(ans, *s.rbegin() - *s.begin());
++ r;
} else {
break;
}
}
break;
}
}
std::cout << ans << "\n";
}