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;
}

posted @ 2025-01-22 00:33  Natural_TLP  阅读(92)  评论(0)    收藏  举报