[ZJOI2022] 树

[ZJOI2022] 树

一些经典的 dp 手法。

考虑这个题目在讲什么,每个点都要朝左右两边连各一条有向边,限制是一个点要么左边没有入边要么右边没有入边,但不能两边同时没有入边。

发现没法转化,考虑硬做。设 \(f_{i, j, k, l}\) 表示考虑前 \(i\) 个点,有 \(j\) 条向右的有向边终点待定,有 \(k\) 个点必须接受以后的向左的入边,\(l\) 个点可以接受而不必须接受。有朴素的 \(O(n^5)\) 的 dp。

发现后面两维很冗余,考虑怎么干掉这两维。发现关键在于维护必须和非必须,启发我们考虑容斥,必须接 = 随便接 - 不接。这样设 \(f_{i, j, k}\) 其中 \(k\) 表示有 \(k\) 个点是随便接不接的。代码如下:

f[1][1][1] = 1;
for(int i = 2; i <= n; ++i) {
  for(int j = 1; j < i; ++j) {
    for(int k = 0; k < i; ++k) if(f[i - 1][j][k]) {
      int w = 1ll * f[i - 1][j][k] * k % P;
      add(f[i][j + 1][k + 1], w);
      sub(f[i][j + 1][k], w);
      for(int l = 1; l <= j; ++l)
        add(f[i][j - l + 1][k], 1ll * binom[j][l] * w % P);
    }
  }
  for(int j = 0; j <= i; ++j)
    add(F[i], f[i][1][j]);
  cout << F[i] << '\n';
}

写完后发现这时候第二维转移是 \(O(n)\) 的,导致最终复杂度变成了 \(O(n^4)\)。考虑把这部分也优化掉。有一个经典手法,就是在每个点就钦定好向右连边的终点,我们只需维护目前需要多少个终点,支持新增一个终点即可。总复杂度 \(O(n^3)\)

int main() {
  cin >> n >> P;
  f[1][1][1] = 1;
  for(int i = 2; i <= n; ++i) {
    int ans = 0;
    for(int j = 0; j <= n - i + 1; ++j)
      for(int k = 0; k < i; ++k) if(f[i - 1][j][k]) {
        int w = 1ll * f[i - 1][j][k] * k % P;
        add(f[i][j - 1][k], 1ll * w * (j - 1) % P);
        if(j == 1) add(ans, w);
        add(f[i][j][k], 1ll * w * j % P);
        add(f[i][j][k + 1], 1ll * w * j % P);
        add(f[i][j + 1][k + 1], 1ll * w * (j + 1) % P);
        sub(f[i][j][k], 1ll * w * j % P);
        sub(f[i][j + 1][k], 1ll * w * (j + 1) % P);
      }
    cout << ans << '\n';
  }
}
posted @ 2024-02-21 22:00  DCH233  阅读(12)  评论(0编辑  收藏  举报