【lgj】【排列 DP】奶牛安家
有 头奶牛,在一条数轴上选择自己的家。数轴上有 个整点,分别是 至 。每头奶牛都选择一个不同的整点作为的自己的家。第 头奶牛有一个安全距离 ,表示的意义是第 头奶牛与它的邻居的距离不能小于 。也就是说,如果奶牛 和奶牛 是邻居,那么这两头奶牛之间的距离不能小于。输出有多少种不同的合法安家方案,答案模 。
输入格式
第一行,一个整数 ,表示有 组测试数据。。
每组测试数据格式如下:
第一行,两个整数: 和 。 ,。
第二行, 个整数,第 个整数是 。 。
(倾情搬运,原题面是英文的,用了 LGJ 的题面)
如果我们知道了某个排列需要多少空间,就可以用插板法算出来该排列的情况。
定义 为还没有处理 时,有 个排列,需要 个位置,注意,这里需要的位置不包括排列最左,最右两边的空位,否则有后效性。
同样是为了避免后效性,将 升序排序。不然区间合并的时候不知道代价。
可以新增一个排列,或在当前所有排列的左,右延申,或者将两个排列合并。
那么状态转移方程就是(搬运链接,感谢这位 dalao):
为什么要这样设置呢?
如果没有新增再合并这一操作,我们无法将一个新元素放到一个排列的非中间位置。那
为什么在延伸时候要区分左,右 (),但是合并却不要?
因为延伸的时候,左,右会使得排列的长相不同,比如 延申后变成 和 ,但是合并的时候,状态已经包含了这两种情况,所以不能算两次。
/*
- 别摆了
- By yfz
*/
#include<bits/stdc++.h>
using namespace std;
//#define int long long
const int N = 45, mod = 1e9+7, M = 1e5+10;
struct D{
int f[N][N][N*N],n,d; // 还没处理 i,有 j 个排列,需要 k 的空间
int fac[M],inv[M];
int mi(int x,int k) {int p; return k?p=mi(x,k>>1),1ll*p*p%mod*(k&1?x:1)%mod:1;}
void init() {
fac[0] = inv[0] = 1;
for(int i=1;i<M;i++) {
fac[i] = 1LL * fac[i-1] * i % mod;
}
inv[M-1] = mi(fac[M-1],mod-2);
for(int i=M-2;i;i--) inv[i] = 1LL * inv[i+1] * (i+1) % mod;
}
int C(int n,int m) {
// return 1;
return 1LL * fac[n] * inv[n-m] % mod * inv[m] % mod;
}
int a[N];
int main() {
init();
cin>>n>>d;
for(int i=1;i<=d;i++) cin>>a[i];
sort(a+1,a+d+1);
f[1][0][0]= 1;
for(int i=1;i<=d;i++) {
for(int j=0;j<=d;j++) {
for(int k=0;k<=min(1600,n);k++) {
(f[i+1][j][k+a[i]] += 1LL * f[i][j][k] * 2 % mod * j % mod) %= mod;
(f[i+1][j+1][k] += f[i][j][k]) %= mod;
if(j > 0)
(f[i+1][j-1][k+a[i] * 2] += 1LL * f[i][j][k] * j % mod * (j-1) % mod) %= mod;
}
}
}
int ans = 0;
for(int k=1;k<=min(n,1600);k++) {
(ans += 1ll * f[d+1][1][k-1] * C(n-k+d,d) % mod) %= mod;
}
// cout<<f[3][1][3]<<endl;
cout<<ans;
return 0;
}
}F[5];
signed main() {
int T; cin>>T;
while(T--) {
F[T].main();
}
return 0;
}