DP 练习

P6189 [NOI Online #1 入门组] 跑步

先考虑两种暴力 dp。

  1. \(f_{i,j}\) 表示可重集内最大元素 \(\le i\),总和为 \(j\) 的方案数。
    有转移 \(f_{i,j}=f_{i-1,j}+f_{i,j-i}\)
    答案为 \(ans=f_{n,n}\)
  2. \(g_{i,j}\) 表示可重集内总共有 \(i\) 个元素,总和为 \(j\) 的方案数。
    考虑将图像画出来,如下:

    对于这种阶梯状物,考虑一种非常经典的计数方法。
    用以下两种操作去生成一个阶梯状物:
    加入一个长为 \(j\) 的条和整体长度全部加 \(1\)
    有转移 \(g_{i,j}=g_{i-1,j-1}+g_{i-1,j-i}\)
    答案为 \(ans=\sum_{i=1}^n g_{i,n}\)

不难发现,第一种做法的瓶颈在于最大元素的值,第二种做法的瓶颈在于元素的个数。
考虑根号分治,将可重集分为两部分。令 $B=\left \lfloor \sqrt n\right \rfloor $。第一部分只包含 \(<B\) 的元素,第二部分只包含 \(\ge B\) 的部分。

对于第一部分,时间复杂度为 \(\Theta(nB)\)

对于第二部分,将每次加 \(1\) 改成每次加 \(B\),转移方程式改为 \(g_{i,j}=g_{i-1,j-B}+g_{i-1,j-i}\)

答案为 \(ans=f_{B-1,s}\times\sum_{i=1}^{\left \lfloor \frac n B\right \rfloor} g_{i,n-s}\)

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int kN = 1e5 + 7;
const int kM = 320;
int n, p;
void Add(int &x, int y){(x += y) >= p ? x -= p : 0;}
int add(int x, int y){Add(x, y); return x;}
int f[kM][kN], g[kM][kN], ans;
signed main(){
  ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  cin >> n >> p;
  int B = ceil(sqrt(n));
  f[0][0] = g[0][0] = 1;
  for(int i = 1; i < B; i++){
    for(int j = 0; j <= n; j++){
      Add(f[i][j], f[i - 1][j]);
      if(j >= i) Add(f[i][j], f[i][j - i]);
    }
  }
  for(int i = 1; i <= n / B; i++){
    for(int j = B; j <= n; j++){
      Add(g[i][j], g[i - 1][j - B]);
      if(j >= i) Add(g[i][j], g[i][j - i]);
    }
  }
  for(int i = 0; i <= n; i++){
    for(int j = 0; j <= n / B; j++){
      Add(ans, 1ll * f[B - 1][i] * g[j][n - i] % p);
    }
  }
  cout << ans << '\n';
  return 0;
}

CF1608F MEX counting

很深刻的题。

题目给的限制为前缀 \(mex\) 的取值范围,令 \(c_i\) 表示 \([1,i]\) 的前缀 \(mex\)

考虑从前往后插入排列,发现对于 \(i,j(j<i)\),若 \(a_j > c_i\),则 \(a_j\) 的值对 \(c_i\) 没有影响。

那么可以 延迟决策,如果当前的 \(a_j\)\(c_i\) 没有影响,则先填上一个 \(?\) 号,等到有影响的时候在决定。

这里我们考虑记录 \(?\) 号的种类数,因为值相同的 \(?\) 号只需要考虑最前面的值取得是什么。

思考

记录 \(?\) 号的个数有什么不便之处,怎么做?

于是状态出来了,令 \(f_{i,j,l}\) 表示到第 \(i\) 个位置,\([1,i]\) 的前缀 \(mex\)\(j\),前面有 \(l\)\(?\) 号的方案数。

转移:

\(j=j_2\)\(f_{i+1,j_2,l}\gets f_{i,j,l-1}+f_{i,j,l}\times(l+j)\)
\(j<j_2\)\(f_{i+1,j_2,l}\gets f_{i,j,l+j_2-j-1}\times A_{l+j_2-j-1}^{j_2-j-1}\)

答案为 \(ans=\sum_{i=0}^n \sum_{c_n} f_{n,c_n,i}\times A_{n-c_n}^{i}\)

上面的做法为 \(\Theta(n^2 k^2)\)

考虑如何优化。

发现上面做法的瓶颈在于第二种转移。发现 \(A_{l+j_2-j-1}^{j_2-j-1}\) 可以写成 \(\frac{(l+j_2-j-1)!}{l!}\),将 \(\frac 1 {l!}\) 提出来变成 \(\frac 1 {l!} \times f_{i,j,l+j_2-j-1}\times (l+j_2-j-1)!\)

\(p=l+j_2-1\),上面式子可以继续写成 \(\frac 1 {l!} \times f_{i,j,p-j}\times (p-j)!\)

不难发现 \(j\)\(p-j\) 的和为与 \(j\) 无关的值,可以用前缀和优化。

\(g_{j,l}\) 表示 \(\sum_{l'=0}^l f_{i,l,j-l}\times(j-l)!\)

\(j\) 的范围为 \([l_j,r_j]\) 则上面的式子可以接着写成 \(\frac 1 {l!} \times (g_{p,r_j}-g_{p,l_j-1})\),于是复杂度被我们压到了 \(\Theta(n^2 k)\)

点击查看代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll mod = 998244353;
const int kN = 2e3 + 7;
const int K = 53;
void Add(ll &x, ll y){(x += y) >= mod ? x -= mod : 0;}
ll add(ll x, ll y){Add(x, y); return x;}
int n, k, b[kN];
ll f[2][K << 1][kN], g[kN + (K << 1)][K << 1], jc[kN + (K << 1)], jcinv[kN + (K << 1)], ans;
ll Pow(ll a, ll b){
  a %= mod;
  ll res = 1;
  for(; b; b >>= 1){
    if(b & 1) res = res * a % mod;
    a = a * a % mod;
  }
  return res;
}
ll A(int n, int m){
  if(n < m) return 0ll;
  return jc[n] * jcinv[n - m] % mod;
}
ll C(int n, int m){
  if(n < m) return 0ll;
  return jc[n] * jcinv[n - m] % mod * jcinv[m] % mod;
}
signed main(){
  ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  cin >> n >> k;
  jc[0] = jcinv[0] = 1;
  int _n = n + k * 2;
  for(int i = 1; i <= _n; i++) jc[i] = jc[i - 1] * 1ll * i % mod;
  jcinv[_n] = Pow(jc[_n], mod - 2);
  for(int i = _n - 1; i >=  1; i--) jcinv[i] = jcinv[i + 1] * 1ll * (i + 1) % mod;
  for(int i = 1; i <= n; i++) cin >> b[i];
  f[0][k][0] = 1;
  for(int i = 0; i < n; i++){
    memset(f[1], 0, sizeof(f[1]));
    for(int j = 0; j <= n + 2 * k; j++){
      for(int l = 0; l <= min(j, 2 * k); l++){
        g[j][l] = f[0][l][j - l] * jc[j - l] % mod;
        if(l) Add(g[j][l], g[j][l - 1]);
      } 
    }
    for(int j = max(k - b[i], 0); j <= min(k + i - b[i], 2 * k); j++){
      int res = b[i] + j - k;
      if(b[i] + j - b[i + 1] >= max(k - b[i + 1], 0) && b[i] + j - b[i + 1] <= min(k + i + 1 - b[i + 1], 2 * k)){
        int j2 = b[i] + j - b[i + 1];
        for(int l = 0; l <= i; l++){
          Add(f[1][j2][l + 1], f[0][j][l]);
          Add(f[1][j2][l], f[0][j][l] * 1ll * (res + l) % mod);
        }
      }
    }
    for(int l = 0; l <= i + 1; l++){
      int L = max(k - b[i + 1], 0);
      int R = min(k + i + 1 - b[i + 1], 2 * k);
      for(int j2 = L; j2 <= R; j2++){
        int Lj = max(k - b[i], 0);
        int Rj = min(k + i - b[i], min(2 * k, b[i + 1] - b[i] + j2 - 1));
        int t = b[i + 1] - b[i] + j2;
        int P = l + t - 1;
        ll res = 0;
        if(Lj <= Rj) res = (g[P][Rj] - (Lj ? g[P][Lj - 1] : 0ll) + mod) % mod;
        Add(f[1][j2][l], jcinv[l] * res % mod);
      } 
    }
    swap(f[0], f[1]);
  }
  for(int j = 0; j <= 2 * k; j++){
    int res = b[n] + j - k;
    if(res < 0 || res > n) continue;
    for(int l = 0; l <= n; l++){
      Add(ans, f[0][j][l] * A(n - res, l) % mod);
    }
  }
  cout << ans << '\n';
  return 0;
}

P6453 [COCI 2008/2009 #4] PERIODNI

原来多边形转笛卡尔树是常见的。

发现从左往右一列一列的考虑是没有前途的,于是我们考虑从下到上考虑。

经典结论

对于一个 \(n\times m\) 的矩形,放入 \(k\) 个车,互不攻击的方案数是 \({n\choose k}\times {m\choose k} \times k!\)

我们模拟删矩形的过程,每次删除最下方的最大的矩形,然后递归到子问题。

本质上是高度从小往大删,应该想到笛卡尔树。

考虑以高度为权值,建小根笛卡尔树。

发现矩形的宽为节点子树的大小,长为当前节点与父节点的高度差。

接下来可以 \(dp\) 了,令 \(f_{x,i}\) 表示以 \(x\) 为根的子树,放 \(i\) 个数的合法的方案数。

对于点 \(x\) 有两种选择,即考虑当前矩形选或不选位置填数字。

为了方便转移,记 \(g_{x,i}\) 表示以 \(x\) 为根的子树,放 \(i\) 个数,且 \(x\) 这个矩形内不放数的合法的方案数。

不选: \(g_{x,i}=\sum_j f_{lson,j}\times f_{rson,i-j}\)

选:\(f_{x,i}=\sum_j g_{x,j}\times F(x,i-j)\)

其中 \(F(x,i-j)\) 表示 \(x\) 这个矩形选 \(i-j\) 个数的方案数,这个可以用最初的经典结论解决。

最后复杂度为 \(\Theta(n k^2)\)

点击查看代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int kN = 510;
const int kL = 1e6 + 7;
const ll mod = 1e9 + 7;
ll Pow(ll a, int b){
  a %= mod;
  ll res = 1;
  for(; b; b >>= 1){
    if(b & 1) res = res * a % mod;
    a = a * a % mod;
  }
  return res;
}
void Add(ll &x, ll y){(x += y) >= mod ? x -= mod : 0;}
ll add(ll x, ll y){Add(x, y); return x;}
ll jc[kL], jcinv[kL];
ll C(int n, int m){
  if(n < m) return 0ll;
  return jc[n] * jcinv[n - m] % mod * jcinv[m] % mod;
}
int stk[kN], top, ls[kN], rs[kN], l[kN], w[kN], n, k, h[kN];
ll f[kN][kN], g[kN][kN];
ll F(int x, int k){
  return C(l[x], k) * C(w[x], k) % mod * jc[k] % mod;
}
void dfs(int x, int fa){
  l[x] = h[x] - h[fa];
  w[x] = 1;
  if(ls[x]){
    dfs(ls[x], x);
    w[x] += w[ls[x]];
  }
  if(rs[x]){
    dfs(rs[x], x);
    w[x] += w[rs[x]];
  }
}
void DP(int x){
  if(ls[x]) DP(ls[x]);
  if(rs[x]) DP(rs[x]);
  for(int i = 1; i <= k; i++){
    for(int j = 0; j <= i; j++){
      Add(g[x][i], f[ls[x]][j] * f[rs[x]][i - j] % mod);
    }
  }
  for(int i = 1; i <= k; i++){
    for(int j = 0; j <= i; j++){
      w[x] -= j;
      Add(f[x][i], g[x][j] * F(x, i - j) % mod);
      w[x] += j;
    }
  }
}
signed main(){
  ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  jc[0] = jcinv[0] = 1ll;
  for(int i = 1; i < kL; i++) jc[i] = jc[i - 1] * 1ll * i % mod;
  jcinv[kL - 1] = Pow(jc[kL - 1], mod - 2);
  for(int i = kL - 2; i >= 1; i--) jcinv[i] = jcinv[i + 1] * 1ll * (i + 1) % mod;
  cin >> n >> k;
  for(int i = 1; i <= n; i++){
    cin >> h[i];
    int k = top;
    while(top && h[stk[top]] > h[i]) top--;
    if(top < k) ls[i] = stk[top + 1];
    if(top) rs[stk[top]] = i;
    stk[++top] = i;
  }
  dfs(stk[1], 0);
  for(int i = 0; i <= n; i++) f[i][0] = g[i][0] = 1;
  DP(stk[1]);
  cout << f[stk[1]][k] << '\n';
  return 0;
}

P5933 [清华集训 2012] 串珠子

\(n\) 很小,考虑状压。这算是一个经典的问题了。

一般问图连通块计数的问题,都要考虑容斥。记 \(f_i\) 表示点集 \(i\) 的连通方案数,\(g_i\) 表示点集 \(i\) 的方案数。

\(f_s=g_s-\sum_{t\subseteq s} f_t \times g_{s-t}\)

发现上面的式子会算重,考虑强制钦定一个点所在的点集是连通块,剩下的点集与该点不连通。这样子可以保证不重不漏。

#include <bits/stdc++.h>
using namespace std;
void File(){
  freopen("1.in", "r", stdin);
}
using ll = long long;
const ll mod = 1e9 + 7;
const int kN = 16;
void Add(ll &x, ll y){(x += y) >= mod ? x -= mod : 0;}
int n, c[kN][kN];
ll f[1 << kN], g[1 << kN];
signed main(){
  File();
  ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  cin >> n;
  for(int i = 0; i < n; i++){
    for(int j = 0; j < n; j++) cin >> c[i][j];
  }
  for(int S = 0; S < (1 << n); S++){
    g[S] = 1;
    for(int i = 0; i < n; i++){
      for(int j = i + 1; j < n; j++){
        if(((S >> i) & 1) && ((S >> j) & 1)){
          g[S] = g[S] * 1ll * (c[i][j] + 1) % mod;  
        }
      }
    }
  }
  f[0] = 1;
  for(int S = 1; S < (1 << n); S++){
    f[S] = g[S];
    int tmp = S ^ (S & -S);
    for(int T = tmp; T; T = (T - 1) & tmp){
      Add(f[S], mod - g[T] * f[S - T] % mod);
    }
  }
  cout << f[(1 << n) - 1] << '\n';
  return 0;
}
posted @ 2026-05-05 15:38  CJ-rnd  阅读(7)  评论(0)    收藏  举报