[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。
对这五种解法的总结
等我多做点题再说吧。
[SDOI2010]地精部落 DP的更多相关文章
- BZOJ 1925: [Sdoi2010]地精部落( dp )
dp(i,j)表示1~i的排列中, 以1~j为开头且开头是下降的合法方案数 这种数列具有对称性, 即对于一个满足题意且开头是上升的n的排列{an}, 令bn = n-an+1, 那么{bn}就是一个满 ...
- [BZOJ1925][SDOI2010]地精部落(DP)
题意 传说很久以前,大地上居住着一种神秘的生物:地精. 地精喜欢住在连绵不绝的山脉中.具体地说,一座长度为 N 的山脉 H可分 为从左到右的 N 段,每段有一个独一无二的高度 Hi,其中Hi是1到N ...
- 【BZOJ】1925: [Sdoi2010]地精部落 DP+滚动数组
题目链接:http://www.lydsy.com/JudgeOnline/problem.php?id=1925 题意:输入一个数N(1 <= N <= 4200),问将这些数排列成折线 ...
- Luogu2467 SDOI2010 地精部落 DP
传送门 一个与相对大小关系相关的$DP$ 设$f_{i,j,0/1}$表示放了$i$个,其中最后一个数字在$i$个中是第$j$大,且最后一个是极大值($1$)或极小值时($0$)的方案数.转移: $$ ...
- P2467 [SDOI2010]地精部落 DP
传送门:https://www.luogu.org/problemnew/show/P2467 参考与学习:https://www.luogu.org/blog/user55639/solution- ...
- Luogu 2467[SDOI2010]地精部落 - DP
Solution 这题真秒啊,我眼瞎没有看到这是个排列 很显然, 有一条性质: 第一个是山峰 和 第一个是山谷的情况是一一对应的, 只需要把每个数 $x$ 变成 $n-x+1$ 然后窝萌定义数组 $ ...
- 【BZOJ1925】[Sdoi2010]地精部落 组合数+DP
[BZOJ1925][Sdoi2010]地精部落 Description 传说很久以前,大地上居住着一种神秘的生物:地精. 地精喜欢住在连绵不绝的山脉中.具体地说,一座长度为 N 的山脉 H可分 为从 ...
- 【BZOJ1925】[SDOI2010]地精部落(动态规划)
[BZOJ1925][SDOI2010]地精部落(动态规划) 题面 BZOJ 洛谷 题解 一道性质\(dp\)题.(所以当然是照搬学长PPT了啊 先来罗列性质,我们称题目所求的序列为抖动序列: 一个抖 ...
- [BZ1925] [SDOI2010]地精部落
[BZ1925] [SDOI2010]地精部落 传送门 一道很有意思的DP题. 我们发现因为很难考虑每个排列中的数是否使用过,所以我们想到只维护相对关系. 当我们考虑新的一个位置时,给新的位置的数分配 ...
随机推荐
- 3.2 Spark内置RPC框架
实现的HttpFileServer,但在Spark 2.0.0版本中它也被废弃了,现在使用的是基于Spark内置RPC框架的NettyStreamManager.节点间的Shuffle过程和Block ...
- 20165302实验二java面向对象程序设计
20165302实验二java面向对象程序设计 实验结果 提交点1 1.实验要求: 参考 (http://www.cnblogs.com/rocedu/p/6371315.html#SECUNITTE ...
- 配置mysql允许远程链接
默认情况下,mysql帐号不允许从远程登陆,只能在localhost登录.本文提供了二种方法设置mysql可以通过远程主机进行连接. 修改用户表的数据 登入mysql后,更改 mysql 数据库里的 ...
- C/C++结构体字节对齐详解
结构体的sizeof先看一个结构体:struct S1{ char c; int i;}; sizeof(S1)在VC6中按默认设置得到的结果为8.我们先看看sizeof的定义——size ...
- 【HTML5-基础-SVG实践】
关于svg HTML页面常用加载svg图片方式: HTML元素 // data 和 type 至少指定一项 <object data = './public/icon.svg' width='2 ...
- JavaScript互斥锁案例
朋友今天问起来关于JS中多个函数共享同一个全局变量时,顺序调用执行的函数,前者修改了全局变量值,后调用的函数访问时却为undefined. 前不久开发项目过程中,队友也遇到了同样的问题,索性就写份博客 ...
- ODI使用流程
---恢复内容开始--- ODI流程 Topology 1.建立 源 物理结构体系 2.建立 目的 物理结构体系 步骤同上 3.建立 源 逻辑架构 4.建立 目的 逻辑架构 步骤同上 Designer ...
- ios开发UI篇—UISlider
概述 UISlider用于从连续范围的值中选择单个值的控件. 当您移动滑块的大拇指时,会将其更新后的值传递给附加的任何动作.滑块的外观是可配置的; 您可以对曲目和大拇指进行着色,并提供出现在滑块末端的 ...
- Java职业规划
java学习这一部分其实也算是今天的重点,这一部分用来回答很多群里的朋友所问过的问题,那就是我你是如何学习Java的,能不能给点建议?今天我是打算来点干货,因此咱们就不说一些学习方法和技巧了,直接来谈 ...
- ajax 动态载入html后不能执行其中的js解决方法
事件背景 有一个公用页面需要在多个页面调用,其中涉及到部分js已经写在了公用页面中,通过ajax加载该页面后无法执行其中的js. 解决思路 1. 采用附加一个iframe的方法去执行js,为我等代码洁 ...