[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题. 我们发现因为很难考虑每个排列中的数是否使用过,所以我们想到只维护相对关系. 当我们考虑新的一个位置时,给新的位置的数分配 ...
随机推荐
- 【jQuery mobile】启程跨平台开发之旅
APICloud创建跨平台应用有两种方法,一种在云端直接创建,一种是在APICloud Studio中创建. 创建一个应用 1.注册账号 2.创建HelloApp应用 3.留意应用的ID . 4.下载 ...
- 【Vue】hello world
参考链接:http://www.jianshu.com/p/5ba253651c3b 1.Vue 是一个前端框架,特点是数据绑定.组件化 如果你之前已经习惯了用jQuery操作DOM,学习Vue.js ...
- 20145314郑凯杰 《Java程序设计》第10周学习总结
20145314郑凯杰 <Java程序设计>第10周学习总结 代码托管: 学习内容总结 网络编程 会打手机吗? 第一个问题:会打手机吗?很多人可能说肯定会啊,不就是按按电话号码,拨打电话嘛 ...
- idea 注册码(2019)
MTW881U3Z5-eyJsaWNlbnNlSWQiOiJNVFc4ODFVM1o1IiwibGljZW5zZWVOYW1lIjoiTnNzIEltIiwiYXNzaWduZWVOYW1lIjoiI ...
- JDK(七)JDK1.8源码分析【集合】TreeMap
本文转载自joemsu,原文链接 [JDK1.8]JDK1.8集合源码阅读——TreeMap(二) TreeMap是JDK中一种排序的数据结构.在这一篇里,我们将分析TreeMap的数据结构,深入理解 ...
- iOS 关闭图片渲染
在为Button 设置背景图片的时候, 会发现显示的效果和UI给的图片不一样, 往往是把图片显示成为蓝色, 这是因为在新版的iOS中, 会自动对图片渲染. 我们只要把图片渲染关掉就OK了 - (UII ...
- 修改jupyter notebook的默认路径
我的系统环境是win10,安装了anaconda3 for python 3.6.6首先需要配置notebook的变量环境:打开 cmd 输入命令 jupyter notebook --generat ...
- jQuery之scroll用法实例
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- 不可变字符串String与可变字符串StringBuilder、StringBuffer使用详解
String字符串 char类型只能表示一个字符,而String可以表示字符串,也就是一个字符序列.但String不是基本类型,而是一个定义好的类,是一个引用类型.在Java中,可以将字符串直接量赋给 ...
- Ionic3项目实践记录
Ionic3首次项目实践记录 标签(空格分隔): Angular Ionic Ionic3踩坑 1. 路由懒加载(lazy load) 如果设置了懒加载,就必须全部懒加载(包括TabsPage),否则 ...