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;
}

P10432 [JOIST 2024] 滑雪 2 / Ski 2

  • 一个点有两种操作,且这两种操作不会都进行。

反证法:假设存在点 \(x\) 进行了两种操作,不妨设 \(x\) 点抬高后连向点 \(y\),连向 \(x\) 的两个点为 \(a\) 和 \(b\),且 \(H_a\ge H_b\),我们不难得出在抬高海拔前 \(H_a\ge H_b>H_y>H_x\)。

若 \(c_x\ge c_y\),则我们不会在点 \(x\) 上增加设施,而是直接在 \(y\) 点上增加设施,这样会更优。
否则,可以放弃抬高 \(x\) 点的海拔,直接让 \(y\) 连向 \(x\),并让 \(a\) 和 \(b\) 中的任意一个该连 \(y\) 即可,这样会更优。

  • \(H_i\) 相同的点不可能高度全部抬高,至少有一个高度不变,我们让 \(c_i\) 更小的点留下来最优。

首先,我们不可能把所有的点都抬高。其次,假设我们把若干个点抬高到当前高度,不如留下原来就在当前高度的点,因为它们都能为更高位置提供一个免费设施,进而,我们一定要保留 \(c_i\) 最小的那个点。

此时我们可以 DP 了,我们按高度从低到高扫描。设 \(f_{i,j,k}\) 表示当前高度为 \(i\),高度在 \([1,i-1]\) 中有 \(j\) 个设施,有 \(k\) 个点将从高度 \(i\) 往上拔高到高度 \(i+1\)。

令 \(cnt_i\) 为原来在高度 \(i\) 的点,\(Mn_i\) 为原来高度为 \([1,i]\) 的点中 \(c_i\) 的最小值。

接下来考虑从高度 \(i\) 转移到 \(i+1\)。

不妨设有 \(x\) 个原来在高度 \(i+1\) 的点连向前面高度在 \([1,i]\) 的 \(j\) 个设施,贪心可知这 \(x\) 个点的高度无需拔高。还可以它们每连向一个设施,自己也会提供一个新的免费设施,代价为 \(k\times K\)。

设有 \(y\) 个在高度 \([1,i+1]\) 的点增加了额外设施,

P14344 [JOISC 2019] 两道料理 / Two Dishes

\(\Theta(n^2)\) 暴力:

令 \(f_{i,j}\) 表示第一个序列选了 \(i\) 个,第二个序列选了 \(j\) 个的方案数。
转移很朴素。

\(\forall i,\forall j, s_i = t_j\) 的特殊性质:

枚举第一个序列中选到第 \(i\) 个,第二个序列通过对时间作前缀和加二分可求出第二个序列中选到第 \(j\) 个。

答案为 \(\max{(\sum_{t=1}^i p_t + \sum_{t=1}^j q_t)}\)。

略

P5369 [PKUSC2018] 最大前缀和

posted @ 2026-05-05 15:38  CJ-rnd  阅读(14)  评论(0)    收藏  举报