[SDOI2010]地精部落 DP

LG传送门
DP好题
题意很简单,就是求1~n的排列,满足一个数两边的数要么都比它大要么都比它小,求这样的排列个数对\(p\)取膜的值(为了表述简单,我们称这样的排列为波动序列)。
这个题我第一眼看到时自然是懵逼的,然后果断看题解,题解里有五种我觉得还不错的方法,但是有些讲的不太清楚,所以我就自己写一篇。

第一种

先证两条引理(自己手玩一下就可以证明了)

引理1:在一个波动序列中,如果\(i-1\)\(i\)不相邻,交换\(i-1\)\(i\)即可得到一个新的波动序列。
引理2:把长度为\(n\)一个波动序列中的数字\(a_i\)变成\((n+1)-a_i\)会得到一个新的波动序列,且新波动序列的山峰和山谷与原序列相反。

状态与转移

\(f[i][j]\)表示由1~i这些数组成的,满足第一个数为\(j\),且\(j\)为山峰的波动序列数。
考虑转移,我先写上转移方程:
\(\qquad f[i][j]=f[i][j-1]+f[i-1][i-j+1]\)
请结合下面的文字描述理解。
由引理1知若\(j-1\)\(j\)不相邻,交换\(j-1\)\(j\)之后会得到一些波动序列,这些序列就是以\(j-1\)开头的所有波动序列,且这些序列与交换前的序列一一对应,所以应该加上一个\(f[i][j-1]\)表示以\(j\)开头的一些波动序列可以由以\(j-1\)开头的波动序列转移过来。
\(j-1\)\(j\)相邻,既然我们规定了\(j\)是山峰,那么\(j-1\)就一定是山谷,仿照上面的推导过程,由引理2中的一一对应关系可知,长度为\(i-1\)、以\(j-1\)开头且\(j-1\)为山谷的序列数与以\((i-1+1)-(j-1)\)\(i-j+1\)开头且\(i-j+1\)为山峰的序列数相同,所以需要加上一个\(f[i-1][i-j+1]\)
最后的答案就是\(2*\sum_{i=2} ^n f[n][i]\)(反正以\(1\)开头构不成波动序列即\(f[n][1]=0\),所以不用从\(1\)开始加),×2是因为上面只考虑了开头是山峰的情况,由引理2知对于每一种上面算过的情况一定有且仅有一种开头是山谷的情况与之对应。

我觉得我的证明已经很不西江月了。

代码实现

可以用滚动数组优化空间。

#include<cstdio>
using namespace std;
const int S=4211;
int f[2][S];
int main(){
	register int n,p,i,j,o=0;
	scanf("%d%d",&n,&p),f[0][2]=1;
	for(i=3;i<=n;++i)
		for(j=2;j<=i;++j)
			f[i&1][j]=(f[i&1][j-1]+f[(i-1)&1][i-j+1])%p;
	for(i=2;i<=n;++i) o=(o+f[n&1][i])%p;
	printf("%d",(o<<1)%p);
	return 0;
}

实测221ms,792kb。

第二种

一种比较朴实沉毅的方法。
首先有一个显然的结论是波动序列一定是山峰山谷交替出现的,再利用上一种解法的引理2,可知对于1~n的数山峰先出现的序列数一定与山谷先出现的序列数相等。还有一个显然的结论,只要数字的个数相同,组成波动序列的方案数就一定相同(我在这里说是显然的结论一定真的是显然的,如果你觉得不显然一定是你没有认真想) 。

状态与转移

\(f[i]\)表示1~i共有多少种先降后升的波动序列,最后答案就是\(f[n]*2\)。转移时枚举\(i\)插在第\(j\)个位置且必须在山峰(由于是先降后升的序列,所以\(j\)一定是奇数),在\(i-1\)个数中取\(j-1\)个(\(C_{i-1}^{j-1}\))放在左边(\(f[j-1]\),注意前面提到的第二条显然结论),剩下的\(i-j\)个放在右边(\(f[i-j]\),把先降后升的序列变成先升后降的序列才能与前面相接,但方案数是一样的),于是有转移方程:
\(\qquad f[i]= \sum _ {j = 1} ^iC_{i-1}^{j-1}*f[j-1]*f[i-j]\)

代码实现

同样用了滚动数组。

#include<cstdio>
using namespace std;
const int S=4211;
int c[2][S],f[S];
int main(){
	register int n,p,i,j;
	scanf("%d%d",&n,&p),c[0][0]=c[1][0]=f[0]=1;
	for(i=1;i<=n;++i)
		for(j=1;j<=i;++j){
			if(j&1) (f[i]+=1ll*f[j-1]*f[i-j]%p*c[(i-1)&1][j-1]%p)%=p;
			c[i&1][j]=(c[(i-1)&1][j]+c[(i-1)&1][j-1])%p;
		}
	printf("%d",2ll*f[n]%p);
	return 0;
}

实测397ms,842kb。

第三种

状态与转移

定义一个块表示只能在两头加入数字的一段,设\(f[i][j][k]\)表示1~i的数字被分成\(j\)块,两端上有\(k(k\leq2)\)个位置不能插入数字(即把两端的块与边界连接起来)。有三种转移:
把已有的两个块合在一起:\(f[i+1][j-1][k]+=f[i][j][k]*(j-1)\)
把两端中的某一端与边界连接起来:\(f[i+1][j][k-1]+=f[i][j][k]*(2-k)\)
新建一个块:\(f[i+1][j+1][k]+=f[i][j][k]*(j+1-k)\)
如果你怀疑为什么没有一种转移是在某个块的边上插入数字\(i\)而不把这个块与另一个块或边界连起来,注意在\(i\)之前插入的数都比\(i\)小,而在\(i\)之后插入的数都比\(i\)大,如果不把它与另一个块或边界连起来,那么在它的一边连的是比它小的数而另一边连的是比它大的数,就不符合条件了。
最后的答案就是\(f[n][1][0]+f[n][1][1]+f[n][1][2]\)

代码实现

仍然用了滚动数组。

#include<cstdio>
#include<cstring>
using namespace std;
const int S=4211;
long long f[2][S][3];
inline int min(int x,int y){return x<y?x:y;}
int main(){
	register int n,p,i,j,k,a,m;
	scanf("%d%d",&n,&p),a=1,f[1][1][0]=1;
	for(i=1;i<n;++i,a^=1){
		memset(f[a^1],0,sizeof f[a^1]),m=min(i,n-i+1);
		for(j=1;j<=m;++j)
			for(k=0;k<=2;++k)
				if(f[a][j][k]){
					if(j>1) (f[a^1][j-1][k]+=f[a][j][k]*(j-1)%p)%=p;
					if(k<2) (f[a^1][j][k+1]+=f[a][j][k]*(2-k)%p)%=p;
					(f[a^1][j+1][k]+=f[a][j][k]*(j+1-k)%p)%=p;
				}
	}
	printf("%lld",(f[a][1][0]+f[a][1][1]+f[a][1][2])%p);
	return 0;
}

实测1065ms,1036kb。

第四种

这个方法最好自己一边手动模拟一边感性理解。

状态与转移

\(f[i][j]\)表示1~i的数字构成的序列,有\(j\)个不合法的方案数。考虑到新加入的数字比前面的都大,因此它只会对它两边的数中更大的那一个造成影响,根据它造成的影响,有三种转移:
合法\(\Rightarrow\)合法:如果位于序列两头的数是山峰,就把新加入的数放在它与另一个数之间,如果是山谷,就把新加入的数放在端点上,\(f[i][j]+=f[i-1][j]*2\)
不合法\(\Rightarrow\)合法:放在一个不合法数与在他旁边比它更小的数之间,\(f[i][j]+=f[i-1][j+1]*(j+1)\)
合法\(\Rightarrow\)不合法:剩下的位置,\(f[i][j]+=f[i-1][j-1]*(i-j-1)\)
没有 不合法\(\Rightarrow\)不合法 的情况。

代码实现

一开始没用滚动数组被卡了空间。

#include<cstdio>
#include<cstring>
using namespace std;
const int S=4211;
long long f[2][S];
int main(){
	register int n,m,p,i,j,a=1;
	scanf("%d%d",&n,&p),f[a][0]=4,f[a][1]=2;
	for(i=4;i<=n;++i){
		a^=1,memset(f[a],0,sizeof f[a]);
		for(j=0,m=i-1;j<m;++j){
			(f[a][j]+=f[!a][j]<<1)%=p;
			(f[a][j]+=f[!a][j+1]*(j+1))%=p;
			if(j) (f[a][j]+=f[!a][j-1]*(i-j-1))%=p;
		}
	}
	printf("%lld",f[a][0]);
	return 0;
}

实测849ms,812kb。

第五种

其实与第四种比较类似,但是洛谷上的题解有点问题,所以在这里说一下。

状态与转移

\(f[i][j]\)表示1~i的数字构成的序列,有\(j\)个不合法且这个数的后一个数比它大的方案数。考虑到新加入的数字比前面的都大,因此它只会对它两边的数中更大的那一个造成影响,根据它造成的影响,有三种转移:
合法\(\Rightarrow\)合法:如果位于序列尾的数是山峰,就把新加入的数放在它之前,如果是山谷,就把新加入的数放在它之后,\(f[i+1][j]+=f[i][j]\)
不合法\(\Rightarrow\)合法:放在一个不合法数之前,\(f[i+1][j-1]+=f[i][j]*j\)
合法\(\Rightarrow\)不合法:剩下的位置,\(f[i+1][j+1]+=f[i][j]*(i-j)\)
没有 不合法\(\Rightarrow\)不合法 的情况。
注意这里的状态与上一种解法的不同点,答案是\(f[n][0]*2\)

代码实现

相信你不敢不用滚动数组 然而一开始没用滚动数组似乎能过

#include<cstdio>
#include<cstring>
using namespace std;
const int S=4211;
long long f[2][S];
int main(){
	register int n,p,i,j,a=1;
	scanf("%d%d",&n,&p),f[a][0]=1;
	for(i=1;i<=n;++i,a^=1){
		memset(f[!a],0,sizeof f[!a]);
		for(j=0;j<=i;++j){
			(f[!a][j]+=f[a][j])%=p;
			if(j) (f[!a][j-1]+=f[a][j]*j)%=p;
			(f[!a][j+1]+=f[a][j]*(i-j))%=p;
		}
	}
	printf("%lld",(f[!a][0]<<1)%p);
	return 0;
}

实测1139ms,804kb。

对这五种解法的总结

等我多做点题再说吧。

posted @ 2018-11-16 10:35  newbiechd  阅读(394)  评论(2编辑  收藏  举报