跳到正文

「比赛日记」2026牛客多校R2

2026牛客多校R2

赛时两题,区完了;

总结:

  • 小范围暴力枚举 + 大范围正常做法的题见少了,且做题结论证明不充分 (G);
  • 被打信息差了,完全没学线性基 (B);

M - Maybe Connected

题意

一个大小为 \(n\) 的无向图,添加 \(n\) 条边,求连通但不相连的点对的个数的最大值。不允许添加重边或自环。

  • \(1 ≤ n ≤ 10^5 , 0 ≤ m ≤ min ( 10^9 , \frac{(n-1)*n}{2} )\)

思路

考虑最大化连通块的大小,但不能每次连上不同的点;

显然当 \(m\) 条边是一个大小为 \(n\) 的菊花图的边集的子集最优,每一个点都和另外一个相同的点相连

此时答案为 \(\frac{(m-1)*m}{2} \ (m \le n-1)\)

那当 \(m \ge n\) 时呢?这时原图不再能构成菊花图的树形结构,我们需要当前菊花图的一个叶子结点,成为第二个菊花图的“花心”。

这意味着每加一条边至少都会少一对非连边但联通的节点;

此时答案为\(\frac{(n-1)*(n-2)}{2} - (m - n + 1)\) ,化简得 $\frac{n*(n-1)}{2} - m $;

完整代码

inline void solve() {
    cin >> n >> m;
    if(m<=n-1) cout << m*(m+1)/2 - m << '\n';
    else cout << n*(n-1)/2 - m << '\n';
}

N - Narrow to Median

题意

有一个长度为 \(n\) 的数组,现对其操作恰一次,每次操作长度为 \(k\) 的子序 列全部替换为这个子序列的中位数,求数组最后的和的最大值。

  • \(1 ≤ k ≤ n, ∑n ≤ 2 × 10^5 , 1 ≤ a_i ≤ 10^9\)

思路

一个显然的思路是枚举中位数,而答案和位置无关,故排序;

对于一个确定的中位数,我们要让它左侧的数尽可能小,右侧的数尽可能接近它;

那左侧显然只要从头开始取,右侧从中位数下一个开始取就行了;

计算答案只要用数组总和减去选择序列的原总和加上\(k \ *\) 中位数;

对于计算总和,用前缀和维护即可;

对于中位数的枚举,由于选择序列长度奇偶会导致计算方法不同,需要分类讨论;

  • 对于最终序列是奇数长度的情况,枚举当前数为中位数;
  • 对于最终序列是偶数长度的情况,枚举当前数和下一个数的和的一半为中位数;

注意选择的区间下标计算和边界情况即可,场上因为这个爽吃罚时;

完整代码

#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
typedef pair<ll, ll> pll;

const ll mod = 1e9 + 7;
const ll inf = 1e9;
const int N = 3e5 + 10;


inline void solve() {
    ll n, k; cin >> n >> k;
    vector<ll> a(n+1,0), ps(n+1,0);
    for(int i = 1; i <= n;++i) cin >> a[i];
    sort(a.begin() + 1, a.end());
    for(int i = 1; i <= n;++i) ps[i] = ps[i - 1] + a[i];
    ll ans = -1e18;

    if(k == 1){
        cout << ps[n] <<"\n";
        return ;
    }

    for(int i = 1; i <= n;++i){
        if(k % 2 == 0){
            ll l = k / 2 - 1;
            ll r = i + k / 2;
            if(r > n || l >= i) continue;
            ll div = -(ps[r] - ps[i - 1] + ps[l] - ps[0]) + ((a[i] + a[i + 1]) * (k / 2));
            ans = max(ans, ps[n] + div);
        }
        else{
            ll l = k / 2;
            ll r = i + k / 2;
            if(r > n || l >= i) continue;
            ll div = -(ps[r] - ps[i - 1] + ps[l] - ps[0]) + a[i] * k;
            ans = max(ans, ps[n] + div);
        } 
    }
    cout << ans << "\n";
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);cout.tie(0);
    int T = 1;
    cin >> T;
    while (T--)
        solve();
    return 0;
}

B - Bitwise Maximization

题意

将一个长度为 \(n\) 的序列 \(A\) 拆分为两个集合,使得两个集合的异或和之和最大,求这个最大值。

  • \(\sum n \le 5*10^5, \ 0 \le A \le 2^{30}\)

思路

先考虑不拆分,此时答案就是整个数组的异或和 \(sum\)

没什么思路,在考虑按位看,显然如果 \(sum\) 中某一位为 \(1\), 显然整个数组拆开之后,无论你怎么拆分,必有一个集合的总和的答案这一位上是 \(1\);

对于 \(sum\) 中其他位,我们不能直接确定其值,考虑尽可能让它们变为1,且我们只关心这些位;

容易想到把原数组中,\(sum\) 中为 \(1\) 的位去掉,再插入线性基,通过线性基求剩下部分的最值;

由于分成了两个集合,且原来 \(sum\) 在这些位置上是 \(0\), 说明拆分后保证可以两边都能在这些位上取到 \(1\) (直观的想,两边都取奇数个 \(1\)), 因此线性基最大值对答案的贡献要 \(*2\) .

完整代码

#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
typedef pair<ll, ll> pll;

const ll mod = 1e9 + 7;
const ll inf = 1e9;
const int N = 2e5 + 10;

struct Basis {
    int n;
    bool uninsed = true;
    vector<ll> base, t;
    Basis(int _n) : n(_n), base(n + 1), t(n) {}
    void insert(ll x) {
        for (int i = n; i >= 0; --i) {
            if (((x >> i) & 1ll) == 0) continue;
            if (base[i] == 0) {
                base[i] = x;
                return;
            }
            x ^= base[i];
        }
        uninsed = true;
    }
    bool check(ll x) {
        for (int i = n; i >= 0; --i) {
            if (((x >> i) & 1ll) == 0) continue;
            if (base[i] == 0) return false;
            x ^= base[i];
        }
        return true;
    }

    ll qmx() {
        ll res = 0;
        for (int i = n; i >= 0;--i) {
            res = max(res, (res ^ base[i]));
        }
        return res;
    }
    ll qmn() {
        if (uninsed) return 0ll;
        for (ll i = 0; i <= n;++i) {
            if (base[i]) return base[i];
        }
    }
    ll query(ll k) {
        ll res = 0, cnt = 0;
        if (uninsed && k == 1) return 0;
        for (ll i = 0; i <= n; ++i) {
            for (ll j = i - 1; j >= 0; --j)
                if (base[i] & (1ll << j)) base[i] ^= base[j];
            if (base[i]) t[cnt++] = t[i];
        }
        if (k >= (1ll << cnt)) return -1;
        for (ll i = 0;i < cnt;i++)
            if (k & (1ll << i)) res ^= t[i];
        return res;
    }// k-th smallest
    void merge(const Basis& other) {
        for (int i = 0; i <= n; ++i) {
            if (other.base[i] != 0) {
                insert(other.base[i]);
            }
        }
    }
};

inline void solve() {
    ll n, sum = 0, res = 0; cin >> n;
    Basis base(32);
    vector<ll> a(n + 1, 0);
    for (int i = 1; i <= n;++i) {
        cin >> a[i];
        sum ^= a[i];
    }
    res = (((1 << 30) - 1) ^ sum);
    for (int j = 1; j <= n;++j) {
        base.insert((a[j] & res));
    }
    cout << sum + 2ll * base.qmx() << "\n";
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);cout.tie(0);
    int T = 1;
    cin >> T;
    while (T--)
        solve();
    return 0;
}

G - GCD Graph

对于一个由所有正整数构成的图,对于 \(u \lt v\)\(u\)\(v\) 的边权为 \(gcd(u, v)\) 。对于 \(u < v\) ,记 \(u\)\(v\)的最短路为 \(dis(u, v)\) ,求 \(\sum_{i =l}^{r}dis(i, n)\) .

  • \(1 ≤ l ≤ r < n ≤ 10^7\)

思路1

很显然,对于 \(i \in [l,r]\),有:

  • \(i,n\) 互质,对答案的贡献为 \(1\), 因为 \(gcd(i,n) = 1\) ;
  • \(i,n\) 不互质,则有 \(j \in (i,n)\)\(gcd(i,j) = 1\)\(gcd(j,n) = 1\) ,对答案的贡献为 \(2\) ;

到这里很多队伍都会猜这个结论是恒成立的(一个机房里有三个一开始是这么猜的),实则不然;

那先假设成立,怎么写?

我们显然只要筛选 \([l,r]\) 之间的 \(n\) 的质因数的倍数,这样我们就可以知道区间内有多少和 \(n\) 不互质的数

要筛选 \(n\) 的质因数,先线性筛,再试除法加上素数判断即可。

对于区间内质因数倍数的筛选,我们做容斥;

显然 \(n\le10^7\) 能包括的不同质因数很少,我们可以二进制枚举选取的因数,然后做容斥筛选得到和 \(n\) 不互质的数的数目;

区间长度减去这个数目即和 \(n\) 互质的数的数目,可以直接计算答案。

思路2

但是提交之后会 WA,因为你无法证明你考虑贡献时,第二条性质是否恒成立。

若第二条性质成立,当且仅当 \(j\) 是个质数或者是个恰好和 \(i,n\) 互质的数;

当区间长度很短时,不能总是保证找到 \(j\)\(i = 2184,n = 2200\)是个很好的例子;

\([1,\ 10^7]\) 中,最大的质数相邻的间隔在 \([150,200]\) 左右,这意味着与 \(n\) 距离超过200的点我们肯定可以找到一个质数;

那只要对距离n不到200的点暴力算一下最短路就行了,防止出现既没有质数也没有和 \(i,n\) 互质的数的情况。

完整代码

#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
typedef pair<ll, ll> pll;

const ll mod = 1e9 + 7;
const ll inf = 1e7;
const int N = 1e7 + 10;

vector<ll> prime;
bool isprime[N];
void getprime() {
    memset(isprime, true, sizeof isprime);
    isprime[1] = false;
    for (ll i = 2; i <= inf; ++i) {
        if (isprime[i]) prime.push_back(i);
        for (ll j = 0; j < prime.size() && prime[j] * i <= inf; ++j) {
            isprime[i * prime[j]] = false;
            if (i % prime[j] == 0) break;
        }
    }
}

ll gcd(ll a, ll b) {
    if (b > a) swap(a, b);
    if (b == 0) return a;
    return gcd(b, a % b);
}

inline void solve() {
    ll l, r, n; cin >> l >> r >> n;
    vector<ll> a;
    for (ll i = 2; i * i <= n; ++i) {
        if (n % i == 0) {
            if (isprime[i]) a.push_back(i);
            if (i * i != n && isprime[n / i]) a.push_back(n / i);
        }
    }
    const ll len = 200;
    ll pos = max(l, n - len), div = 0;
    if (pos <= r) {
        vector<ll> dis(n - pos + 1, inf);
        dis[n - pos] = 0;
        for (ll i = n - 1; i >= pos; --i) {
            for (ll j = i + 1; j <= n; ++j) {
                dis[i - pos] = min(dis[i - pos],
                    gcd(i, j) + dis[j - pos]);
            }
        }
        for (ll i = pos; i <= r; ++i) {
            div += dis[i - pos] - (gcd(i, n) == 1 ? 1 : 2);
        }
    }

    int tot = a.size();
    ll res = 0;
    for (int i = 1; i < (1 << tot); ++i) {
        ll cnt = 0, cur = 1;
        for (int j = 0; j < tot; ++j) {
            if (((i >> j) & 1)) cur *= a[j], cnt++;
            if (cur > r) break;
        }
        if (cnt % 2 == 1) res += (r / cur) - ((l - 1) / cur);
        else res -= (r / cur) - ((l - 1) / cur);
    }
    cout << 2ll * res + (r - l + 1 - res) + div << "\n";
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);cout.tie(0);
    int T = 1;
    cin >> T;
    getprime();
    while (T--)
        solve();
    return 0;
}

L - Lazy Shuffling

题意

对于一个长度为 \(n\) 的排列 \(A = [A_{1}, A_{2}, . . . , A_{n}]\),用另一个长度为 \(n\) 的 排列 \(p = [p_{1}, p_{2}, . . . , p_{n}]\)打乱成 \([A_{p_{1}} , A_{p_{2}} , . . . , A_{p_{n}} ]\) 问有多少个 \(A\) 能最大化打乱前后逆序对数量之差的绝对值?结果关于 \(998 244 353\) 取模。

  • \(n \le 22\)

思路1

首先理解最大化:打乱之后的逆序对数要么最大要么最小;

发现直接理解很难,手玩一下第一组样例:

3
3 1 2

可以列出共 \(6\) 个排列及其打乱后的结果以及绝对值之差:

A = 123			A' = 312		fx = |2 - 0| = 2
A = 132			A' = 213		fx = |3 - 1| = 2
A = 213			A' = 321		fx = |1 - 1| = 0
A = 231			A' = 123		fx = |0 - 2| = 2
A = 312			A' = 231		fx = |2 - 2| = 0
A = 321			A' = 132		fx = |1 - 3| = 2

可以观察到:

对于一组 \(1 \le i \lt j \le n\) :

  • \(p_i \lt p_j\) :
    • \(A_{p_i} > A_{p_j}\) : 则 \(A_{p_i}\)\(A_{p_j}\) 原先就是逆序对,打乱后相对位置不变,对逆序对个数没有贡献;
    • \(A_{p_i} < A_{p_j}\) : 同理,相对位置没有变化,打乱前后都是顺序对;
  • \(p_i \gt p_j\) :
    • \(A_{p_i} > A_{p_j}\) : 则 \(A_{p_i}\)\(A_{p_j}\) 原先不是逆序对,打乱后,两者互为逆序对,贡献 \(+1\)
      • 由于 \(i \lt j\) , 新排列中顺序会是 \(A_{p_i} , A_{p_j}\) ;而原排列中有 \(p_i \gt p_j\) ,顺序会是 \(A_{p_j} , …… , A_{p_i}\) ;
    • \(A_{p_i} < A_{p_j}\) : 同理,反过来理解就行,贡献会 \(-1\);

基于上述观察,我们可以总结出:

​ 由于原排列和新排列中 逆序对差值的绝对值 是上述贡献的总和,那么这个总和其实只和 \(p\) 中的逆序对有关。

思路 2

得到上述结论,我们考虑怎么让逆序对差值的绝对值(记作\(F\)) 最大化;

显然,只要上述贡献都增或都减,即同方向,即可最大化\(F\)

问题就转化为了:

  • 对于 \(p\) 中所有的逆序对 \((i, j) (1 ≤ i < j ≤ n)\),要么 都有 \(A_i < A_j\) ,要么都有 \(A_i > A_j\) 的排列 \(A\) 的个数

相当于给一张 \(n\) 阶有向完全图的一些边的拓扑序,求不同的拓扑序的个数;

\(n\) 很小,提示我们状压表示选择的点集来 \(dp\) 出答案;

考虑一个确定了拓扑序的子集 \(S\) 对应的答案是 \(dp[S]\) ,我们可以选择一个不在此集合中,且与 \(S\) 中没有确定关系的点 \(u\) ,则我们可以将 \(dp[S]\) 转移 到 \(dp[S + {u}]\) 中去。

随后可以观察到 \(A_i < A_j\)\(A_i > A_j\) 的图只是已知关系的方向相反,答案 \(*2\) 即可;

注意,如果 \(p\) 原先就有序,显然会重复计算,需要特判;

完整代码

inline void solve() {
  int n;  cin >> n;
  vector<int> p(n + 1, 0);
  vector<ll> dp((1ll << n), 0), to(n + 1, 0);
  for (int i = 0; i < n; ++i) cin >> p[i];

  bool f = 1;
  for (int i = 0; i < n; ++i) {
    for (int j = i + 1;j < n;++j) {
      if (p[i] > p[j]) {
        f = 0;
        to[i] = (to[i] | (1 << j)); // 记确定拓扑序
      }
    }
  }

  if (f) {
    ll prod = 1;
    for (ll i = 1; i <= n;++i) {
      prod = (prod * i) % mod;
    }
    cout << prod;
    return;
  }

  ll tot = (1ll << n); dp[0] = 1;
  for (ll i = 0; i < tot;++i) {
    for (ll j = 0; j < n;++j) {
      if (((i >> j) & 1ll) || ((to[j] & i) != to[j])) continue;
      ll t = (i ^ (1ll << j));
      dp[t] = (dp[t] + dp[i]) % mod;
    }
  }
  cout << (dp[tot - 1] * 2) % mod;
}
posted @ 2026-07-25 00:49  MisaUdon  阅读(3)  评论(0)    收藏  举报