「比赛日记」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\);
- 若 \(A_{p_i} > A_{p_j}\) : 则 \(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;
}

浙公网安备 33010602011771号