一小片夕阳落在我手上.|

Natural_TLP

园龄:5个月粉丝:3关注:2

2025-01-22 00:33阅读: 50评论: 0推荐: 0

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。

思路

一个很有趣的题,涉及到数论,我赛后写了一下发现不难。

首先

\[x \oplus y = gcd(x, y)\\ \Rightarrow gcd(x, y) ~ | ~ x 、gcd(x, y) ~ | ~ y \]

然后,假设 \(i = x \oplus y = gcd(x, y)\),根据异或的性质有

\[x = i \oplus y \\ 则 ~ i = gcd(i \oplus y, 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)

这其实是一个和调和级数有关的时间复杂度。
即:

\[\frac{N}{1} + \frac{N}{2} + \frac{N}{3} + \dots + \frac{N}{N - 1} + \frac{N}{N} \\ \Rightarrow N \times(\frac{1}{1} + \frac{1}{2} + \frac{1}{3} + \dots + \frac{1}{N - 1} + \frac{1}{N}) \\ 而 ~ \frac{1}{1} + \frac{1}{2} + \frac{1}{3} + \dots + \frac{1}{N - 1} + \frac{1}{N}就是调和级数,当N无穷大时结果约等于ln(N) \\ 所以上面的双重循环的时间复杂度可以看作是N倍调和级数即O(N \times ln(N))。 \]

代码

点击查看代码
#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,然后做前缀和。那么怎么确定子区间是双生数组呢?就是前缀和的值相等的区间就是双生数组。

比如:

\[数组:x ~ x ~ y ~ x ~ y ~ y ~ x ~ y \\ 贡献:1 ~ 2 ~ 1 ~ 2 ~ 1 ~ 0 ~ 1 ~ 0 \\ \]

你会发现,第一个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;
}

本文作者:Natural-TLP

本文链接:https://www.cnblogs.com/Natural-TLP/p/18684517

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   Natural_TLP  阅读(50)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起
  1. 1 404 not found REOL
404 not found - REOL
00:00 / 00:00
An audio error has occurred.

作曲 : Reol

作词 : Reol

fade away...do over again...

fade away...do over again...

歌い始めの一文字目 いつも迷ってる

歌い始めの一文字目 いつも迷ってる

どうせとりとめのないことだけど

伝わらなきゃもっと意味がない

どうしたってこんなに複雑なのに

どうしたってこんなに複雑なのに

噛み砕いてやらなきゃ伝わらない

ほら結局歌詞なんかどうだっていい

僕の音楽なんかこの世になくたっていいんだよ

Everybody don't know why.

Everybody don't know why.

Everybody don't know much.

僕は気にしない 君は気付かない

何処にももういないいない

Everybody don't know why.

Everybody don't know why.

Everybody don't know much.

忘れていく 忘れられていく

We don't know,We don't know.

目の前 広がる現実世界がまた歪んだ

目の前 広がる現実世界がまた歪んだ

何度リセットしても

僕は僕以外の誰かには生まれ変われない

「そんなの知ってるよ」

気になるあの子の噂話も

シニカル標的は次の速報

麻痺しちゃってるこっからエスケープ

麻痺しちゃってるこっからエスケープ

遠く遠くまで行けるよ

安定なんてない 不安定な世界

安定なんてない 不安定な世界

安定なんてない きっと明日には忘れるよ

fade away...do over again...

fade away...do over again...

そうだ世界はどこかがいつも嘘くさい

そうだ世界はどこかがいつも嘘くさい

綺麗事だけじゃ大事な人たちすら守れない

くだらない 僕らみんなどこか狂ってるみたい

本当のことなんか全部神様も知らない

Everybody don't know why.

Everybody don't know why.

Everybody don't know much.

僕は気にしない 君は気付かない

何処にももういないいない

Everybody don't know why.

Everybody don't know why.

Everybody don't know much.

忘れていく 忘れられていく

We don't know,We don't know.