题目难度顺序大致为: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 secondusing 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 longusing 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 secondusing 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;
}