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. 【转】Faster RCNN 原理

    看过好几篇讲Faster RCNN的文章,有一些基础以后,看这个文章是最好的. https://www.cnblogs.com/wangyong/p/8513563.html

  2. 【【模板】严格次小生成树[BJWC2010]】

    树上的路径怎么能没有树剖 显然,次小生成树和最小生成树只在一条边上有差距,于是我们就可以枚举这一条边,将所有边加入最小生成树,之后再来从这些并不是那么小的生成树中找到那个最小的 我们往最小生成树里加入 ...

  3. adb命令篇 (转载)

    转自:https://www.cnblogs.com/ailiailan/p/7896534.html 1.抓log方法  (bat文件)  mkdir D:\logcat set /p miaosh ...

  4. 用firefox的插件下载网页中的视频

    对于网页中的一些视频,直接下载不了,可以用专用下载软件下载,也可以用firefox的NetVideohunter Video Downloader插件下载网页中的视频,方便快捷. 工具/原料   fi ...

  5. 6、Dubbo-配置(1)

    覆盖关系 下图展示了配置覆盖关系的优先级,从上到下优先级依次降低: 参考官网:http://dubbo.apache.org/zh-cn/docs/user/configuration/configu ...

  6. [运维笔记] Nginx编译安装

    yum -y install pcre-devel.x86_64 yum -y install openssl openssl-devel.x86_64 useradd www -s /sbin/no ...

  7. Linux内存管理-高端内存(二)

    在支持MMU的32位处理器平台上,Linux系统中的物理存储空间和虚拟存储空间的地址范围分别都是从0x00000000到0xFFFFFFFF,共4GB,但物理存储空间与虚拟存储空间布局完全不同.Lin ...

  8. JDBC——释放资源的代码

    public static void release(ResultSet rs, Statement statement, Connection conn) { if (rs != null) { t ...

  9. PyCharm2018激活码

    亲测可用: 来自:https://blog.csdn.net/u014044812/article/details/78727496

  10. MySQL的笔记

    一.   SELECT tmp2.name,tmp2.browseNum FROM (SELECT tmp.`name`, COUNT(tmp.id) AS browseNum FROM(SELECT ...