题解 | 练习赛 Round 149
A.调色题Ⅰ
假设我们将原始颜色编号 从小到大排序,得到序列
,其中
。
设最终变换后的颜色序列为 。为了使总代价
最小,且满足
互不相同及
的约束,最优策略应保证
的相对顺序与
一致,即
。
对于序列中的每一个位置 ,其最终颜色
必须满足两个限制条件:
。
- 对于
,必须满足
。
综上,为了使 尽可能小,我们可以得到递推关系:
最终的最小操作总次数即为:。
时间复杂度:。
void solve() {
int n;
cin >> n;
i64 ans = 0;
vector<int> a(n + 1);
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
sort(a.begin() + 1, a.end());
for (int i = 1; i <= n; i++) {
ans += max(a[i - 1] + 1 - a[i], 0);
a[i] = max(a[i - 1] + 1, a[i]);
}
cout << ans << "\n";
return;
}
B.调色题Ⅰ
定义打完一轮后的总能量净变化为:
定义 为在第一轮中,打出编号为
的牌之前的能量值:
- 对于
,
-
首先判断
的情况,必须满足两个限制条件:
- 能够成功打出前
张牌,即对于所有
,均满足
。
- 一轮后的能量净变化
。
- 能够成功打出前
-
对于编号为
的牌,其能够被成功打出的最大轮数
满足:
这表示该编号的牌在最后一次可以被打出的是第
轮,所以答案小于
。此时,它对总出牌数的贡献上限为
:
最终的答案即
:
时间复杂度:。
void solve() {
int n;
i64 x;
cin >> n >> x;
i64 res = x;
vector<int> a(n + 1), b(n + 1);
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
for (int i = 1; i <= n; i++) {
cin >> b[i];
}
for (int i = 1; i <= n; i++) {
res -= a[i];
if (res >= 0) {
res += b[i];
} else {
cout << i - 1 << "\n";
return;
}
}
i64 ch = x - res;
if (ch <= 0) {
cout << "Infinity\n";
return;
}
i64 ans = 1e18;
for (int i = 1; i <= n; i++) {
i64 sum = (x - a[i]) / ch;
ans = min(ans, (sum + 1) * n + i - 1);
x += b[i] - a[i];
}
cout << ans << "\n";
return;
}
C.调色题Ⅱ
考虑对问题进行建图求解。我们将每一个数值视作图中的一个节点。对于初始数组中的每一个 ,它可以经过一次调色操作变为
,这等价于在图上存在一条从节点
连向节点
的有向边,且边权为
。
题目要求所有方格最终的颜色编号完全相同,这等价于我们要寻找一个全局目标节点 ,使得所有的初始起点
都能到达
,且使得总代价
达到最小。
考虑到任何数通过 操作产生的状态必然是该数自身的约数,所以真正有用的节点仅限于所有初始颜色
的约数集合。设值域上限为
,单个数的最大约数个数记为
,则总的有效节点数最多为
个。我们只需要在这些节点构成的空间上运行 BFS(广度优先搜索)求解最短路即可。
时间复杂度: 。
constexpr int N = 5e5;
void solve() {
int n, m;
cin >> n >> m;
vector<int> a(n + 1), b(m + 1);
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
for (int i = 1; i <= m; i++) {
cin >> b[i];
}
vector<int> dis(N + 1, 1e9);
vector<int> sum(N + 1), cnt(N + 1);
for (int i = 1; i <= n; i++) {
vector<int> vec;
queue<int> q;
q.push(a[i]);
dis[a[i]] = 0;
cnt[a[i]]++;
while (!q.empty()) {
int x = q.front();
q.pop();
vec.push_back(x);
for (int j = 1; j <= m; j++) {
int gd = gcd(x, b[j]);
if (dis[gd] == 1e9) {
dis[gd] = dis[x] + 1;
sum[gd] += dis[gd];
cnt[gd]++;
q.push(gd);
}
}
}
for (auto x : vec) {
dis[x] = 1e9;
}
}
int ans = 1e9;
for (int i = 1; i <= N; i++) {
if (cnt[i] == n) {
ans = min(ans, sum[i]);
}
}
cout << (ans == 1e9 ? -1 : ans) << "\n";
return;
}
D.鼠鼠题
只要当前攻击力 足以击败区间左侧或右侧的 Jerry,贪心地扩张区间显然是最优策略。
通过 ST 表结合二分查找,在每一轮中快速找到向左和向右满足 的最远可达边界。
这种扩张过程最多只会持续 轮,
为值域。若当前攻击力
因为小于左侧或右侧某个位置的体力值
而停止扩张(即
),那么当新的攻击力
时,新的攻击力
满足:
由于 ,且
,可以得出:
这意味着,每一次二分查找都会使攻击力至少翻倍,最多只会二分查找 次。
时间复杂度: 。
void solve() {
int n, q;
cin >> n >> q;
vector<int> a(n + 1);
vector<i64> pre(n + 1);
for (int i = 1; i <= n; i++) {
cin >> a[i];
pre[i] = pre[i - 1] + a[i];
}
ST<int> st(a);
while (q--) {
i64 pos, x;
cin >> pos >> x;
if (a[pos] > x) {
cout << 0 << "\n";
continue;
}
x += a[pos];
int L = pos, R = pos;
bool flag = 1;
while (flag) {
flag = 0;
if (L != 1) {
int l = 1, r = L - 1;
while (l < r) {
int mid = l + r >> 1;
if (st.range(mid, L - 1) > x) {
l = mid + 1;
} else {
r = mid;
}
}
if (st.range(l, L - 1) <= x) {
x += pre[L - 1] - pre[l - 1];
L = l;
flag = 1;
}
}
if (R != n) {
int l = R + 1, r = n;
while (l < r) {
int mid = l + r + 1 >> 1;
if (st.range(R + 1, mid) > x) {
r = mid - 1;
} else {
l = mid;
}
}
if (st.range(R + 1, l) <= x) {
x += pre[l] - pre[R];
R = l;
flag = 1;
}
}
}
cout << R - L + 1 << "\n";
}
return;
}
E.代码题
考虑转化成求期望 。
设 表示当前能触发更新的集合为
时,后续期望的更新次数。
枚举第一次更新的元素 ,由于每个元素被选中的概率均为
,转移方程如下:
注意到只有当 时,状态才会有有效的后续转移。因此,我们只需要计算
在
到
范围内的
值。当
时,
。
对于转移方程的这部分求和 ,有
,使用一个变量维护即可。
时间复杂度: 。
void solve() {
int n;
cin >> n;
i64 ans = 1;
for (int i = 1; i < n; i++) {
int m = (n - 1) / i;
vector<i64> dp(m + 2);
i64 suf = 0;
for (int j = m; j >= 1; j--) {
if (i * j + 1 <= m) {
suf = (suf + dp[i * j + 1]) % mod;
} else if (i * j + 1 <= n) {
suf = (suf + 1) % mod;
}
dp[j] = (1 + comb.inv(n - j + 1) * suf) % mod;
}
ans = (ans + dp[1]) % mod;
}
ans = ans * comb.fac(n) % mod;
cout << ans << "\n";
return;
}
F.数数题
可以观察到,每次操作后的结果只与当前操作数 的奇偶性有关。记序列
中奇数有
个,偶数有
个。
设 为某个特定序列
操作后结果为奇数的操作序列数量,
为操作后结果为偶数的操作序列数量。
定义 为
个奇数和
个偶数的所有排列情况的
之和;
定义 为
个奇数和
个偶数的所有排列情况的
之和。
由于同奇偶性的数字之间可以任意互换位置,最终答案为 。
思路一
考虑先求出结果为奇数的方案数,再用总方案数进行容斥。
考虑最后一次操作和最后一个数 的奇偶性:
- 如果最后一个数
是奇数:无论前
个数计算出的结果
是奇是偶,我们总有且仅有一种操作(加法或乘法)能让最终结果变为奇数。因此,只要确定最后一个数是奇数,前面的排列和操作方式可以任意选择。这部分的方案数为
。
- 如果最后一个数
是偶数:要让最终结果为奇数,最后一次操作必须使用加法,且前面的计算结果
必须是奇数。这等价于在少了一个偶数的序列中求奇数结果的方案数,即
。
所以有递推式:,不断展开递推即可。
思路二
对于边界情况 或者
有:
对于 且
,考虑状态转移,有如下递推:
为了解出上述递推,设以下辅助变量:
将原递推式代入可得 的递推关系:
又因为和差关系:
将 展开代入
中,可以推导出:
对于 且
,进而解得
和
:
时间复杂度: 。
void solve() {
int n;
cin >> n;
int odd = 0, even = 0;
for (int i = 1; i <= n; i++) {
int x;
cin >> x;
if (x & 1) {
odd++;
} else {
even++;
}
}
if (odd == 0) {
cout << qpow(2, even - 1) * comb.fac(even) % mod << "\n";
return;
}
i64 ans = (odd == 1 ? mod - inv(2) : 0);
for (int i = 0; i <= even; i++) {
ans = (ans + qpow(2, odd + i - 2) * comb(odd + i, i)) % mod;
}
ans = ans * comb.fac(odd) % mod * comb.fac(even) % mod;
cout << ans << "\n";
return;
}
G.猜猜题
任意两个区间要么相离,要么存在包含关系,我们可以将每个区间 挂载在其所包含的最小区间
下,从而构建出森林,考虑进行树上dp。
设 表示在以
为根的子树中,共选择了
个区间,且区间
的确定状态为
:
:区间
内所有变量均已知,选择
个区间的方案数。
:区间
内恰好有一个变量未知,选择
个区间的方案数。
对于节点 的所有子节点
,转移如下:
然后考虑是否选择节点 :
- 若
此时选择
不会提供额外信息,而不选
仍能保持状态:
- 若
此时必须选择
:
设 为恰好在选择
个区间时完全确定数组的方案数。由于每次抽取是等概率的,总的抽取方案数为
。
记 为在
次询问内完成确定的概率。则 Alice 恰好在第
次询问后停止的概率为:
最终期望为:
注意判断无解的情况。
时间复杂度: 。
vector<i64> operator*(const vector<i64> &a, const vector<i64> &b) {
vector<i64> c(a.size() + b.size() - 1, 0);
for (int i = 0; i < a.size(); ++i) {
for (int j = 0; j < b.size(); ++j) {
c[i + j] = (c[i + j] + a[i] * b[j]) % mod;
}
}
return c;
}
vector<i64> operator+(const vector<i64> &a, const vector<i64> &b) {
vector<i64> c(max(a.size(), b.size()), 0);
for (int i = 0; i < a.size(); ++i) {
c[i] = (c[i] + a[i]) % mod;
}
for (int i = 0; i < b.size(); ++i) {
c[i] = (c[i] + b[i]) % mod;
}
return c;
}
void solve() {
int n, m;
cin >> n >> m;
vector<pair<int, int>> a(m);
for (auto &[l, r] : a) {
cin >> l >> r;
}
ranges::sort(a, [](auto x, auto y) {
x.second = -x.second;
y.second = -y.second;
return x < y;
});
vector<vector<int>> tree(m);
vector<int> vis(m);
stack<int> stk;
for (int i = 0; i < m; ++i) {
while (!stk.empty() && a[stk.top()].second < a[i].first) {
stk.pop();
}
if (!stk.empty()) {
tree[stk.top()].push_back(i);
}
stk.push(i);
}
bool flag = 0;
auto dfs = [&](auto dfs, int u) -> pair<vector<i64>, vector<i64>> {
vis[u] = 1;
auto [l, r] = a[u];
int len = r - l + 1, sum = 0;
for (auto v : tree[u]) {
auto [l, r] = a[v];
sum += r - l + 1;
}
vector<i64> f0{1}, f1{0};
for (auto v : tree[u]) {
auto [g0, g1] = dfs(dfs, v);
f1 = f1 * g0 + f0 * g1;
f0 = f0 * g0;
}
if (len == sum) {
f0 = f0 * vector<i64>{1, 1} + f1 * vector<i64>{0, 1};
} else if (len == sum + 1) {
f1 = f0;
f0 = f0 * vector<i64>{0, 1};
} else {
flag = 1;
}
return make_pair(f0, f1);
};
vector<i64> res{1};
i64 sum = 0;
for (int i = 0; i < m; i++) {
if (vis[i]) {
continue;
}
auto [l, r] = a[i];
sum += r - l + 1;
auto [f0, f1] = dfs(dfs, i);
res = res * f0;
}
if (sum != n || flag) {
cout << -1 << "\n";
return;
}
i64 ans = 0;
for (int i = m; i >= 1; i--) {
res[i] = res[i] * inv(comb(m, i)) % mod;
}
for (int i = m; i >= 1; i--) {
res[i] = (res[i] - res[i - 1] + mod) % mod;
}
for (int i = 0; i <= m; i++) {
ans = (ans + i * res[i]) % mod;
}
cout << ans << "\n";
return;
}
组合数
constexpr i64 mod = 998244353;
i64 qpow(i64 a, i64 b) {
i64 res = 1;
while (b) {
if (b & 1) {
res = res * a % mod;
}
a = a * a % mod;
b >>= 1;
}
return res;
}
i64 inv(i64 a) { return qpow(a, mod - 2); }
struct Comb {
vector<i64> _fac, _invfac;
int n;
Comb() : n(0), _fac(1, 1), _invfac(1, 1) {}
Comb(int n) : Comb() { init(n); }
void init(int m) {
if (m <= n) return;
_fac.resize(m + 1);
_invfac.resize(m + 1);
for (int i = n + 1; i <= m; i++) {
_fac[i] = _fac[i - 1] * i % mod;
}
_invfac[m] = ::inv(_fac[m]);
for (int i = m; i > n; i--) {
_invfac[i - 1] = _invfac[i] * i % mod;
}
n = m;
}
i64 fac(int m) {
if (m > n) init(2 * m);
return _fac[m];
}
i64 invfac(int m) {
if (m > n) init(2 * m);
return _invfac[m];
}
i64 inv(int m) {
return invfac(m) * fac(m - 1) % mod;
}
i64 operator()(int n, int m) {
if (m < 0 || n < m) {
return 0;
}
return fac(n) * invfac(m) % mod * invfac(n - m) % mod;
}
i64 Lucas(i64 n, i64 m) {
if (m == 0) {
return 1;
}
return (*this)(n % mod, m % mod) * Lucas(n / mod, m / mod) % mod;
}
} comb;
ST表
template <typename T, typename Func = function<T(const T &, const T &)>>
struct ST {
vector<vector<T>> st;
Func func;
ST() = default;
ST(const vector<T> &v, Func func = [](const T &a, const T &b) {
return max(a, b);
}) : func(move(func)) {
int k = bit_width<unsigned>(v.size());
st.resize(k + 1, vector<T>(v.size()));
st[0] = v;
for (int i = 0; i < k; i++) {
for (int j = 0; j + (1 << (i + 1)) - 1 < v.size(); ++j) {
st[i + 1][j] = this->func(st[i][j], st[i][j + (1 << i)]);
}
}
}
T range(int l, int r) {
int t = __lg(r - l + 1);
return func(st[t][l], st[t][r + 1 - (1 << t)]);
}
};

京公网安备 11010502036488号