Codeforces Round 1086 (Div. 2) 题解

Codeforces Round 1086 (Div. 2) 题解

我打赢复活赛了!

Problem A. Bingo Candies

给你一个 \(n\times n\) 的数字矩阵。求将数字重排后,是否存在一种重排方案使得任意一行或者一列的数字种类大于 \(2\)

猜结论的题。只要任何一种数字数量都不超过 \(n(n-1)\) 就够了。

int n;
int a[200][200];

void solve() {
  int n;
  cin >> n;
  std::vector<int> cnt(n *n  + 10);
  int mx = 0;
  for (int i = 1, x; i <= n * n; i++) {
    cin >> x, cnt[x]++;
    upmax(mx, cnt[x]);
  }
  cout << (mx <= (n * (n - 1)) ? "Yes" : "No") << endl;
}

signed main() {
  cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
  int t;
  cin >> t;
  while (t--) solve();
  return 0;
}

Problem B. Cyclists

初始有 \(n\) 张牌,第 \(i\) 张牌有一个打出代价 \(a_i\)。每次可以任意打出前 \(k\) 张牌中的任意一张,打出后将牌放到队尾。求总代价不超过 \(m\) 的情况下,第 \(p\) 张牌最多能打出多少次。

比较考验眼力劲儿。

我一开始读错题了,以为牌在哪个位置就需要花费 a[i] 的代价……

如果 win-condition card(胜利条件卡)本来就在前 k 位,那么很显然直接打出即可。

如果不在,那么贪心地想,你需要打出 p-k 张其他的牌来将胜利条件卡挤到最前面。

通过手玩会发现,无论牌的顺序如何,最优的情况一定是打出最小的 p-k 张牌,并且一定可以打出这 p-k 张最小牌。

同理可以得到打出牌之后的情况,情况同上这里不再赘述。

int n, k, p, m;
int a[kMaxN];
int b[kMaxN];
int pre[kMaxN];

void solve() {
  cin >> n >> k >> p >> m;
  for (int i = 1; i <= n; i++) cin >> a[i];
  int cst = a[p];
  if (p > k) {
    std::sort(a + 1, a + p);
    int need = p - k;
    for (int i = 1; i <= need; i++) cst += a[i];
  }
  if (cst > m) return cout << 0 << endl, void();
  m -= cst, cst = a[p];
  if (n > k) {
    std::swap(a[p], a[n]);
    std::sort(a + 1, a + n);
    int need = n - k;
    for (int i = 1; i <= need; i++) cst += a[i];
  }
  cout << 1 + m / cst << endl;
}

signed main() {
  cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
  int t;
  cin >> t;
  while (t--) solve();
  return 0;
}

Problem C. Stamina and Tasks

初始体力 \(S=1\)。有 \(n\) 个任务,不可以调换任务顺序。
你可以选择执行任务,这样获得 \(S \times c_i\) 的收益并且体力变为 \(S \times (1-\frac{p_i}{100})\)
如果不执行任务,那么什么都不会发生,并走到下一个任务。

正着做很难写,状态涉及到收益和体力。

但是倒着做就很显然了,对于当前的总收益 \(t\),如果你选择执行第 \(i\) 个任务,那么收益会变成 \(t \times (1-\frac{p_i}{100}) + c_i\)。因为体力减少是乘累计的,这样就可以打包计算所有后续收益。

int n, S;
int p[kMaxN], c[kMaxN];

void solve() {
  S = 1;
  cin >> n;
  for (int i = 1; i <= n; i++) cin >> c[i] >> p[i];
  double ans = 0;
  for (int i = n; i >= 1; i--) {
    double c = ::c[i];
    double p = 1.0 - (::p[i] / 100.0);
    upmax(ans, c + p * ans);
  }
  printf("%.10lf\n", ans);
}

signed main() {
  cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
  int t;
  cin >> t;
  while (t--) solve();
  return 0;
}

Problem D. Tree Orientation

给你一个矩阵 \(s\),其中 \(s_{i, j}\) 表示 \(i\) 是否可以到达 \(j\)。要求构造出一个有向树,使得其满足这种到达关系,或者判定不存在。
两个难度仅仅是 \(n\) 范围内不同,一个 \(500\), 一个 \(8000\)

为了方便起见,点 \(u\) 可到达的点集合记作 \(S_u\),可以到达 \(u\) 的点集合记作 \(T\)

考虑树边 \((u, v)\) 是否存在,有两种判定方式:

  • 不存在一个点 \(k\),使得 \(u\) 可以到达 \(k\) 并且 \(k\) 可以到达 \(v\)。这样 \(u\)\(v\) 就是直接相连。

  • 树边存在的一个必要条件就是 \(v\) 的可到达集合一定是 \(u\) 可到达集合的子集。同时不存在另外一个点 \(k\),使得 \(S_v \subset S_k \subset S_u\),此时 \((u, v)\) 树边一定存在。

第一种直接写可以写出 \(n^3\) 的判定,看上去可能会 TLE。(嗯没错我一开始就 TLE 了)所以可以使用 bitset 优化一下,看 \(S_u\)\(T_v\) 的交集是否为 \(2\)

第二种判定方法看上去和第一种没有本质区别,但是它可以引出一个贪心:
\(S_u\) 中的所有点 \(p\) 按照 \(|S_p|\) 的大小从大往下排。这样当你遍历时,原先遍历的点越有可能是 \(u\) 的儿子。一旦你确定 \(p\)\(u\) 的儿子,那么将 \(S_p\) 中所有点打上标记记作已经是子树节点,这样后续遍历的时候直接跳过即可。
容易知道 \(u\) 所有儿子的 \(S_i\) 一定是互不相交的。
这里可能会引出一个问题:如果某个 \(p \in S_u\),但是 \(S_p \not\subset S_u\) 呢?说明原树不存在,交给后续判定。

确定的树边一定是 \(n-1\) 并且在无向图下所有点联通,否则原树不存在。

然后 \(O(n^2)\) 基于选定树边构造 \(S^\prime\) 就可以了。

不过这里有个关于异或哈希的小巧思:为每一个点 \(p\) 赋予一个随机权值 \(val_p\)。如果要判定 \(S_i\) 是否等于 \(S^\prime_i\),直接将集合内所有点的值异或起来然后判断是否相等就可以了。由于只转移异或值,会相当好转移。这样能将判定的复杂度降到 \(O(n)\)。(当然没什么卵用就是了)

(我说是一开始算错了复杂度以为链会退化到 \(O(n^3)\) 你信吗。)

#include <algorithm>
#include <iostream>
#include <random>
#include <string>
#include <vector>

using std::cerr;
using std::cin;
using std::cout;

const char endl = '\n';
const int kMaxN = 8000 + 100;

int rand(int l, int r) {
  static std::random_device seed;
  static std::mt19937_64 e(seed());
  std::uniform_int_distribution<int> rd(l, r);
  return rd(e);
}

template <class type>
void upmin(type& a, const type& b) {
  a = std::min(a, b);
}

template <class type>
void upmax(type& a, const type& b) {
  a = std::max(a, b);
}

// 如果 u 可以直接到达 v,那么 v 的到达集合一定是 u 的子集
// 所以贪心就可以得到直接出边

int n;
std::string a[kMaxN];
int sum[kMaxN];
int val[kMaxN];
std::vector<int> go[kMaxN];
bool used[kMaxN];
int fa[kMaxN];

int find(int x) {
  return fa[x] == x ? x : fa[x] = find(fa[x]);
}

int mem[kMaxN];

int qry(int u) {
  if (mem[u]) return mem[u];
  mem[u] = val[u];
  for (auto v : go[u]) {
    mem[u] ^= qry(v);
  }
  return mem[u];
}

void solve() {
  cin >> n;
  for (int i = 1; i <= n; i++) go[i].clear();
  for (int i = 1; i <= n; i++) used[i] = false;
  for (int i = 1; i <= n; i++) mem[i] = 0;
  for (int i = 1; i <= n; i++) {
    cin >> a[i], a[i] = '#' + a[i];
    sum[i] = 0;
    for (int j = 1; j <= n; j++) {
      if (a[i][j] - '0') sum[i] ^= val[j], go[i].push_back(j);
    }
  }
  for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= n; j++) {
      if (i == j && a[i][j] == '0') return cout << "No" << endl, void();
      if (i != j && a[i][j] == '1' && a[j][i] == '1') return cout << "No" << endl, void();
    }
  }
  std::vector<std::pair<int, int>> edge;
  for (int i = 1; i <= n; i++) fa[i] = i;
  for (int u = 1; u <= n; u++) {
    std::vector<int> id;
    for (auto v : go[u]) {
      if (v == u) continue;
      id.push_back(v);
    }
    std::sort(id.begin(), id.end(), [](int i, int j) {
      return go[i].size() == go[j].size() ? i < j : go[i].size() > go[j].size();
    });
    for (auto v : id) {
      if (used[v]) continue;
      used[v] = true;
      edge.push_back({u, v});
      fa[find(u)] = find(v);
      for (auto j : go[v]) used[j] = true;
    }
    for (auto v : id) used[v] = false;
  }
  if (edge.size() != n - 1) return cout << "No" << endl, void();
  int ccnt = 0;
  for (int i = 1; i <= n; i++) ccnt += fa[i] == i, go[i].clear();
  if (ccnt > 1) return cout << "No" << endl, void();
  for (auto& [u, v] : edge) go[u].push_back(v);
  for (int i = 1; i <= n; i++) {
    // cout << qry(i) << ' ' << sum[i] << endl;
    if (qry(i) != sum[i]) return cout << "No" << endl, void();
  }
  cout << "Yes" << endl;
  for (auto& [u, v] : edge) cout << u << ' ' << v << endl;
}

signed main() {
  cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
  for (int i = 0; i < kMaxN; i++) val[i] = rand(1, 1e9);
  int t;
  cin >> t;
  while (t--) solve();
  return 0;
}

Problem E. Counting Cute Arrays

对于一个序列 \(X\),定义它为可爱的序列,当且仅当存在一个序列 \(A\),可以通过如下步骤定义一个函数 \(f(A)\) 生成 \(f(A)=X\)

  • 对于任意 \(i\),定义 \(X_i=j\) 时要求 \(j < i\) 并且 \(A_j < A_i\)。如果有多个满足条件的 \(j\),则选取最靠右的 \(j\)

给定一个 \(X\),如果 \(X_i=-1\) 则表示这个位置待定。问有多少种 \(X\) 是可爱的。

\(n\leq 5000\)

\(X\) 是可爱的序列的充要条件是:

  • \(\forall i, X_i < i\)
  • 不存在 \(j < i\),使得 \(X_j < X_i < j < i\)

第一个比较显然,对于第二个条件的必要性也是很显然的:

  • 如果存在这样的 \(j < i\),那么 \(A_{X_j} < A_j, A_{X_i} > A_j\),但是又 \(A_{X_i} < A_i\),所以 \(X_i\) 应该为 \(j\),矛盾。所以不存在。

至于第二个条件的充分性,可以手玩发现对于一个给定的 \(X\),在满足第二个条件的情况下一定可以构造出一个满足条件的 \(A\) 使得 \(f(A)=X\)。具体流程不赘述。

这个充要命题的抽象是必要的。可以将 \((X_i, i)\) 视作一个区间,容易发现这个条件就是要求任意区间只能是包含、相离的关系。当然,端点相交是可以的。

对于一个确定的 \((X_i, i)\),可以知道未赋值的 \(X_j, j > i\) 是无论如何都无法将 \(X_j\) 复制到 \((x_i, i)\) 里面的。所以对于一个边界确定的子区间,它的计数是独立且不受外界影响的。

于是我们考虑去 \(dp\) 一个边界确定的 \((X_i, i)\) 来计数符合要求的条件数。容易知道只有 \(j \in [X_i + 1, i - 1]\)\(X_j\) 是可以被修改的,且左端点不能超过 \(X_i\)

当我们 \(dp\) 处理 \((X_i, i)\) 时,会有若干被完全包裹的边界确定的子区间,这些区间应当当作子问题处理后再转移。比较值得一提的是,这些完全包裹的关系可以构成一颗树。

  • 这个包裹关系需要处理好,我们举一个例子,当你手上有三个区间:\(0:(1, 10), 1:(1, 5), 2:(1, 3)\) 时,显然构成的关系是 \(0->1->2\),在处理 \(0\)\(dp\) 转移时,你的左端点可以选取到 \(5\) 但是绝对不能选取到 \(4\)

除去子区间以外剩余需要被重新赋值的 \(X_p\),由于我们需要知道有哪些左端点可以被选取,而可以被选定的左端点数量比较适合作为状态进行转移,于是可以记状态 \(dp[p][c]\)\(p\) 赋值完后、剩余 \(c\) 个端点可以被当作左端点进行选取。

  1. 如果 \(p\) 是某个子区间内的点,显然不应该由当前问题去考虑转移,而是交给独立子问题去处理。

  2. \(p\) 作为某个直接子区间的右端点时,它显然无法被赋值,但是依旧可以担任后续赋值的左端点,此时状态转移:

\[dp[p - 1][c] \to dp[p][c + 1] \]

  1. 剩余一般情况,对于 \(dp[p - 1][c]\),它可以转移到 \(\forall c^\prime < c, dp[p][c^\prime + 1]\) 中。因为你的赋值使得中间可选左端点变得不再可选,所以能够如此转移。比如 \(c=4\) 时,有四个左端点 \(T_1, T_2, T_3, T_4\),如果你选择 \(T_1\) 作为左端点,那么其余三个在后续转移中将不再可用。同时 \(p\) 本身也会在后续中担任左端点作用,所以要 \(+1\)

于是这个题就写完了。

#include <iostream>
#include <algorithm>
#include <random>
#include <vector>

using std::cerr;
using std::cin;
using std::cout;

const char endl = '\n';
const int kMaxN = 5e3 + 100;

int rand(int l, int r) {
  static std::random_device seed;
  static std::mt19937_64 e(seed());
  std::uniform_int_distribution<int> rd(l, r);
  return rd(e);
}

template <class type>
void upmin(type& a, const type& b) {
  a = std::min(a, b);
}

template <class type>
void upmax(type& a, const type& b) {
  a = std::max(a, b);
}

const int MOD = 998244353;

struct modint {
  int val;

   modint(long long v = 0) {
    if (v < 0) v = v % MOD + MOD;
    if (v >= MOD) v %= MOD;
    val = (int)v;
  }

  explicit operator int() const { return val; }

  friend  modint qpow(modint a, long long p) {
    modint ans(1);
    while (p > 0) {
      if (p & 1) ans *= a;
      a *= a, p >>= 1;
    }
    return ans;
  }

   modint inv() const { return qpow(*this, MOD - 2); }

   modint& operator+=(modint b) {
    val += b.val;
    if (val >= MOD) val -= MOD;
    return *this;
  }
   modint& operator-=(modint b) {
    val -= b.val;
    if (val < 0) val += MOD;
    return *this;
  }
   modint& operator*=(modint b) {
    val = (int)(1LL * val * b.val % MOD);
    return *this;
  }
   modint& operator/=(modint b) { return *this *= b.inv(); }

  friend  modint operator+(modint a, modint b) { return a += b; }
  friend  modint operator-(modint a, modint b) { return a -= b; }
  friend  modint operator*(modint a, modint b) { return a *= b; }
  friend  modint operator/(modint a, modint b) { return a /= b; }
};

int n;
int dp[kMaxN][kMaxN];
int a[kMaxN];

struct line {
  int l, r;
  bool operator<(const line& b) const {
    if (l != b.l) return l < b.l;
    return r > b.r;
  }
};

void solve() {
  cin >> n;
  bool flag = true;
  for (int i = 1; i <= n; i++) {
    cin >> a[i];
    if (a[i] >= i) flag = false;
  }
  for (int i = 1; i <= n; i++) {
    for (int j = 1; j < i; j++) {
      if (a[i] == -1 || a[j] == -1) continue;
      // 判定是否出现交叉情况
      if (a[j] < a[i] && a[i] < j) flag = false;
    }
  }
  if (flag == false) return cout << 0 << endl, void();
  // 左端点可能有很多个,但是右端点是一定的
  std::vector<line> s;
  std::vector<int> stk;
  s.push_back({0, n + 1});
  for (int i = 1; i <= n; i++) {
    if (~a[i]) s.push_back({a[i], i});
  }
  std::sort(s.begin(), s.end());
  std::vector<std::vector<int>> go(s.size() + 1);
  for (int i = 0; i < s.size(); i++) {
    while (!stk.empty() && s[stk.back()].r < s[i].r) stk.pop_back();
    if (!stk.empty()) go[stk.back()].push_back(i);
    stk.push_back(i);
  }

  auto dfs = [&](auto& self, int u) -> modint {
    // cerr << u << endl;
    modint ans1 = 1;
    for (auto v : go[u]) ans1 *= self(self, v);
    int L = s[u].l, R = s[u].r, len = R - L + 1;
    std::vector<modint> dp(len + 2, 0);
    dp[1] = 1;
    std::vector<bool> inChild(R + 2, false), Right(R + 2, false);
    for (auto v : go[u]) {
      Right[s[v].r] = true;
      for (int p = s[v].l + 1; p <= s[v].r - 1; p++) inChild[p] = true;
    }
    for (int p = L + 1; p <= R - 1; p++) {
      if (inChild[p]) continue;
      std::vector<modint> nxt(len + 2, 0);
      if (Right[p]) {
        for (int c = 1; c <= len; c++) nxt[c + 1] = dp[c];
      } else {
        modint sum = 0;
        for (int c = len; c >= 1; c--) {
          sum += dp[c];
          nxt[c + 1] = sum;
        }
      }
      dp = std::move(nxt);
    }
    modint ans = 0;
    for (int c = 1; c <= len; c++) ans += dp[c];
    return ans1 * ans;
  };
  cout << (int)dfs(dfs, 0) << endl;
}

signed main() {
  cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
  int t;
  cin >> t;
  while (t--) solve();
  return 0;
}
posted @ 2026-03-19 19:33  sudoyc  阅读(61)  评论(0)    收藏  举报