Luogu P2467 [SDOI2010]地精部落 | 神奇的dp

题目链接

DP

 

题目大意:给定一个数n,求1~n这n个整数的所有排列中有多少个波动数列,将这个数量%p后输出。

 

什么是波动数列呢?顾名思义,就是一个大、一个小、一个大、一个小……或者是一个小、一个大、一个小、一个大……像“5,2 ,3,1,4”和“2,3,1,5,4”这样的数列就叫做波动数列,题目里也很形象地说了。

 

首先,关于波动数列,我们可以推出两条性质:

 

性质一:在一个波动数列中,若一个数x与另一个数(x+1)不相邻,那么交换这两个数的位置就可以得出一个新的波动数列

 

例子:波动数列“1,3,2,5,4”中的“1”与“2”不相邻,那么交换“1”与“2”的位置就可以得出新的波动数列“2,3,1,5,4”。

 

证明:对于符合这个性质的波动数列我们可以这样表示:{...... a,x,b,......,c,(x+1),d,.......}(省略号表示的是数列中的其它数),那么有四种情况:

 

情况一:当x为山谷,(x+1)也为山谷时,a>x,b>x,c>x+1,d>x+1。

①因为此题中的波动数列没有重复的数,所以a!=x+1,b!=x+1,所以a>x+1,b>x+1,所以当(x+1)移动到x的位置后不会破坏波动数列。

②因为c>x+1,d>x+1,所以c>x,d>x,所以当x移动到(x+1)的位置后不会破坏波动数列。

 

情况二:当x为山峰,(x+1)为山谷时,a<x,b<x,c>x+1,d>x+1。

①因为a<x,b<x,所以a<x+1,b<x+1,所以当(x+1)移动到x的位置后不会破坏波动数列。

②同情况一第②条

 

情况三:当x为山谷,(x+1)为山峰时,a>x,b>x,c<x+1,d<x+1。

①同情况一第①条。

②因为此题中的波动数列没有重复的数,所以c!=x,d!=x,所以c<x,d<x,所以当x移动到(x+1)的位置后不会破坏波动数列。

 

情况四:当x为山峰,(x+1)也为山峰时,a<x,b<x,c<x+1,d<x+1。

①同情况二第①条。

②同情况三第②条。

 

综上所述,将x与(x+1)在数列中的位置交换后可得出新的波动数列。

 

性质二:在一个为n的波动数列中,将每个数x都变成(n-x+1)就可以得出一个新的波动数列

 

例子:波动数列“3,2,4,1”按上述步骤操作后可变成新波动数列“2,3,1,4”。

 

证明:这个操作其实就是把最大的数变成最小的数,最小的数变成最大的数,次大的数变成次小的数,次小的数变成次大的数……假如有这样一个山峰: a,b,c,其中a<b,c<b,那么它变形后就是这样子的:n-a+1,n-b+1,n-c+1。将变形后的三个数同时减去(n+1),即可得出-a,-b,-c,由a<b,c<b可知-a>-b,-c>-b,又因为变形后的三个数同时减去同一个数后大小关系是不变的,所以可得n-a+1>n-b+1,n-c+1>n-b+1。我们可以发现,山峰变成了山谷,但这并没有破坏波动数列,所以这是没有影响的。如果这变形之前是一个山谷也是一样的道理,大家可以自己动脑想一下。综上所属,将一个波动数列按性质二中的步骤操作后可以得出一个新的波动数列。

 

知道了这三条性质之后,我们就能用动态规划来解决这一问题了,状态就是f[i][j],这表示前i个数,以j为开头且j为山峰时的方案数。为什么只考虑开头为山峰时的方案数呢?因为根据性质二,将开头为山峰的、长度为n的波动数列中的每个数x都变成(n-x+1)后就是一个开头为山谷的波动数列了(证明中有写到),所以我们最后只要将开头为山峰的方案数2就好了。

 

有了状态之后,剩下的问题就是如何转移了,这里我先给出状态转移方程吧,如下:

 

                        f[i][j]=f[i][j-1]+f[i-1][i-j+1]

 

思路:对于状态转移,我们考虑从(j-1)转移过来。那么这里就有两种情况,一种是j与(j-1)不相邻,另一种是j与(j-1)相邻。

 

j与(j-1)不相邻的情况很好处理,因为根据性质一,若(j-1)与j不相邻,那么交换(j-1)与j的位置即可得出一个新的波动数列,所以这一种情况的方案数即为f[i][j-1]。有人可能会问,如果f[i][j-1]中包含了(j-1)与j相邻的情况怎么办?这样一交换不就错了吗?这一种情况是不可能出现的,因为f[i][j-1]是表示前i个数中以(j-1)为开头且(j-1)为山峰的方案数,那么与(j-1)相邻的只能是第二个数且第二个数一定比(j-1)要小,而j又比(j-1)要大,所以在f[i][j-1]所包含的方案中j是不会与(j-1)相邻的。

 

j与(j-1)相邻的情况其实也不太难想,大家请想一想,此时(j-1)紧紧地跟在j后面,这不就变成了求有i个数,(j-1)为开头且(j-1)为山谷的方案数吗?但这与我们所设立的状态表示的意义不同怎么办?没关系的,我们可以把它变一变。j后面的数都在[1,j-1][j+1,i]这两个区间内,一共有(i-1)个数。我们可以将[j+1,i]这个区间内的数全部减去1,就变成了[j,i-1],再与前一个区间合并一下,变成[1,i-1],这样减是不会改变数之间的相对的大小关系的,所以方案数也不会变。但此时的开头(j-1)仍是山谷啊,没错,但根据性质二,将开头为山谷的波动数列按性质中的步骤操作一番就变成开头为山峰的波动数列啦,反过来也一样。那么,我们只要求出以[(i-1)-(j-1)+1]为开头且为山峰的方案数就好了(再反一次就变回以(j-1)为开头且为山谷的方案数了嘛)。所以,这一种情况的方案数即为f[i-1][(i-1)][(i-1)-(j-1)+1],化简后就是f[i-1][i-j+1]

 

综上所述,状态转移方程即为f[i][j]=f[i][j-1]+f[i-1][i-j+1]

 

最后,极短的代码奉上:

 

#include<iostream>
#include<cstdio>
    using namespace std;
    int f[4205][4205];
int main()
{
    int n=0,p=0,ans=0;
    scanf("%d%d",&n,&p);
    f[1][1]=1;//初始化
    for(int i=2;i<=n;i++)
        for(int j=2;j<=i;j++)//1是不可能做为山峰的,所以从2开始枚举
            f[i][j]=(f[i][j-1]+f[i-1][i-j+1])%p;
    for(int i=1;i<=n;i++) ans=(ans+f[n][i])%p;
    printf("%d",(ans*2)%p);
    return 0;
}

 

 

 

参考文章:https://www.luogu.org/blog/user55639/solution-p2467

 

posted @ 2018-07-17 21:10  wozaixuexi  阅读(149)  评论(0编辑  收藏  举报