这是我没有接触过的dp类型。因此记录一下。
先看lydsy4498(这题现在OJ上已经消失了)。
首先容易想到,对于一个布阵方式,我们可以把多余的格子抽掉,还原出一个魔法师紧凑的局面。那么每一个魔法师紧凑的局面,如果长度为
l
l
l,往其中填充
L
−
l
L-l
L−l个格子,通过插板原理,我们知道它对应着
C
n
+
L
−
l
n
C_{n+L-l}^n
Cn+L−ln个布阵,于是问题转化为求长度为
l
l
l的紧凑局面有多少个。
到这里基本上没想法了,只能看题解的套路。首先将法师的攻击范围从大到小排序。然后开始想象把法师一个个填入战线中。用
d
p
[
i
]
[
j
]
[
k
]
dp[i][j][k]
dp[i][j][k]表示当前填入到了第i个法师,总的长度到了j,剩余k个连续空段还需要填入法师。注意第三维,很精彩。我们填入新法师时,首先看他是将左右两个法师连接(或者说消灭一个空段),还是连接左侧或者右侧的一个法师(或者说保留空段),还是两边都不连接(或者说增加一个空段)。更新时,第一种情况,由于法师范围递减,所以j只需要增加1给这个法师立足即可,第二种情况,需要增加
d
[
i
]
d[i]
d[i]腾出位置,而第三种情况需要
2
∗
d
[
i
]
−
1
2*d[i]-1
2∗d[i]−1(这就是之前从大到小排序的原因,对长度的贡献是固定化的)。
最后
d
p
[
n
]
[
l
]
[
0
]
dp[n][l][0]
dp[n][l][0]就是之前子问题的答案,乘上组合数相加即可。
#include<cstdio>
#include<functional>
#include<algorithm>
#define mo 1000000007
using namespace std;
using LL=long long;
int L,n,d[45],o,dp[2][3205][45],sum,ans;
int fac[1000005];
int quick_power(int x, int y)
{
int res=1,base=x;
while(y)
{
if(y&1)
res=(LL)res*base%mo;
base=(LL)base*base%mo;
y>>=1;
}
return res;
}
int C(int n, int m)
{
return (LL)fac[n]*quick_power(fac[m],mo-2)%mo*quick_power(fac[n-m],mo-2)%mo;
}
int main()
{
scanf("%d%d",&L,&n);
for(int i=1;i<=n;i++)
scanf("%d",&d[i]);
fac[0]=1;
for(int i=1;i<=L;i++)
fac[i]=(LL)fac[i-1]*i%mo;
dp[o][0][1]=1;
sort(d+1,d+n+1,greater<int>());
for(int i=1;i<=n;i++)
{
for(int j=0;j<=sum+d[i]*2-1;j++)
for(int k=0;k<=n+1;k++)
dp[o^1][j][k]=0;
for(int j=0;j<=sum;j++)
for(int k=1;k<=n;k++)
{
(dp[o^1][j+d[i]*2-1][k+1]+=(LL)dp[o][j][k]*k%mo)%=mo;
(dp[o^1][j+d[i]][k]+=(LL)dp[o][j][k]*k*2%mo)%=mo;
(dp[o^1][j+1][k-1]+=(LL)dp[o][j][k]*k%mo)%=mo;
}
o^=1;
sum+=d[i]*2-1;
}
for(int j=0;j<=sum&&j<=L;j++)
(ans+=(LL)dp[o][j][0]*C(L-j+n,n)%mo)%=mo;
printf("%d",ans);
return 0;
}
再看一道类似的题。
4664: Count
Time Limit: 20 Sec Memory Limit: 128 MB
Submit: 159 Solved: 71
[Submit][Status][Discuss]
Description
小叶子的桌面上有 n 本高度不相同的书,n+e 现在需要把这些书按照一定的顺序摆放好。假设第 i 本书的高度为
h[i],n+e 的摆放用一个 1~n的排列 pi 来表示。定义一个摆放的混乱程度:|h[p2]-h[p1]|+|h[p3]-h[p2]|+…
…+|h[pn]-h[pn-1]|,即相邻两本书的高度差的绝对值之和。已知合法的摆放要求其混乱程度不超过 L。小叶子想
要知道,n+e 到底有多少种合法的摆放的方法呢?作为将要参加 NOI 的选手,你应该知道,小叶子只关心这个数
对10^9+7 取模的结果。
Input
第一行两个数 n,L。接下来一行 n 个数 h[i]
1<=n<=100,1<=L,h[i]<=1000
Output
输出一行,表示方案数对 10^9+7 取模的结果。
Sample Input
3 2
2 3 4
Sample Output
2
【样例解释】
两种合法的摆放姿势如下:
2 3 4
4 3 2
通过上一题,不难想到dp状态为
d
p
[
i
]
[
j
]
[
c
]
[
k
]
dp[i][j][c][k]
dp[i][j][c][k],表示前i个数放完了,还剩下j段连续空位需要填入,两头第一个数和最后一个数还有c个没有填的数字,当前的状态值为k时的方案数。我们知道,如果从小到大排序,填入数字而且“两头空”的情况,状态值需要减去
2
∗
h
[
k
]
2*h[k]
2∗h[k],一头空的情况,状态值不变,两头都有的情况,状态值需要加上
2
∗
h
[
k
]
2*h[k]
2∗h[k],另外如果填入最左端或者最右端使得c减小1,一头空则状态值需要减去
h
[
k
]
h[k]
h[k],不空则加上
h
[
k
]
h[k]
h[k],(这就是需要另一维度c的原因,填入两头的贡献是不一样的)。答案就是
0..
L
0..L
0..L的
d
p
[
n
]
[
0
]
[
0
]
[
l
]
dp[n][0][0][l]
dp[n][0][0][l]求和。
到这里问题基本解决。但是,状态值的大小的范围太大了,虽然我们知道最后的状态值应该取
0..
L
0..L
0..L,中途的状态值可能会小于或大于这个范围。一个技巧是,假设中途的空位全部消失,把这个时候新序列的状态值代替原来的。可以看出实际上就是在原来基础上加上
h
[
i
]
∗
(
j
∗
2
−
c
)
h[i]*(j*2-c)
h[i]∗(j∗2−c),显然,在新序列中插入一个数,新状态值是不减的。这样的话我们就可以在dp过程中限制状态值的范围了。
另外,这道题dp没有把n=1的情况纳入,记得特判一下。
#include<cstdio>
#include<algorithm>
#define mo 1000000007
using namespace std;
typedef long long LL;
int n,L,dp[105][105][3][1005],h[105],res;
int main()
{
scanf("%d%d",&n,&L);
if(n==1)
{
printf("1");
return 0;
}
for(int i=1;i<=n;i++)
scanf("%d",&h[i]);
sort(h+1,h+n+1);
dp[0][1][2][0]=1;
for(int i=0;i<n;i++)
for(int j=1;j<=i+1;j++)
for(int c=0;c<3;c++)
for(int tk=0,k;tk<=L;tk++)
if(dp[i][j][c][tk])
{
k=tk-h[i]*(j*2-c); //新状态值求出原来状态值
if(k-h[i+1]*2+h[i+1]*((j+1)*2-c)<=L)
(dp[i+1][j+1][c][k-h[i+1]*2+h[i+1]*((j+1)*2-c)]+=(LL)j*dp[i][j][c][tk]%mo)%=mo;
if(k+h[i+1]*(j*2-c)<=L)
(dp[i+1][j][c][k+h[i+1]*(j*2-c)]+=(LL)(j*2-c)*dp[i][j][c][tk]%mo)%=mo;
if(k+h[i+1]*2+h[i+1]*((j-1)*2-c)<=L)
(dp[i+1][j-1][c][k+h[i+1]*2+h[i+1]*((j-1)*2-c)]+=(LL)(j-c)*dp[i][j][c][tk]%mo)%=mo;
if(c)
{
if(k+h[i+1]+h[i+1]*((j-1)*2-c+1)<=L)
(dp[i+1][j-1][c-1][k+h[i+1]+h[i+1]*((j-1)*2-c+1)]+=c*dp[i][j][c][tk]%mo)%=mo;
if(k-h[i+1]+h[i+1]*(j*2-c+1)<=L)
(dp[i+1][j][c-1][k-h[i+1]+h[i+1]*(j*2-c+1)]+=c*dp[i][j][c][tk]%mo)%=mo;
}
}
for(int i=0;i<=L;i++)
(res+=dp[n][0][0][i])%=mo;
printf("%d",res);
return 0;
}
发现状态的式子还可以化简,所以最后是这个样子。
#include<cstdio>
#include<algorithm>
#define mo 1000000007
using namespace std;
typedef long long LL;
int n,L,dp[105][105][3][1005],h[105],res;
int main()
{
scanf("%d%d",&n,&L);
if(n==1)
{
printf("1");
return 0;
}
for(int i=1;i<=n;i++)
scanf("%d",&h[i]);
sort(h+1,h+n+1);
dp[0][1][2][0]=1;
for(int i=0;i<n;i++)
for(int j=1;j<=i+1;j++)
for(int c=0;c<3;c++)
for(int k=0,t;k<=L;k++)
if(dp[i][j][c][k]&&k+(j*2-c)*(h[i+1]-h[i])<=L)
{
t=k+(j*2-c)*(h[i+1]-h[i]);
(dp[i+1][j+1][c][t]+=(LL)j*dp[i][j][c][k]%mo)%=mo;
(dp[i+1][j][c][t]+=(LL)(j*2-c)*dp[i][j][c][k]%mo)%=mo;
(dp[i+1][j-1][c][t]+=(LL)(j-c)*dp[i][j][c][k]%mo)%=mo;
if(c)
{
(dp[i+1][j-1][c-1][t]+=c*dp[i][j][c][k]%mo)%=mo;
(dp[i+1][j][c-1][t]+=c*dp[i][j][c][k]%mo)%=mo;
}
}
for(int i=0;i<=L;i++)
(res+=dp[n][0][0][i])%=mo;
printf("%d",res);
return 0;
}
总之,这两题都用到了连续段数或者连续空白段数的状态,以后对这种类型的dp要留个心眼。