文章目录
- [AtCoder-ABC209-f] Deforestation
- [AtCoder-Educational DP Contest-T]Permutation
- 「JOI Open 2016」摩天大楼
- topcoder srm 489 div1 lev3 : AppleTrees
- [CodeForces-626F] Group Projects
- [TopCoder] Seatfriends
- 小结
[AtCoder-ABC209-f] Deforestation
考虑相邻的两棵树,先砍 i−1i-1i−1 再砍 iii,花费 hi−1+2∗hih_{i-1}+2*h_ihi−1+2∗hi,先砍 iii 再砍 i−1i-1i−1,花费 2∗hi−1+hi2*h_{i-1}+h_i2∗hi−1+hi。
也就是说,后砍的树贡献次数要多一次。贪心地有 越高的树越先砍。
设 f(i,j):f(i,j):f(i,j): 所有排列中在只考虑前 iii 个树的情况下, iii 树是第 jjj 个被砍掉的数量。
-
hi=hi−1h_i=h_{i-1}hi=hi−1,无所谓先后。
f(i,j)=∑k=1i−1f(i−1,k)f(i,j)=\sum_{k=1}^{i-1}f(i-1,k)f(i,j)=∑k=1i−1f(i−1,k)。
-
hi>hi−1h_i>h_{i-1}hi>hi−1。
f(i,j)=∑k=ji−1f(i−1,k)f(i,j)=\sum_{k=j}^{i-1}f(i-1,k)f(i,j)=∑k=ji−1f(i−1,k)。
-
hi<hi−1h_i<h_{i-1}hi<hi−1。
f(i,j)=∑k=1j−1f(i−1,k)f(i,j)=\sum_{k=1}^{j-1}f(i-1,k)f(i,j)=∑k=1j−1f(i−1,k)。
前缀和优化即可。
注意:可能你会疑惑为什么求和上限不是 nnn。如果是 nnn 其实想一下就知道会算重,但又会疑惑为什么这样不会算重。下面给出两种解释(本质一样)。
- 这个第 jjj 个被砍掉的含义是,假设已知最后 nnn 棵树的砍顺序,把 1∼i1\sim i1∼i 的树的砍树顺序单独拎出来组成长度为 iii 的顺序,从小到大排序后,iii 树在这个小排列中是第 jjj 个被砍的。
- Oxide\text{Oxide}Oxide 解释:这是个相对过程,后面的树一旦插入 jjj,相当于把前面的树砍顺序在 jjj 及以后的都再往后移了一个。
#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;elsedp[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
vjudge
这道题和上一道题目是同一个类型,dpdpdp 定义以及优化也是一样的。
#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;elsef[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」摩天大楼
LOJ#2743
将 AAA 从小到大排序。考虑微元法。
答案排列相邻的一对 Ai,AjA_i,A_jAi,Aj,产生的贡献可以表示为 ∑k=ji−1Ak+1−Ak\sum_{k=j}^{i-1}A_{k+1}-A_k∑k=ji−1Ak+1−Ak。
我们可以提前计算 Ak+1−AkA_{k+1}-A_{k}Ak+1−Ak 对答案的贡献次数。
设 f(i,j,k,d):f(i,j,k,d):f(i,j,k,d): 放了前 iii 个数,产生了 jjj 个互相独立的连续段,总贡献为 kkk,排列的首尾(下面称为墙)被占了 ddd 个。
则可以算出当前 Ai+1−AiA_{i+1}-A_iAi+1−Ai 的贡献,即为 (Ai+1−Ai)⋅(2∗j−d)(A_{i+1}-A_i)·(2*j-d)(Ai+1−Ai)⋅(2∗j−d)。
因为有 2∗j−d2*j-d2∗j−d(墙只能延伸一个方向)个连续段的左右在后面某个时刻会放数,这部分一定会贡献微元。
令 t=k+(2∗j−d)(Ai+1−Ai)t=k+(2*j-d)(A_{i+1}-A_i)t=k+(2∗j−d)(Ai+1−Ai)。
-
i+1i+1i+1 独立成一段。
-
独立成一个中间段,非墙。
如果一个墙都没有,那么有 j+1j+1j+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+1i+1i+1 做枢纽,合并某两个段。jjj 个段,有 j−1j-1j−1 个空填入 i+1i+1i+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+1i+1i+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,di,j,k,di,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
Vjudge-AppleTrees
topcoder这是什么煞笔提交方式(无能狂怒
如果我们确定了一个种树的顺序,那么相邻树的最小间距也随之确定。
将 DDD 减去这个最小间距,就可以看成一个插板问题了。
所以我们要求出对于每一个 LLL,满足条件 L=∑i=1n−1max{r(Pi),r(Pi+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)} 的排列数量。
注意到 rrr 最大的 iii 左右种什么数不影响这两段间隙的最小长度,因为均由 rir_iri 决定。
所以我们将 rrr 从小到大排序,贡献由相邻两棵树之间后放入的树(rrr更大的树)决定。
设 f(i,j,k):f(i,j,k):f(i,j,k): 前 iii 棵树,形成了 jjj 个不相交的排列,排列的代价总和为 kkk 的方案数。
-
i+1i+1i+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+1i+1i+1 延伸某个排列。每个排列还有左右延伸之分。
f(i+1,j,k+ri+1)←f(i,j,k)∗2∗jf(i+1,j,k+r_{i+1})\leftarrow f(i,j,k)*2*jf(i+1,j,k+ri+1)←f(i,j,k)∗2∗j。
-
i+1i+1i+1 合并两个排列。由于每个排列之间没有敲定顺序,所以是任选两个排列还要区分接法。
f(i+1,j−1,k+2∗ri+1)←f(i,j,k)∗Aj2f(i+1,j-1,k+2*r_{i+1})\leftarrow f(i,j,k)*A_{j}^2f(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
CodeForces
与上面的摩天大楼类似。
对于每一组,我们只关心最大值和最小值。
将序列从小到大排序后,每组的最大值减去最小值的差值相当于是一段排序后数组的差分和。
设 f(i,j,k):f(i,j,k):f(i,j,k): 前 iii 个数,当前分成了若干组,其中有 jjj 组还能继续放数,各组的极值之和为 kkk 的方案数。
当前差分值的贡献为 (ai+1−ai)∗j(a_{i+1}-a_i)*j(ai+1−ai)∗j。记 t=(ai+1−ai)∗j+kt=(a_{i+1}-a_i)*j+kt=(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)。
- 插入某个组,但不结束那个组,有 jjj 种方式:f(i+1,j,t)←f(i,j,k)∗jf(i+1,j,t)\leftarrow f(i,j,k)*jf(i+1,j,t)←f(i,j,k)∗j。
- 插入某个组,且结束那个组,仍有 jjj 种方式:f(i+1,j,t)←f(i,j,k)∗jf(i+1,j,t)\leftarrow f(i,j,k)*jf(i+1,j,t)←f(i,j,k)∗j。
答案即为 ∑i=0Kf(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
Vjudge
与上面的AppleTrees类似。
如果把空位放入动态规划一起考虑,计数会变得很麻烦。所以只考虑有 kkk 个位置的情况。
因为是环排列,所以先固定第一个人的位置,有 nnn 种选择,f(1,1)=nf(1,1)=nf(1,1)=n。
设 f(i,j):if(i,j):if(i,j):i 个人分成 jjj 组的方案数。注意保证转移过程全程合法。
- 在任意两组内新增一个组:f(i+1,j+1)←f(i,j)∗jf(i+1,j+1)\leftarrow f(i,j)*jf(i+1,j+1)←f(i,j)∗j。因为是个环,所以空应该有 jjj 个。
- 新加一个在组的其中一边:f(i+1,j)←f(i,j)∗j∗2f(i+1,j)\leftarrow f(i,j)*j*2f(i+1,j)←f(i,j)∗j∗2。
- 合并两个组,空仍然有 jjj 个:f(i+1,j−1)←f(i,j)∗jf(i+1,j-1)\leftarrow f(i,j)*jf(i+1,j−1)←f(i,j)∗j。
注意,当 n=kn=kn=k 的时候,直接返回 f(k−1,1)f(k-1,1)f(k−1,1)。因为最后一个人只能坐剩下来的那个空位。
最后考虑各组间插入空位的情况,对于 f(k,j)f(k,j)f(k,j) 而言,要将 n−kn-kn−k 个空位插入 jjj 个间隔中(每一个至少需要一个空位),显然为 (n−k−1j−1)\binom{n-k-1}{j-1}(j−1n−k−1)。
ans=∑j=1Gf(k,j)(n−k−1j−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,...):if(i,j,...):if(i,j,...):i 个分成 jjj 组,然后可能还要附带一些信息。(是否抵到“墙”)
- 无序转有序。
- 状态转移一般都有:插入自成一组;加入某一组;合并某两组;墙组还是普通组。
- 一般不需要什么多牛逼的优化。