【刷题记录】排列dp

[AtCoder-ABC209-f] Deforestation

考虑相邻的两棵树,先砍 i − 1 i-1 i1 再砍 i i i,花费 h i − 1 + 2 ∗ h i h_{i-1}+2*h_i hi1+2hi,先砍 i i i 再砍 i − 1 i-1 i1,花费 2 ∗ h i − 1 + h i 2*h_{i-1}+h_i 2hi1+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=hi1,无所谓先后。

    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=1i1f(i1,k)

  • h i > h i − 1 h_i>h_{i-1} hi>hi1

    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=ji1f(i1,k)

  • h i < h i − 1 h_i<h_{i-1} hi<hi1

    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=1j1f(i1,k)

前缀和优化即可。

注意:可能你会疑惑为什么求和上限不是 n n n。如果是 n n n 其实想一下就知道会算重,但又会疑惑为什么这样不会算重。下面给出两种解释(本质一样)。

  • 这个第 j j j 个被砍掉的含义是,假设已知最后 n n n 棵树的砍顺序,把 1 ∼ i 1\sim i 1i 的树的砍树顺序单独拎出来组成长度为 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

vjudge

这道题和上一道题目是同一个类型, 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」摩天大楼

LOJ#2743

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=ji1Ak+1Ak

我们可以提前计算 A k + 1 − A k A_{k+1}-A_{k} Ak+1Ak 对答案的贡献次数。

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+1Ai 的贡献,即为 ( A i + 1 − A i ) ⋅ ( 2 ∗ j − d ) (A_{i+1}-A_i)·(2*j-d) (Ai+1Ai)(2jd)

因为有 2 ∗ j − d 2*j-d 2jd(墙只能延伸一个方向)个连续段的左右在后面某个时刻会放数,这部分一定会贡献微元。

t = k + ( 2 ∗ j − d ) ( A i + 1 − A i ) t=k+(2*j-d)(A_{i+1}-A_i) t=k+(2jd)(Ai+1Ai)

  • 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+1d)

    • 独立成一个墙段,如果前后两个墙均未有,则还要考虑是做首还是尾。

      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)(2d)

  • i + 1 i+1 i+1 做枢纽,合并某两个段。 j j j 个段,有 j − 1 j-1 j1 个空填入 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,j1,t,d)f(i,j,k,d)(j1)

  • 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)(2jd)

    • 延伸到了墙。

      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)(2l)

有些转移还有对 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

Vjudge-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=1n1max{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)2j

  • 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,j1,k+2ri+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): i i i 个数,当前分成了若干组,其中有 j j j 组还能继续放数,各组的极值之和为 k k k 的方案数。

当前差分值的贡献为 ( a i + 1 − a i ) ∗ j (a_{i+1}-a_i)*j (ai+1ai)j。记 t = ( a i + 1 − a i ) ∗ j + k t=(a_{i+1}-a_i)*j+k t=(ai+1ai)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

Vjudge

与上面的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)j2
  • 合并两个组,空仍然有 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,j1)f(i,j)j

注意,当 n = k n=k n=k 的时候,直接返回 f ( k − 1 , 1 ) f(k-1,1) f(k1,1)。因为最后一个人只能坐剩下来的那个空位。

最后考虑各组间插入空位的情况,对于 f ( k , j ) f(k,j) f(k,j) 而言,要将 n − k n-k nk 个空位插入 j j j 个间隔中(每一个至少需要一个空位),显然为 ( n − k − 1 j − 1 ) \binom{n-k-1}{j-1} (j1nk1)

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)(j1nk1)

#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 组,然后可能还要附带一些信息。(是否抵到“墙”)
  • 无序转有序。
  • 状态转移一般都有:插入自成一组;加入某一组;合并某两组;墙组还是普通组。
  • 一般不需要什么多牛逼的优化。
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值