题解:ZROI3323 [25noip十连测day2] Demon

题解:ZROI3323 [25noip十连测day2] Demon

题目描述

Demon | Zhengrui Online Judge

题面

输入回车确认或点击

等待操作...

题解

我们将这个问题倒过来考虑,即把问题改成删边,求 \(X\) 的总和。然后分为三个部分:边双的答案、连通图的答案、一般图的答案。

准备

显然我们 dp 的时候需要同时记录方案数和 \(\sum X\),我们可以用一个结构体同时存储,并重载相关运算符。

struct node {
  mint sum, cnt;
  node operator*(const mint& rhs) const {
    return {sum * rhs, cnt * rhs};
  }
  node operator*(const node& rhs) const {
    return {sum * rhs.cnt + rhs.sum * cnt, cnt * rhs.cnt};
  }
  friend node extend(const node& lhs, const mint& rhs, int times = 1) {
    return {lhs.sum + rhs * lhs.cnt * times, lhs.cnt};
  }
  node& operator+=(const node& rhs) {
    sum += rhs.sum, cnt += rhs.cnt;
    return *this;
  }
  friend node operator+(node lhs, const node& rhs) { return lhs += rhs; }
  friend node operator*(const mint& rhs, const node& lhs) { return lhs * rhs; }
};

\(\newcommand\extend{\operatorname{extend}}\extend\):注意到 \(\extend(kA,c)=k\extend(A,c)\)

边双

\(f_{n,m}\) 表示 \(n\) 个点 \(m\) 条边的边双连通分量的答案(node 类型)。当我们删去最后一条边时,有两种情况:

  1. 仍然是边双。

\(\extend(\binom{n+1}2 f_{n,m-1},B)\) 转移过来。

  1. 成为一条链,链由至少两个边双连接起来。

这里就需要背包。令 \(b_{n,m}\) 是这个背包,则转移:

\[b_{n,m}=\sum_{x,y}\binom n x\binom m y x^2yf_{x,y-1}\extend(b_{n-x,m-y} + (n-x)^2f_{n - x,m - y}, A) \]

注意看清楚,我们需要定这条新链的链头代表的边双,所以需要定这个边双的点和边,以及它向下一个边双连的边的编号和方案数(链上的边的方案数是 \(\prod_i x_i^2\)\(x_1,x_2,\cdots,x_k\) 分别是链上的边双的大小,所以乘一个 \(x^2\) 的系数)以及它删除时带来的 \(A\) 的贡献。由于链必须由至少两个边双构成,所以需要手动从 \(f\) 那边转移一下。

最后 \(b_{n,m-1}/2\) 转移到 \(f_{n,m}\),因为我们 dp 的链是有方向的,会算重两遍。

有一些问题

  • 什么时候是 \(\binom myy\),什么时候是 \(\binom my\)?例如边双的第一种情况没有额外乘上 \(m\),第二种情况却乘上了 \(y\)

当我们要求这条边是最后加入的时候,它的编号就已经确定了。在背包部分里,我们不确定链上的边是什么时候删除的,所以要额外确定它的编号。

连通图

我这里的想法和题解有一点点区别。令 \(g_{n,m}\) 表示 \(n\) 个点 \(m\) 条边的连通图的答案(node 类型)。我们肯定要在圆方树上考虑。当我们删去最后一条边时,有三种情况:

  1. 边是圆方树的树边,删除后分裂成两个连通块。

这种情况最简单,\(\sum_{x,y}\binom{n-1}{x-1}\binom {m-1}{y-1}x(n-x)g_{x,y-1}g_{n-x,m-y}\) 转移到 \(g_{n,m}\) 就行了。也是注意,这条树边的编号是已经确定的,不需要分配。

  1. 边在边双中,删除后仍然是边双。
  2. 边在边双中,删除后边权被分裂成一条链。

第三种情况非常棘手,我们不希望处理它。我们这样做:考虑 \(g_{n,m-1}\) 代表的所有圆方树和他们对应的答案,考虑往上面加边,转移到 \(g_{n,m}\) 去。我们将原来的贡献和加边带来的贡献分开算。原来的贡献自然是 \(\binom {n+1}2g_{n,m-1}\)。加边带来的贡献,有两种:

  1. 加边加到边双里面,贡献是 \(B\)
  2. 加边加到两个边双之间,贡献是 \(C\)

好消息,加边方案数是确定的 \(\binom{n+1}2\),我们可以求出其中一种的贡献方案数,再求出另一种。当然是 \(B\) 的系数更简单,它是 \(\sum_{i}\binom{x_i+1}2\),我们可以求出 \(B\) 的系数,然后算出 \(C\) 的系数。或者更直击本质的方案是令 \(C=0\),将 \(C\neq 0\) 的情况转化为 \(C=0\),这很简单。

\(\sum_{i}\binom{x_i+1}2\) 要求我们知晓所有边双的大小,我们被迫做树形背包。树形背包用 \(f\) 拼成一棵树,需要记录方案数和 \(\sum_i\binom{x_i+1}2\),这个其实和我们的 node 类型非常适配,我们可以直接挪用。

做树形背包的过程需要知道连树边的系数,它是 \(\prod_{i}x_i^{deg_i}\)。也就是说我们需要知道每个点的度数和大小,选一个记到状态里。那当然是记度数。令背包 \(b_{n,m,j}\),表示根有 \(j\) 个儿子,但是我们不把根的边双的点数和边数算进去。再令一个辅助数组 \(b'_{n,m}\) 表示把根也算进去,并认为根的头上有一条树边。

\[b_{n,m,j}=\sum_{x,y}\binom{n-1}{x-1}\binom myyb_{n-x,m-y}b'_{x,y-1} \]

注意,为了去重,我们在这里钦定编号最小的结点在新加入的 \(b'_{x,y-1}\) 中。\(b'\) 的计算:

\[b'_{n,m}=\sum_{x,y,j}\binom nx\binom my x^{j+1}b_{n-x,m-y,j}\extend\left(\text{node}\left(0, cnt[f_{x,y}]\right),\binom{x+1}2\right) \]

注意手动去除 \(f\) 过来的 \(\sum X\) 并改为 \(\sum_i \binom{x_i+1}{2}\)。然后就是这里不用钦定编号最小的结点在根上,它头上的边让 \(b\) 转移的时候再分配编号,我们只需要为这条边预留一个 \(x\) 的系数。

最后拼成一棵树:

\[\sum_{x,y,j}\binom{n-1}{x-1}\binom{m-1}{y-1}x^jb_{n-x,m-y,j}\extend\left(\text{node}\left(0, cnt[f_{x,y-1}]\right),\binom{x+1}2\right) \]

这里钦定编号最小的点在根的边双中,防止一棵树算结点个数次,我们不需要这样。然后还要手动删掉一条边,所以 \(m,y\) 都减一。

拼完之后 \(g_{n,m}\)\(\sum X\) 加上 \(sum[tree]\times B\) 就行了。如果没有发现可以将 \(C\) 定为 \(0\) 的技巧,也不会因此坠机(真的吗?没试过)。

一般图

\(h_{n,m}\) 表示 \(n\) 个点 \(m\) 条边的一般图的答案(node 类型)。

\[h_{n,m}=\sum_{x,y}h_{n-x,m-y}g_{x,y}\binom{n-1}{x-1}\binom my \]

注意钦定编号最小的结点在新加入的连通块中。

代码实现

请注意常数优化(?)。有一些没说的边界自己看吧!

#include <bits/stdc++.h>
using namespace std;
#ifdef LOCAL
#define debug(...) fprintf(stderr, __VA_ARGS__)
#else
#define endl "\n"
#define debug(...) void(0)
#endif
using LL = long long;
template <unsigned umod>
struct modint {/*{{{*/
  static constexpr int mod = umod;
  unsigned v;
  modint() = default;
  template <class T, enable_if_t<is_integral<T>::value, int> = 0>
    modint(const T& y) : v((unsigned)(y % mod + (is_signed<T>() && y < 0 ? mod : 0))) {}
  modint& operator+=(const modint& rhs) { v += rhs.v; if (v >= umod) v -= umod; return *this; }
  modint& operator-=(const modint& rhs) { v -= rhs.v; if (v >= umod) v += umod; return *this; }
  modint& operator*=(const modint& rhs) { v = (unsigned)(1ull * v * rhs.v % umod); return *this; }
  modint& operator/=(const modint& rhs) { assert(rhs.v); return *this *= qpow(rhs, mod - 2); }
  friend modint operator+(modint lhs, const modint& rhs) { return lhs += rhs; }
  friend modint operator-(modint lhs, const modint& rhs) { return lhs -= rhs; }
  friend modint operator*(modint lhs, const modint& rhs) { return lhs *= rhs; }
  friend modint operator/(modint lhs, const modint& rhs) { return lhs /= rhs; }
  template <class T> friend modint qpow(modint a, T b) {
    modint r = 1;
    for (assert(b >= 0); b; b >>= 1, a *= a) if (b & 1) r *= a;
    return r;
  }
  friend int raw(const modint& self) { return self.v; }
  friend ostream& operator<<(ostream& os, const modint& self) { return os << raw(self); }
  explicit operator bool() const { return v != 0; }
  modint operator-() const { return modint(0) - *this; }
  bool operator==(const modint& rhs) const { return v == rhs.v; }
  bool operator!=(const modint& rhs) const { return v != rhs.v; }
};/*}}}*/
using mint = modint<998244353>;
template <int N>
struct C_prime {
  mint fac[N + 1], ifac[N + 1];
  C_prime() {
    fac[0] = 1;
    for (int i = 1; i <= N; i++) fac[i] = fac[i - 1] * i;
    ifac[N] = 1 / fac[N];
    for (int i = N; i >= 1; i--) ifac[i - 1] = ifac[i] * i;
  }
  mint operator()(int n, int m) const { return n >= m && m >= 0 ? fac[n] * ifac[m] * ifac[n - m] : 0; }
};
C_prime<1000010> binom;
struct node {
  mint sum, cnt;
  node operator*(const mint& rhs) const {
    return {sum * rhs, cnt * rhs};
  }
  node operator*(const node& rhs) const {
    return {sum * rhs.cnt + rhs.sum * cnt, cnt * rhs.cnt};
  }
  friend node extend(const node& lhs, const mint& rhs, int times = 1) {
    return {lhs.sum + rhs * lhs.cnt * times, lhs.cnt};
  }
  node& operator+=(const node& rhs) {
    sum += rhs.sum, cnt += rhs.cnt;
    return *this;
  }
  friend node operator+(node lhs, const node& rhs) { return lhs += rhs; }
  friend node operator*(const mint& rhs, const node& lhs) { return lhs * rhs; }
};
constexpr int N = 60, inv2 = (mint::mod + 1) / 2;
int T, A, B, C;
node f[N][N];
void solve1(int _n, int _m) { // ECC
  static node b[N][N];
  f[1][0] = {0, 1};
  for (int m = 1; m <= _m; m++) f[1][m] = extend(f[1][m - 1], B);
  for (int n = 2; n <= _n; n++) {
    for (int m = 0; m <= _m; m++) {
      for (int x = 1; x <= n; x++) {
        for (int y = 1; y <= m; y++) {
          b[n][m] += binom(n, x) * binom(m, y) * y * qpow(mint(x), 2) * f[x][y - 1] * extend(b[n - x][m - y] + f[n - x][m - y] * qpow(mint(n - x), 2), A);
        }
      }
      if (m) f[n][m] = inv2 * b[n][m - 1] + extend(f[n][m - 1] * binom(n + 1, 2), B);
    }
  }
}
int praw(mint x) {
  return min(raw(x), raw(x) - mint::mod, [](int x, int y) { return abs(x) < abs(y); });
}
node g[N][N];
void solve2(int _n, int _m) { // connected
  static node ws[N][N][N], ws2[N][N];
  static mint pw[N][N];
  for (int i = 1; i <= _n; i++) {
    pw[i][0] = 1;
    for (int j = 1; j <= _n + 1; j++) pw[i][j] = pw[i][j - 1] * i;
  }
  g[1][0] = {0, 1};
  ws[0][0][0] = {0, 1};
  for (int n = 1; n <= _n; n++) {
    for (int m = 0; m <= _m; m++) {
      node sum = {};
      for (int x = 1; x <= n; x++) {
        for (int y = 1; y <= m; y++) {
          sum += g[x][y - 1] * g[n - x][m - y] * binom(n - 1, x - 1) * binom(m - 1, y - 1) * x * (n - x);
        }
      }
      g[n][m] += extend(sum, A); // 合并连通块        
      mint coe;
        for (int x = 1; x <= n; x++) {
          for (int y = 1; y <= m; y++) {
      for (int j = 1; j <= n - x + 1; j++) {
            coe = binom(n - 1, x - 1) * binom(m, y) * y;
            ws[n][m][j] += ws[n - x][m - y][j - 1] * coe * ws2[x][y - 1];
          }
        }
      }
        for (int x = 1; x <= n; x++) {
          for (int y = 0; y <= m; y++) {
      for (int j = 0; j <= n - x; j++) {
            coe = binom(n, x) * binom(m, y) * pw[x][j + 1];
            ws2[n][m] += extend(ws[n - x][m - y][j] * node{0, f[x][y].cnt} * coe, binom(x + 1, 2));
            if (y) {
              coe = binom(n - 1, x - 1) * binom(m - 1, y - 1) * pw[x][j];
              auto tree_ws = extend(ws[n - x][m - y][j] * coe * node{0, f[x][y - 1].cnt}, binom(x + 1, 2));
              // 注意,强制钦定 1 号点在根上        
              g[n][m].sum += tree_ws.sum * B; // 唐的没边了        
            }
          }
        }
      }
      if (m) g[n][m] += g[n][m - 1] * binom(n + 1, 2);
      if (g[n][m].cnt) debug("g[%d][%d] = {%d, %d}\n", n, m, praw(g[n][m].sum), raw(g[n][m].cnt));
    }
  }
}
node h[N][N];
void solve3(int _n, int _m) { // all
  static node b[N][N];
  b[0][0] = {0, 1};
  for (int n = 1; n <= _n; n++) {
    for (int m = 0; m <= _m; m++) {
      for (int x = 1; x <= n; x++) {
        for (int y = 0; y <= m; y++) {
          b[n][m] += b[n - x][m - y] * g[x][y] * binom(n - 1, x - 1) * binom(m, y);
        }
      }
      h[n][m] += b[n][m];
      if (h[n][m].cnt) debug("h[%d][%d] = {%d, %d}\n", n, m, praw(h[n][m].sum), raw(h[n][m].cnt));
      assert(h[n][m].cnt == qpow(mint(n * (n + 1) / 2), m));
    }
  }
}
int main() {
#ifndef LOCAL
  cin.tie(nullptr)->sync_with_stdio(false);  
#endif
  int n, m;
  cin >> T >> n >> m >> A >> B >> C;
  A -= C, B -= C;
  solve1(n, m);
  solve2(n, m);
  solve3(n, m);
  if (T == 1) cout << extend(f[n][m], C, m).sum << endl;
  else cout << extend(h[n][m], C, m).sum << endl;
  return 0;
}


posted @ 2025-09-09 16:34  caijianhong  阅读(63)  评论(0)    收藏  举报