[BZOJ4664]count

壹、题目描述

传送门

贰、题解

有个绝对值赶脚很烦,我们考虑怎么将这个绝对值去掉,其实比较简单,按照 \(h\) 从小到大排序之后再插入,不难发现,在后面插入的书,如果它和某本书相邻,那么它一定是较大的一个,我们只需要加上其高度即可。

注意到上述分析,一本书的插入和它是否与其他书相邻有关,那么我们可以设置状态 \(\tt f[i][j][0|1|2][k]\) 表示考虑到第 \(i\) 本书,目前一共有 \(j\) 个连续段,而其中有 \(0/1/2\) 个是靠近了边界,目前总混乱度为 \(k\) 的方案数,那么我们可以考虑插入第 \(i\) 本书的转移,分几种情况进行转移即可。答案即最后只有一段的情况。

这样做确实很妙,但是由于第四维可以从大往小转移(第 \(1,3\) 种情况),这使得我们还得将超出 \(L\) 的部分全都算上,考虑这最多会达到 \(\mathcal O(nL)\) 级别,所以我们的复杂度为 \(\mathcal O(n^3L)\),无法通过。

考虑进行一些转化,使得我们的转移最后没有从大往小转移的情况。

哪里出现的从大往小转移呢?这是由于我们在插入小的 \(h\) 的时候,提前将 \(h_{big}-h_{small}\)\(h_{small}\) 部分减去了,而就是在这个时候,出现从大往小转移的。

由于我们始终有一个这样的柿子,若有一个有序的、从小到大排序的 \(a_1,a_2,...a_n\),那么有 \(a_n-a_1=(a_2-a_1)+(a_3-a_2)+...+(a_{n}-a_{n-1})\),再考虑到我们插入书本的时候其实也是这样一个有序的数列,而每次插入书本的时候,除了独立成段的情况,我们都是和前面较高的、并且处在端点上的书本放在一起的,假设我们将高度为 \(a_i\) 的书本与高度为 \(a_j\) 的书本(假定 \(a_j>a_i\))放在一起,那么,我们算的其实是

\[a_j-a_i=(a_j-a_{j-1})+(a_{j-1}-a_{j-2})+...+(a_{i+1}-a_i) \]

这实际上是什么?我们能否将其理解为 “每次从小到大放入一本高为 \(a_i\) 书之后,就将所有端点上的书本全整体拔高到 \(a_i\),并且混乱度上升 \((2\times j-t)\times (a_i-a_{i-1})\) 数值(其中 \(t\) 是占据边界的情况)”,这样,我们的转移之中就只会有混乱度上升的情况,又由于最后的混乱度在 \(L\) 的范围中,所以状态只需要到 \(L\),最后的复杂度就只有 \(\mathcal O(n^2L)\) 了,十分友善。

并且,我们可以比较容易地得到转移:

  1. 如果这本书单独成段,不贴边缘,\(f[i][j][t][k]\overset{\times (j+1-t)}{\rightarrow}f[i+1][j+1][t][nxt]\)
  2. 如果这本书独立地放在了某个边缘,\(f[i][j][t][k]\overset{\times (2-t)}\rightarrow f[i+1][j+1][t+1][nxt]\)
  3. 如果这本书放在某一段的边缘,\(f[i][j][t][k]\overset{\times (2\times j-t)}{\rightarrow}f[i+1][j][t][nxt]\)
  4. 如果这本书合并了两个块,\(f[i][j][t][k]\overset{\times (j-1)}{\rightarrow}f[i+1][j-1][t][nxt]\)
  5. 如果这本书合并了一个块和一个边缘,\(f[i][j][t][k]\overset{\times (2-t)}{\rightarrow}f[i+1][j][t+1][nxt]\)

\(nxt=k+(2\times j-t)\times (h_i-h_{i-1})\).

空间还得再滚一维。

叁、参考代码

\(\color{red}{\text{talk is s**t, show you the code.}}\)

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

typedef long long ll;
template<class T>inline T readin(T x){
	x=0; int f=0; char c;
	while((c=getchar())<'0' || '9'<c) if(c=='-') f=1;
	for(x=(c^48); '0'<=(c=getchar()) && c<='9'; x=(x<<1)+(x<<3)+(c^48));
	return f? -x: x;
}

const int maxn=100;
const int maxl=1000;
const int mod=1e9+7;

int h[maxn+5], n, L;

inline void input(){
	n=readin(1), L=readin(1);
	for(int i=1; i<=n; ++i)
		h[i]=readin(1);
	sort(h+1, h+n+1);
}

int f[2][maxn+5][3][maxl+5];
inline int add(int& x, int v){
	return x=(x+v)%mod;
}
inline void getf(){
	int cur=0, nxt=1;
	// the ways to put the first book
	f[0][1][0][0]=1, f[0][1][1][0]=2;
	for(int i=2; i<=n; ++i, swap(cur, nxt)){
		memset(f[nxt], 0, sizeof f[nxt]);
		for(int j=1; j<=i; ++j){
			for(int t=0; t<=2; ++t){
				int boost=(2*j-t)*(h[i]-h[i-1]);
				for(int k=0; k<=L-boost; ++k){
					if(!f[cur][j][t][k]) continue;
					// dependence
					add(f[nxt][j+1][t][k+boost], 1ll*f[cur][j][t][k]*(j+1-t)%mod);
					// dependently lay at the available boundary
					if(t<2) add(f[nxt][j+1][t+1][k+boost], 1ll*f[cur][j][t][k]*(2-t)%mod);
					// lay at the side of the other book
					add(f[nxt][j][t][k+boost], 1ll*f[cur][j][t][k]*(2*j-t)%mod);
					// connect two blocks
					if(j) add(f[nxt][j-1][t][k+boost], 1ll*f[cur][j][t][k]*(j-1)%mod);
					// connect a boundary and a block
					if(t<2) add(f[nxt][j][t+1][k+boost], 1ll*f[cur][j][t][k]*(2-t)%mod);
				}
			}
		}
	}
	int ans=0;
	for(int k=0; k<=L; ++k)
		add(ans, f[cur][1][2][k]);
	printf("%d\n", ans);
}

signed main(){
	input();
	if(n==1) printf("1\n");
	else getf();
	return 0;
}

肆、用到の小 \(\tt trick\)

这方法叫做插头 \(\tt DP\)?赶脚好新奇的名字,管他的,能用就行......

这个转移方法,最关键的地方在于 “整体贡献,分布处理” 的思想,或者说,从大到小转移,是因为我们由最开始的用 “最大-最小” 提供的贡献变为每次加上一点变大的,最后还是会达到 “最大-最小”,但是由于一直变大,让我们维护的范围由 \([0,nL]\) 变为 \([0,L]\),降低了一个 \(n\).

posted @ 2021-03-15 21:19  Arextre  阅读(173)  评论(0编辑  收藏  举报