文章目录
[AtCoder-ABC209-f] Deforestation
考虑相邻的两棵树,先砍 i − 1 i-1 i−1 再砍 i i i,花费 h i − 1 + 2 ∗ h i h_{i-1}+2*h_i hi−1+2∗hi,先砍 i i i 再砍 i − 1 i-1 i−1,花费 2 ∗ h i − 1 + h i 2*h_{i-1}+h_i 2∗hi−1+hi。
也就是说,后砍的树贡献次数要多一次。贪心地有 越高的树越先砍。
设 f ( i , j ) : f(i,j): f(i,j): 所有排列中在只考虑前 i i i 个树的情况下, i i i 树是第 j j j 个被砍掉的数量。
-
h i = h i − 1 h_i=h_{i-1} hi=hi−1,无所谓先后。
f ( i , j ) = ∑ k = 1 i − 1 f ( i − 1 , k ) f(i,j)=\sum_{k=1}^{i-1}f(i-1,k) f(i,j)=∑k=1i−1f(i−1,k)。
-
h i > h i − 1 h_i>h_{i-1} hi>hi−1。
f ( i , j ) = ∑ k = j i − 1 f ( i − 1 , k ) f(i,j)=\sum_{k=j}^{i-1}f(i-1,k) f(i,j)=∑k=ji−1f(i−1,k)。
-
h i < h i − 1 h_i<h_{i-1} hi<hi−1。
f ( i , j ) = ∑ k = 1 j − 1 f ( i − 1 , k ) f(i,j)=\sum_{k=1}^{j-1}f(i-1,k) f(i,j)=∑k=1j−1f(i−1,k)。
前缀和优化即可。
注意:可能你会疑惑为什么求和上限不是 n n n。如果是 n n n 其实想一下就知道会算重,但又会疑惑为什么这样不会算重。下面给出两种解释(本质一样)。
- 这个第 j j j 个被砍掉的含义是,假设已知最后 n n n 棵树的砍顺序,把 1 ∼ i 1\sim i 1∼i 的树的砍树顺序单独拎出来组成长度为 i i i 的顺序,从小到大排序后, i i i 树在这个小排列中是第 j j j 个被砍的。
- Oxide \text{Oxide} Oxide 解释:这是个相对过程,后面的树一旦插入 j j j,相当于把前面的树砍顺序在 j j j 及以后的都再往后移了一个。
#include <cstdio>
#define maxn 4005
#define int long long
#define mod 1000000007
int n;
int h[maxn];
int dp[maxn][maxn];
signed main() {
scanf( "%lld", &n );
for( int i = 1;i <= n;i ++ )
scanf( "%lld", &h[i] );
h[0] = h[1], dp[0][0] = 1;
for( int i = 1;i <= n;i ++ ) {
for( int j = 1;j <= i;j ++ ) {
if( h[i] == h[i - 1] )
dp[i][j] = dp[i - 1][i - 1];
else if( h[i] > h[i - 1] )
dp[i][j] = ( dp[i - 1][i - 1] - dp[i - 1][j - 1] + mod ) % mod;
else
dp[i][j] = dp[i - 1][j - 1];
}
for( int j = 1;j <= i;j ++ )
dp[i][j] = ( dp[i][j] + dp[i][j - 1] ) % mod;
}
printf( "%lld\n", dp[n][n] );
return 0;
}
[AtCoder-Educational DP Contest-T]Permutation
这道题和上一道题目是同一个类型, d p dp dp 定义以及优化也是一样的。
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define mod 1000000007
#define maxn 3005
int f[maxn][maxn];
char s[maxn];
int n;
/*
f(i,j):考虑到i位放j 前面都已经满足s的符号限制 的排列数量
s[i] = '<' f(i+1,j) <- f(i,k) k<j
s[i] = '>' f(i+1,j) <- f(i,k) k>j
*/
signed main() {
scanf( "%lld %s", &n, s + 1 );
for( int i = 1;i <= n;i ++ ) f[1][i] = i;
for( int i = 1;i < n;i ++ ) {
for( int j = 1;j <= n;j ++ ) {
if( s[i] == '<' )
f[i + 1][j] = f[i][j - 1] % mod;
else
f[i + 1][j] = ( f[i][i] - f[i][j - 1] + mod ) % mod;
}
for( int j = 1;j <= n;j ++ )
( f[i + 1][j] += f[i + 1][j - 1] ) %= mod;
}
printf( "%lld\n", f[n][n] );
return 0;
}
「JOI Open 2016」摩天大楼
将 A A A 从小到大排序。考虑微元法。
答案排列相邻的一对 A i , A j A_i,A_j Ai,Aj,产生的贡献可以表示为 ∑ k = j i − 1 A k + 1 − A k \sum_{k=j}^{i-1}A_{k+1}-A_k ∑k=ji−1Ak+1−Ak。
我们可以提前计算 A k + 1 − A k A_{k+1}-A_{k} Ak+1−Ak 对答案的贡献次数。
设 f ( i , j , k , d ) : f(i,j,k,d): f(i,j,k,d): 放了前 i i i 个数,产生了 j j j 个互相独立的连续段,总贡献为 k k k,排列的首尾(下面称为墙)被占了 d d d 个。
则可以算出当前 A i + 1 − A i A_{i+1}-A_i Ai+1−Ai 的贡献,即为 ( A i + 1 − A i ) ⋅ ( 2 ∗ j − d ) (A_{i+1}-A_i)·(2*j-d) (Ai+1−Ai)⋅(2∗j−d)。
因为有 2 ∗ j − d 2*j-d 2∗j−d(墙只能延伸一个方向)个连续段的左右在后面某个时刻会放数,这部分一定会贡献微元。
令 t = k + ( 2 ∗ j − d ) ( A i + 1 − A i ) t=k+(2*j-d)(A_{i+1}-A_i) t=k+(2∗j−d)(Ai+1−Ai)。
-
i + 1 i+1 i+1 独立成一段。
-
独立成一个中间段,非墙。
如果一个墙都没有,那么有 j + 1 j+1 j+1 个空,KaTeX parse error: Expected '}', got '_' at position 7: \text{_̲_X___X___}。
如果有一个墙,就少一个空,KaTeX parse error: Expected '}', got '_' at position 8: \text{X_̲__X___X___}。
所以合并可以写成以下形式:
f ( i + 1 , j + 1 , t , d ) ← f ( i , j , k , d ) ∗ ( j + 1 − d ) f(i+1,j+1,t,d)\leftarrow f(i,j,k,d)*(j+1-d) f(i+1,j+1,t,d)←f(i,j,k,d)∗(j+1−d)。
-
独立成一个墙段,如果前后两个墙均未有,则还要考虑是做首还是尾。
f ( i + 1 , j + 1 , t , d + 1 ) ← f ( i , j , k , d ) ⋅ ( 2 − d ) f(i+1,j+1,t,d+1)\leftarrow f(i,j,k,d)·(2-d) f(i+1,j+1,t,d+1)←f(i,j,k,d)⋅(2−d)。
-
-
i + 1 i+1 i+1 做枢纽,合并某两个段。 j j j 个段,有 j − 1 j-1 j−1 个空填入 i + 1 i+1 i+1 然后连接起来。
f ( i + 1 , j − 1 , t , d ) ← f ( i , j , k , d ) ⋅ ( j − 1 ) f(i+1,j-1,t,d)\leftarrow f(i,j,k,d)·(j-1) f(i+1,j−1,t,d)←f(i,j,k,d)⋅(j−1)。
-
i + 1 i+1 i+1 从某个段的左/右延伸。
-
普通延伸。
f ( i + 1 , j , t , d ) ← f ( i , j , k , d ) ⋅ ( 2 ∗ j − d ) f(i+1,j,t,d)\leftarrow f(i,j,k,d)·(2*j-d) f(i+1,j,t,d)←f(i,j,k,d)⋅(2∗j−d)。
-
延伸到了墙。
f ( i + 1 , j , t , d + 1 ) ← f ( i , j , k , d ) ⋅ ( 2 − l ) f(i+1,j,t,d+1)\leftarrow f(i,j,k,d)·(2-l) f(i+1,j,t,d+1)←f(i,j,k,d)⋅(2−l)。
-
有些转移还有对 i , j , k , d i,j,k,d i,j,k,d 的限制,具体可见下面代码。
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define mod 1000000007
int f[2][102][1002][3];
int a[105], n, L;
signed main() {
scanf( "%lld %lld", &n, &L );
for( int i = 1;i <= n;i ++ ) scanf( "%lld", &a[i] );
if( n == 1 ) return ! puts("1");
sort( a + 1, a + n + 1 );
f[0][0][0][0] = 1;
for( int i = 0;i < n;i ++ ) {
int o = i & 1;
memset( f[o ^ 1], 0, sizeof( f[o ^ 1] ) );
for( int j = 0;j <= i + 1;j ++ )
for( int k = 0;k <= L;k ++ )
for( int d = 0;d <= 2;d ++ ) {
if( ! f[o][j][k][d] ) continue;
int t = k + ( a[i + 1] - a[i] ) * ( j * 2 - d );
if( t > L ) continue;
( f[o ^ 1][j + 1][t][d] += f[o][j][k][d] * ( j + 1 - d ) ) %= mod;
if( d < 2 ) ( f[o ^ 1][j + 1][t][d + 1] += f[o][j][k][d] * ( 2 - d ) ) %= mod;
( f[o ^ 1][j][t][d] += f[o][j][k][d] * ( 2 * j - d ) ) %= mod;
if( j ) ( f[o ^ 1][j - 1][t][d] += f[o][j][k][d] * ( j - 1 ) ) %= mod;
if( d < 2 and j ) ( f[o ^ 1][j][t][d + 1] += f[o][j][k][d] * ( 2 - d ) ) %= mod;
}
}
int ans = 0;
for( int i = 0;i <= L;i ++ ) ( ans += f[n & 1][1][i][2] ) %= mod;
printf( "%lld\n", ans );
return 0;
}
topcoder srm 489 div1 lev3 : AppleTrees
topcoder这是什么煞笔提交方式(无能狂怒
如果我们确定了一个种树的顺序,那么相邻树的最小间距也随之确定。
将 D D D 减去这个最小间距,就可以看成一个插板问题了。
所以我们要求出对于每一个 L L L,满足条件 L = ∑ i = 1 n − 1 max { r ( P i ) , r ( P i + 1 ) } L=\sum_{i=1}^{n-1}\max\Big\{r(P_i),r(P_{i+1})\Big\} L=∑i=1n−1max{r(Pi),r(Pi+1)} 的排列数量。
注意到 r r r 最大的 i i i 左右种什么数不影响这两段间隙的最小长度,因为均由 r i r_i ri 决定。
所以我们将 r r r 从小到大排序,贡献由相邻两棵树之间后放入的树( r r r更大的树)决定。
设 f ( i , j , k ) : f(i,j,k): f(i,j,k): 前 i i i 棵树,形成了 j j j 个不相交的排列,排列的代价总和为 k k k 的方案数。
-
i + 1 i+1 i+1 独立成一个新排列。
f ( i + 1 , j + 1 , k ) ← f ( i , j , k ) f(i+1,j+1,k)\leftarrow f(i,j,k) f(i+1,j+1,k)←f(i,j,k)。
-
i + 1 i+1 i+1 延伸某个排列。每个排列还有左右延伸之分。
f ( i + 1 , j , k + r i + 1 ) ← f ( i , j , k ) ∗ 2 ∗ j f(i+1,j,k+r_{i+1})\leftarrow f(i,j,k)*2*j f(i+1,j,k+ri+1)←f(i,j,k)∗2∗j。
-
i + 1 i+1 i+1 合并两个排列。由于每个排列之间没有敲定顺序,所以是任选两个排列还要区分接法。
f ( i + 1 , j − 1 , k + 2 ∗ r i + 1 ) ← f ( i , j , k ) ∗ A j 2 f(i+1,j-1,k+2*r_{i+1})\leftarrow f(i,j,k)*A_{j}^2 f(i+1,j−1,k+2∗ri+1)←f(i,j,k)∗Aj2。
#include <bits/stdc++.h>
using namespace std;
#define mod 1000000007
long long fac[100005], inv[100005];
long long f[50][50][1700];
int qkpow( int x, int y ) {
int ans = 1;
while( y ) {
if( y & 1 ) ans = 1ll * ans * x % mod;
x = 1ll * x * x % mod;
y >>= 1;
}
return ans;
}
void init( int n ) {
fac[0] = inv[0] = 1;
for( int i = 1;i <= n;i ++ ) fac[i] = fac[i - 1] * i % mod;
inv[n] = qkpow( fac[n], mod - 2 );
for( int i = n - 1;i;i -- ) inv[i] = inv[i + 1] * ( i + 1 ) % mod;
}
long long C( int n, int m ) {
if( n < m ) return 0;
else return fac[n] * inv[m] % mod * inv[n - m] % mod;
}
class AppleTrees {
public :
int theCount( int D, vector < int > r ) {
int n = r.size();
init( D );
sort( r.begin(), r.end() );
f[0][0][0] = 1;
for( int i = 0;i < n;i ++ )
for( int j = 0;j <= n;j ++ )
for( int k = 0;k <= 1600 and k <= D;k ++ ) {
if( ! f[i][j][k] ) continue;
( f[i + 1][j + 1][k] += f[i][j][k] ) %= mod;
( f[i + 1][j][k + r[i]] += f[i][j][k] * (j << 1) % mod ) %= mod;
if( j )
( f[i + 1][j - 1][k + (r[i] << 1)] += f[i][j][k] * j * (j - 1) % mod ) %= mod;
}
int ans = 0;
for( int i = 1;i <= min( 1600, D );i ++ )
ans = ( 1ll * ans + f[n][1][i - 1] * C( D - i + n, n ) % mod ) % mod;
return ans;
}
};
[CodeForces-626F] Group Projects
与上面的摩天大楼类似。
对于每一组,我们只关心最大值和最小值。
将序列从小到大排序后,每组的最大值减去最小值的差值相当于是一段排序后数组的差分和。
设 f ( i , j , k ) : f(i,j,k): f(i,j,k): 前 i i i 个数,当前分成了若干组,其中有 j j j 组还能继续放数,各组的极值之和为 k k k 的方案数。
当前差分值的贡献为 ( a i + 1 − a i ) ∗ j (a_{i+1}-a_i)*j (ai+1−ai)∗j。记 t = ( a i + 1 − a i ) ∗ j + k t=(a_{i+1}-a_i)*j+k t=(ai+1−ai)∗j+k。
- 单独成一组,且是组的起终点,不能继续放数: f ( i + 1 , j , t ) ← f ( i , j , k ) f(i+1,j,t)\leftarrow f(i,j,k) f(i+1,j,t)←f(i,j,k)。
- 新开一个组,且不关闭: f ( i + 1 , j + 1 , t ) ← f ( i , j , k ) f(i+1,j+1,t)\leftarrow f(i,j,k) f(i+1,j+1,t)←f(i,j,k)。
- 插入某个组,但不结束那个组,有 j j j 种方式: f ( i + 1 , j , t ) ← f ( i , j , k ) ∗ j f(i+1,j,t)\leftarrow f(i,j,k)*j f(i+1,j,t)←f(i,j,k)∗j。
- 插入某个组,且结束那个组,仍有 j j j 种方式: f ( i + 1 , j , t ) ← f ( i , j , k ) ∗ j f(i+1,j,t)\leftarrow f(i,j,k)*j f(i+1,j,t)←f(i,j,k)∗j。
答案即为 ∑ i = 0 K f ( n , 0 , i ) \sum_{i=0}^K f(n,0,i) ∑i=0Kf(n,0,i)。
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define mod 1000000007
int f[2][205][1005];
int a[205];
int N, K;
signed main() {
scanf( "%lld %lld", &N, &K );
for( int i = 1;i <= N;i ++ ) scanf( "%lld", &a[i] );
sort( a + 1, a + N + 1 );
f[0][0][0] = 1;
for( int i = 0;i < N;i ++ ) {
int o = i & 1;
memset( f[o ^ 1], 0, sizeof( f[o ^ 1] ) );
for( int j = 0;j <= i + 1;j ++ )
for( int k = 0;k <= K;k ++ ) {
int d = k + ( a[i + 1] - a[i] ) * j;
if( d > K ) continue;
( f[o ^ 1][j][d] += f[o][j][k] ) %= mod;
( f[o ^ 1][j + 1][d] += f[o][j][k] ) %= mod;
( f[o ^ 1][j][d] += f[o][j][k] * j ) %= mod;
if( j ) ( f[o ^ 1][j - 1][d] += f[o][j][k] * j ) %= mod;
}
}
int ans = 0;
for( int i = 0;i <= K;i ++ ) ( ans += f[N & 1][0][i] ) %= mod;
printf( "%lld\n", ans );
return 0;
}
[TopCoder] Seatfriends
与上面的AppleTrees类似。
如果把空位放入动态规划一起考虑,计数会变得很麻烦。所以只考虑有 k k k 个位置的情况。
因为是环排列,所以先固定第一个人的位置,有 n n n 种选择, f ( 1 , 1 ) = n f(1,1)=n f(1,1)=n。
设 f ( i , j ) : i f(i,j):i f(i,j):i 个人分成 j j j 组的方案数。注意保证转移过程全程合法。
- 在任意两组内新增一个组: f ( i + 1 , j + 1 ) ← f ( i , j ) ∗ j f(i+1,j+1)\leftarrow f(i,j)*j f(i+1,j+1)←f(i,j)∗j。因为是个环,所以空应该有 j j j 个。
- 新加一个在组的其中一边: f ( i + 1 , j ) ← f ( i , j ) ∗ j ∗ 2 f(i+1,j)\leftarrow f(i,j)*j*2 f(i+1,j)←f(i,j)∗j∗2。
- 合并两个组,空仍然有 j j j 个: f ( i + 1 , j − 1 ) ← f ( i , j ) ∗ j f(i+1,j-1)\leftarrow f(i,j)*j f(i+1,j−1)←f(i,j)∗j。
注意,当 n = k n=k n=k 的时候,直接返回 f ( k − 1 , 1 ) f(k-1,1) f(k−1,1)。因为最后一个人只能坐剩下来的那个空位。
最后考虑各组间插入空位的情况,对于 f ( k , j ) f(k,j) f(k,j) 而言,要将 n − k n-k n−k 个空位插入 j j j 个间隔中(每一个至少需要一个空位),显然为 ( n − k − 1 j − 1 ) \binom{n-k-1}{j-1} (j−1n−k−1)。
a n s = ∑ j = 1 G f ( k , j ) ( n − k − 1 j − 1 ) ans=\sum_{j=1}^Gf(k,j)\binom{n-k-1}{j-1} ans=∑j=1Gf(k,j)(j−1n−k−1)。
#include <bits/stdc++.h>
using namespace std;
#define maxn 2005
#define mod 1000000007
long long f[2005][2005];
long long fac[maxn], inv[maxn];
int qkpow( int x, int y ) {
int ans = 1;
while( y ) {
if( y & 1 ) ans = 1ll * ans * x % mod;
x = 1ll * x * x % mod;
y >>= 1;
}
return ans;
}
void init( int n ) {
fac[0] = inv[0] = 1;
for( int i = 1;i <= n;i ++ ) fac[i] = fac[i - 1] * i % mod;
inv[n] = qkpow( fac[n], mod - 2 );
for( int i = n - 1;i;i -- ) inv[i] = inv[i + 1] * ( i + 1 ) % mod;
}
int C( int n, int m ) {
if( n < m ) return 0;
else if( ! n or ! m ) return 1;
else return fac[n] * inv[m] % mod * inv[n - m] % mod;
}
class Seatfriends {
public :
int countseatnumb( int n, int k, int g ) {
int N = n, K = k, G = g;
init( N );
f[1][1] = N;
for( int i = 1;i < K;i ++ )
for( int j = 1;j <= G;j ++ ) {
(f[i + 1][j + 1] += f[i][j] * j) %= mod;
(f[i + 1][j] += f[i][j] * j * 2) %= mod;
(f[i + 1][j - 1] += f[i][j] * j) %= mod;
}
if( N == K ) return f[K - 1][1];
int ans = 0;
for( int i = 1;i <= G;i ++ )
(ans += f[K][i] * C(N - K - 1, i - 1) % mod) %= mod;
return ans;
}
};
小结
- 排列有空的计数,先把所有空位抽出来,最后组合数乘回去。转移过程中假设空存在,也就是分组的依据。
- 所求为环排列,钦定一种选法,最后乘上环大小。
- 插入一般理解为插空。
- 注意是否区分一个组的左右。
- 注意是否考虑组之间存在顺序,如果存在顺序且转移没考虑,记得乘组数的阶乘。
- 差分法/微分法技巧
- 一般状态都定义为 f ( i , j , . . . ) : i f(i,j,...):i f(i,j,...):i 个分成 j j j 组,然后可能还要附带一些信息。(是否抵到“墙”)
- 无序转有序。
- 状态转移一般都有:插入自成一组;加入某一组;合并某两组;墙组还是普通组。
- 一般不需要什么多牛逼的优化。