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。

对这五种解法的总结

等我多做点题再说吧。

[SDOI2010]地精部落 DP的更多相关文章

  1. BZOJ 1925: [Sdoi2010]地精部落( dp )

    dp(i,j)表示1~i的排列中, 以1~j为开头且开头是下降的合法方案数 这种数列具有对称性, 即对于一个满足题意且开头是上升的n的排列{an}, 令bn = n-an+1, 那么{bn}就是一个满 ...

  2. [BZOJ1925][SDOI2010]地精部落(DP)

    题意 传说很久以前,大地上居住着一种神秘的生物:地精. 地精喜欢住在连绵不绝的山脉中.具体地说,一座长度为 N 的山脉 H可分 为从左到右的 N 段,每段有一个独一无二的高度 Hi,其中Hi是1到N ...

  3. 【BZOJ】1925: [Sdoi2010]地精部落 DP+滚动数组

    题目链接:http://www.lydsy.com/JudgeOnline/problem.php?id=1925 题意:输入一个数N(1 <= N <= 4200),问将这些数排列成折线 ...

  4. Luogu2467 SDOI2010 地精部落 DP

    传送门 一个与相对大小关系相关的$DP$ 设$f_{i,j,0/1}$表示放了$i$个,其中最后一个数字在$i$个中是第$j$大,且最后一个是极大值($1$)或极小值时($0$)的方案数.转移: $$ ...

  5. P2467 [SDOI2010]地精部落 DP

    传送门:https://www.luogu.org/problemnew/show/P2467 参考与学习:https://www.luogu.org/blog/user55639/solution- ...

  6. Luogu 2467[SDOI2010]地精部落 - DP

    Solution 这题真秒啊,我眼瞎没有看到这是个排列 很显然, 有一条性质: 第一个是山峰 和 第一个是山谷的情况是一一对应的, 只需要把每个数 $x$  变成 $n-x+1$ 然后窝萌定义数组 $ ...

  7. 【BZOJ1925】[Sdoi2010]地精部落 组合数+DP

    [BZOJ1925][Sdoi2010]地精部落 Description 传说很久以前,大地上居住着一种神秘的生物:地精. 地精喜欢住在连绵不绝的山脉中.具体地说,一座长度为 N 的山脉 H可分 为从 ...

  8. 【BZOJ1925】[SDOI2010]地精部落(动态规划)

    [BZOJ1925][SDOI2010]地精部落(动态规划) 题面 BZOJ 洛谷 题解 一道性质\(dp\)题.(所以当然是照搬学长PPT了啊 先来罗列性质,我们称题目所求的序列为抖动序列: 一个抖 ...

  9. [BZ1925] [SDOI2010]地精部落

    [BZ1925] [SDOI2010]地精部落 传送门 一道很有意思的DP题. 我们发现因为很难考虑每个排列中的数是否使用过,所以我们想到只维护相对关系. 当我们考虑新的一个位置时,给新的位置的数分配 ...

随机推荐

  1. Ubuntu root 密码忘记-恢复

    @Ubuntu root 密码忘记-恢复 2012-04-27 11:09:22 方法一: 如果用户具有sudo权限,那么直接可以运行如下命令: #sudo su root #passwd #更改密码 ...

  2. DP入门——01背包 & 完全背包

    01背包: 采药: https://www.luogu.org/problemnew/show/P1048 #include <iostream> #include <algorit ...

  3. I、Q信号是如何产生的,I、Q信号复用的作用

    接收机在中频部分实现模数变换和采样,采样后的信号和数字域的同频相乘,就可以得到基带的I.Q分量.在无线接口传输时,每一种使用特定的载波频率.码(扩频码和扰码)以及载波相对相位(I或Q)的信道都可以理解 ...

  4. Bayesian Theorem

  5. Reading Meticulous Measurement of Control Packets in SDN

    SOSR 17 概要 网络流量中有一部分是用于网络管理,(根据packet process survey,该部分流量属于包转发的slow path部分)由于sdn的数控分离,交换机需要向控制器发送大量 ...

  6. Python 学习笔记(十五)Python类拓展(一)继承

    继承 继承(Inheritance):是面向对象软件技术当中的一个概念.如果一个类别A "继承自" 另一个类B,就把这个A称为“B的子类”,而把B称为“A的父类”,也可以称“B是A ...

  7. 聊聊编程开发的数据库批量插入(sql)

    这里的批量插入,主要是支持SQL的大型存储数据库,本文以Mysql,Oracle,SqlServer,postgresql4类来说明,这大概是国内应用比较多的了.其余的应该可以按照这些去找.提到编程的 ...

  8. JBDC—③数据库连接池的介绍、使用和配置

    首先要知道数据库连接(Connection对象)的创建和关闭是非常浪费系统资源的,如果是使用常规的数据库连接方式来操作数据库,当用户变多时,每次访问数据库都要创建大量的Connnection对象,使用 ...

  9. C++程序设计入门(上) string类的基本用法

    string类中的函数 1. 构造 2. 追加 3. 赋值 4. 位置与清除 5. 长度与容量 6. 比较 7. 子串 8. 搜索 9. 运算符 追加字符串 string s1("Welc ...

  10. sublime Text3安装 markdownediting 报错 解决记录

    看了一下官方文档,也是醉了,都怪自己的无知. 在安装时候不要打开.md的文件,因为你里面有可能有一些语法错误,所以会导致报错. 解决方法关闭其他文件,在重新安装! 官方解释: 参考文档:Trouble ...