DP 练习
先考虑两种暴力 dp。
- 令 \(f_{i,j}\) 表示可重集内最大元素 \(\le i\),总和为 \(j\) 的方案数。
有转移 \(f_{i,j}=f_{i-1,j}+f_{i,j-i}\)
答案为 \(ans=f_{n,n}\) - 令 \(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;
}
很深刻的题。
题目给的限制为前缀 \(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;
}
\(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;
}


浙公网安备 33010602011771号