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;
}
posted @ 2024-10-17 21:02  Yaosicheng124  阅读(5)  评论(0编辑  收藏  举报