2024初秋集训——提高组 #37
A. 子集和问题
题目描述
给定数列 \(A_1,A_2,\dots,A_N\)。对于 \(\forall 0\le k\le N\),求出:
- 有多少种集合 \(S\) 满足 \(S\in\{1,2,\dots,N\}\and\exist T\subseteq S\and |T|=|S|-k \and \sum \limits_{i\in T} A_i \ge M\)。
思路
由于最优情况下 \(T\) 一定是 \(S\) 去掉 \(k\) 个最小的得到的,所以我们可以将 \(A\) 从大到小排序。
令 \(dp_{i,j}\) 表示考虑前 \(i\) 个,\(\min(\sum \limits_{x\in T} A_x,M)=j\) 的方案数。我们可以统计出末尾为 \(i\) 且总和 \(\ge M\) 的 \(T\) 的数量,最后 \(N-i\) 个元素可以随便选,使用组合数计算答案即可。
时空复杂度均为 \(O(NM)\)。
代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 3001, MAXM = 3001, MOD = 998244353;
int n, m, a[MAXN], dp[MAXN][MAXM], ans[MAXN], f[MAXN], inv[MAXN], Inv[MAXN];
void work() {
inv[1] = f[0] = Inv[0] = 1;
for(int i = 1; i <= n; ++i) {
f[i] = 1ll * f[i - 1] * i % MOD;
inv[i] = (i > 1 ? 1ll * (MOD - MOD / i) * inv[MOD % i] % MOD : 1);
Inv[i] = 1ll * Inv[i - 1] * inv[i] % MOD;
}
}
int C(int a, int b) {
return 1ll * f[a] * Inv[b] % MOD * Inv[a - b] % MOD;
}
int main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> n >> m;
work();
for(int i = 1; i <= n; ++i) {
cin >> a[i];
}
sort(a + 1, a + n + 1, greater<int>());
dp[0][0] = 1;
for(int i = 1; i <= n; ++i) {
int res = 0;
for(int j = 0; j <= m; ++j) {
res = (res + (j + a[i] >= m) * dp[i - 1][j]) % MOD;
dp[i][min(m, j + a[i])] = (dp[i][min(m, j + a[i])] + dp[i - 1][j]) % MOD;
dp[i][j] = (dp[i][j] + dp[i - 1][j]) % MOD;
}
for(int j = 0; j <= n - i; ++j) {
ans[j] = (ans[j] + 1ll * C(n - i, j) * res % MOD) % MOD;
}
}
for(int i = 0; i <= n; ++i) {
cout << ans[i] << "\n";
}
return 0;
}
B. 完美挑战
题目描述
有 \(N\) 个关卡,通关第 \(i\) 个关卡需要 \(t_i\) 秒。通过第 \(i\) 关后:
- 有 \(\frac{a_i}{b_i}\) 的概率,你完美通过了该关。
- 有 \(1-\frac{a_i}{b_i}\) 的概率,你没有完美通过该关,你需要从新开始这个游戏。
你可以决定通关关卡的顺序。求最优策略下完成挑战的期望时间。
思路
对于每两个关卡 \(x,y\),我们考虑把哪个放在前面更优:
- 顺序为 \(x,y\)。期望时间为 \(\frac{b_x}{a_x}\cdot \frac{b_y}{a_y}\cdot t_x+\frac{b_y}{a_y}\cdot t_y\),因为你每次完美通关 \(x\) 期望 \(\frac{b_x}{a_x}\) 次,完美通关 \(y\) 期望 \(\frac{b_y}{b_x}\),而你要通关 \(y\) 之前必须完美通关 \(x\)。
- 顺序为 \(y,x\)。期望时间为 \(\frac{b_y}{a_y}\cdot \frac{b_x}{a_x}\cdot t_y+\frac{b_x}{a_x}\cdot t_x\)。
按照哪个放在前面更优排序,并做递推:
- 令 \(x\) 为完美通关 \(i-1\) 所需的期望时间,那么完美通关 \(i\) 的期望时间为 \(\frac{b_i}{a_i}\cdot (x+t_i)\)。
空间复杂度 \(O(N)\),时间复杂度 \(O(N\log N)\)。
代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 200001, MOD = 998244353;
struct Node {
int t, a, b;
}s[MAXN];
int n, ans;
int Pow(int a, int b) {
int ret = 1;
for(; b; a = 1ll * a * a % MOD, b >>= 1) {
if(b & 1) {
ret = 1ll * ret * a % MOD;
}
}
return ret;
}
int main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> n;
for(int i = 1; i <= n; ++i) {
cin >> s[i].t >> s[i].a >> s[i].b;
}
sort(s + 1, s + n + 1, [](const Node &a, const Node &b) -> bool {
return 1.0l * b.b / b.a * b.t + 1.0l * b.b / b.a * a.b / a.a * a.t < 1.0l * a.b / a.a * a.t + 1.0l * a.b / a.a * b.b / b.a * b.t;
});
for(int i = 1; i <= n; ++i) {
ans = 1ll * s[i].b * Pow(s[i].a, MOD - 2) % MOD * (ans + s[i].t) % MOD;
}
cout << ans;
return 0;
}
C. 线段树入门
题目描述
定义函数 \(\text{build}(u,l,r)\) 表示建立一个以 \(u\) 为根,对应区间 \([l,r]\) 的线段树。
\(\text{build}(u,l,r)\) 步骤如下:
- 若 \(l=r\),则 \(u\) 为叶子结点,退出函数。
- 否则,我们添加边 \(u\rightarrow 2u,2u+1\)。令 \(m=\lfloor \frac{l+r}{2}\rfloor\),递归到函数 \(\text{build}(2u,l,m),\text{build}(2u+1,m+1,r)\)。
请求出,如果我们调用了 \(\text{build}(1,1,n)\),那么 \(\sum \limits_{S\in\{1,2,\dots,N\}} \text{LCA}(S)\) 会是多少。
思路
令 \(dp_{x}\) 表示 \(\text{build}(1,1,x)\) 的答案,这里有一个问题,我们无法直接进行转移,因为这里根结点的编号变了。所以我们记 \(s_x\) 表示长度为 \(x\) 令根结点编号加一的贡献。
这样转移就很好做了。令 \(a=\lceil \frac{x}{2}\rceil,b=\lfloor \frac{x}{2}\rfloor,v=(2^a-1)\cdot(a^b-1)\),那么有转移 \(s_x=2s_a+2s_b+v,dp_x=dp_a+dp_b+s_a+2s_b+v\)。
空间复杂度 \(O(\log N)\),时间复杂度 \(O(\log^2 N)\)。
代码
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int MOD = 998244353;
int t;
ll n;
map<ll, int> dp, s;
int Pow(int a, ll b) {
int ret = 1;
for(; b; a = 1ll * a * a % MOD, b >>= 1) {
if(b & 1) {
ret = 1ll * ret * a % MOD;
}
}
return ret;
}
int DP(ll n) {
if(dp.count(n)) {
return dp[n];
}
ll x = (n + 1) / 2, y = n - x, v = 1ll * (Pow(2, x) - 1 + MOD) * (Pow(2, y) - 1 + MOD);
dp[n] = (0ll + DP(x) + DP(y) + s[x] + 2ll * s[y] + v) % MOD;
s[n] = (2ll * s[x] + 2ll * s[y] + v) % MOD;
return dp[n];
}
void Solve() {
cin >> n;
cout << DP(n) << "\n";
}
int main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
dp[1] = s[1] = 1;
for(cin >> t; t--; Solve()) {
}
return 0;
}
D. 关押囚犯
题目描述
有 \(2N\) 个囚犯要关押到两个监狱中,每座监狱要关押恰好 \(N\) 个囚犯,有 \(M\) 对囚犯不和:
- 第 \(i\) 对不和的囚犯为 \(a_i,b_i\),若这两名囚犯在同一个监狱中,那么混乱度将上升 \(2^i\)。
请给出一种关押囚犯的方案,使得混乱度尽可能小。
思路
由于这道题的混乱度为 \(2^i\),所以可以倒着枚举每个条件依次尝试。避免矛盾可以用种类并查集维护,而难点就是每个监狱恰好 \(N\) 个囚犯。我们考虑用背包的方式处理:
- 如果有一个连通块左右分别有 \(x,y\) 个点,那么可以看作是一个价值为 \(|x-y|\) 的物品,因为其中 \(\min(x,y)\) 的部分对答案的贡献是固定的,所以可以统一处理。
- 当我们尝试加入一个关系时,我们可以先撤销连接的两个并查集的贡献,再加入合在一起的贡献。
- 如何撤销并查集呢?由于背包是有交换律的,所以倒过来做就行了。
处理完关系后再跑一边背包即可。
空间复杂度 \(O(N)\),时间复杂度 \(O(N^2)\)。
代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 20001, MAXM = 1000001, MOD = 998244353;
int n, m, a[MAXM], b[MAXM], f[MAXN], sz[MAXN], cnt[MAXN], sum, dp[MAXN], fa[MAXN], _fa[MAXN];
bool vis[MAXN], flag[MAXN];
int getfa(int u) {
return (f[u] == u ? u : f[u] = getfa(f[u]));
}
void Merge(int u, int v) {
u = getfa(u), v = getfa(v);
if(u != v) {
if(sz[u] > sz[v]) {
swap(u, v);
}
f[u] = v, sz[v] += sz[u], cnt[v] += cnt[u];
}
}
void Insert(int a, int b) {
if(a < b) {
swap(a, b);
}
sum += b, a -= b;
if(!a) {
return;
}
for(int i = n; i >= a; --i) {
dp[i] = (dp[i] + dp[i - a]) % MOD;
}
}
void Erase(int a, int b) {
if(a < b) {
swap(a, b);
}
sum -= b, a -= b;
if(!a) {
return;
}
for(int i = a; i <= n; ++i) {
dp[i] = (dp[i] - dp[i - a] + MOD) % MOD;
}
}
int main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> n >> m;
iota(f + 1, f + 4 * n + 1, 1);
fill(sz + 1, sz + 4 * n + 1, 1);
fill(cnt + 1, cnt + 2 * n + 1, 1);
for(int i = 1; i <= m; ++i) {
cin >> a[i] >> b[i];
}
dp[0] = 1;
for(int i = 1; i <= 2 * n; ++i) {
Insert(1, 0);
}
for(int i = m; i >= 1; --i) {
if(getfa(a[i]) == getfa(b[i] + 2 * n) || getfa(a[i]) == getfa(b[i])) {
continue;
}
int u = getfa(a[i]), v = getfa(b[i]), _u = getfa(a[i] + 2 * n), _v = getfa(b[i] + 2 * n);
Erase(cnt[u], cnt[_u]), Erase(cnt[v], cnt[_v]), Insert(cnt[u] + cnt[_v], cnt[v] + cnt[_u]);
if(sum <= n && dp[n - sum]) {
Merge(u, _v), Merge(v, _u);
}else {
Erase(cnt[u] + cnt[_v], cnt[v] + cnt[_u]);
Insert(cnt[u] + cnt[v], cnt[_u] + cnt[_v]);
Merge(u, v), Merge(_u, _v);
}
}
dp[0] = 1;
for(int i = 1; i <= n; ++i) {
dp[i] = 0;
}
for(int i = 1; i <= 2 * n; ++i) {
if(!flag[getfa(i)]) {
int u = getfa(i), v = getfa(i + 2 * n);
flag[getfa(i)] = flag[getfa(i + 2 * n)] = 1;
int w = abs(cnt[u] - cnt[v]);
if(!w) {
continue;
}
for(int j = n; j >= w; --j) {
if(!dp[j] && dp[j - w]) {
dp[j] = 1, fa[j] = u, _fa[j] = v;
}
}
}
}
int pos = n - sum;
for(; pos; ) {
if(cnt[fa[pos]] < cnt[_fa[pos]]) {
swap(fa[pos], _fa[pos]);
}
vis[fa[pos]] = 1;
pos -= cnt[fa[pos]] - cnt[_fa[pos]];
}
for(int i = 1; i <= 2 * n; ++i) {
if(vis[getfa(i)] || vis[getfa(i + 2 * n)]) {
continue;
}
vis[cnt[getfa(i)] < cnt[getfa(i + 2 * n)] ? getfa(i) : getfa(i + 2 * n)] = 1;
}
for(int i = 1; i <= 2 * n; ++i) {
cout << vis[getfa(i)];
}
return 0;
}