JZOJ6439. 【GDOI2020模拟01.17】小 ω 数排列

题目大意

一个长度为 n n n的序列 a a a满足每个元素互不相同。试求有多少个种排列方式使得相邻两数只差的绝对值之和小于等于 L L L n < = 100 , L < = 1000 , 1 < = A i < = 1000 n <= 100,L <= 1000,1 <= Ai <= 1000 n<=100,L<=1000,1<=Ai<=1000

分析

这是一道打着简单思考难度较大的 D P DP DP题。首先按照一般的想法一定是从原序列中随机挑出若干个元素按顺序放入新序列中,如果这样做的话难以避免这个烦人的绝对值,且会发现时间复杂度十分不优秀。正难则反,我们不妨考虑先将原序列排好序,然后按顺序选取并随机放到新序列中,那么新序列会形成若干段。所以我们设状态 f [ i ] [ j ] [ k ] [ l ] f[i][j][k][l] f[i][j][k][l]表示排序后的原序列中选取了前 i i i个,在新序列中形成了 j j j段,总和为 k k k,左右断点一共放了 l l l个时的方案数。
那么转移分为五种:

  • 插入一个独立的段, f [ i + 1 ] [ j + 1 ] [ t ] [ l ] + = f [ i ] [ j ] [ k ] [ l ] ∗ ( j + 1 − l ) f[i + 1][j + 1][t][l] += f[i][j][k][l] * (j + 1 - l) f[i+1][j+1][t][l]+=f[i][j][k][l](j+1l)
  • 插入在一个段的左右两边, f [ i + 1 ] [ j ] [ t ] [ l ] + = f [ i ] [ j ] [ k ] [ l ] ∗ ( 2 ∗ j − l ) f[i +1][j][t][l] += f[i][j][k][l] * (2 * j - l) f[i+1][j][t][l]+=f[i][j][k][l](2jl)
  • 插入在两个段中间将两个段合并, f [ i + 1 ] [ j − 1 ] [ t ] [ l ] + = f [ i ] [ j ] [ k ] [ l ] ∗ ( j − 1 ) f[i +1][j - 1][t][l] += f[i][j][k][l] * (j - 1) f[i+1][j1][t][l]+=f[i][j][k][l](j1)
  • 插入一个独立且在边界上的段, f [ i + 1 ] [ j + 1 ] [ t ] [ l + 1 ] + = f [ i ] [ j ] [ k ] [ l ] ∗ ( 2 − l ) f[i + 1][j + 1][t][l + 1] += f[i][j][k][l] * (2 - l) f[i+1][j+1][t][l+1]+=f[i][j][k][l](2l)
  • 插入在边界上且与相邻的段合并, f [ i + 1 ] [ j ] [ t ] [ l + 1 ] + = f [ i ] [ j ] [ k ] [ l ] ∗ ( 2 − l ) f[i +1][j][t][l + 1] += f[i][j][k][l] *(2 - l) f[i+1][j][t][l+1]+=f[i][j][k][l](2l)

其中 t t t表示加入 a [ i + 1 ] a[i + 1] a[i+1]元素后新的总和,那么上述转移是非常显而易见的。但是单纯的这么转移我们会发现 t t t那一维的范围非常不友善,所以还要改进。

  • 发现我们可以分步算贡献,我们如果所有未闭合的端点左右都填 x x x的总和。可以发现如果当前 x x x变大了,这个总和会增加 ∆ x × ( 2 j − l ) ∆x × (2j − l) x×(2jl)

这个操作比较妙,形象一点地说就是每次将每段的两头用 a [ i ] a[i] a[i]补齐,那么在转移过程中就可以保证第三维可以处在一个比较合理的范围。说着比较抽象可以自己写写画画,或者看看下面简洁的代码或许更好懂。

	#include <cstdio>
#include <algorithm>
#include <cstring>
#define mem(a) memset(a,0,sizeof a)
#define ll long long
using namespace std;

const ll mo = 1e9 + 7;
const int N = 110;
const int LEN = 1100;
int n,L,a[N],s = 0;
ll ans,f[2][N][LEN][3];

int read()
{
	int x = 0;
	char ch = getchar();
	while (ch < '0' || ch > '9') ch = getchar();
	while (ch >= '0' && ch <= '9') {x = x * 10 + ch - '0'; ch = getchar();}
	return x;
}

int main()
{
	freopen("count.in","r",stdin);
	freopen("count.out","w",stdout);
	n = read(),L = read();
	if (n == 1) {printf("%d\n",1); return 0;}
	for (int i = 1; i <= n; i ++) a[i] = read();
	sort(a + 1,a + 1 + n);
	f[0][0][0][0] = 1;
	for (int i = 0; i < n; i ++,s ^= 1)
	{
		mem(f[s ^ 1]);
		for (int j = 0; j <= i; j ++)
			for (int k = 0; k <= L; k ++)
				for (int l = 0; l <= 2; l ++)
				{
					int t = k + (a[i + 1] - a[i]) * (2 * j - l);
					if (t > L) continue;
					(f[s ^ 1][j + 1][t][l] += f[s][j][k][l] * (j + 1 - l) % mo) %= mo;
					if (j)
						(f[s ^ 1][j - 1][t][l] += f[s][j][k][l] * (j - 1) % mo) %= mo,
						(f[s ^ 1][j][t][l] += f[s][j][k][l] * (2 * j - l) % mo) %= mo;
					if (l < 2) (f[s ^ 1][j + 1][t][l + 1] += f[s][j][k][l] * (2 - l) % mo) %= mo;
					if (l < 2 && j) (f[s ^ 1][j][t][l + 1] += f[s][j][k][l] *(2 - l) % mo) %= mo;
				}
	}
	for (int i = 0; i <= L; i ++)
		ans = (ans + f[s][1][i][2]) % mo;
	printf("%lld\n",ans);
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值