SDOI 2010 地精部落 DP

题目链接bzoj点我:-) 洛谷点我:-)

题目描述
给你n,p,请你求出满足这个条件的由1至n这n个数组成的排列有多少种,答案对p取模:即任意的连续上升或下降的一段数字区间长度不超过2(也称震荡序列)

数据范围
对于20%的数据,满足N≤10;
对于40%的数据,满足N≤18;
对于70%的数据,满足N≤550;
对于100%的数据,满足3≤N≤4200,P≤109

思路
定义f[i][j][k]为取1i的数字,它们在最终序列中被分成j块,并且两头的块加新的大数字不合法的个数为k(0至2)
转移方程:
1.把原来两块的连起来:f[i+1][j1][k]+=f[i][j][k](j1)
2.和其中一块连起来:f[i+1][j][k+1]+=f[i][j][k](2k)
3.自己单独创块:f[i+1][j+1][k]+=f[i][j][k](j+1k)

答案:f[n][1][0]+f[n][1][1]+f[n][1][2]
再把数组滚一滚就可以了

感想
也是打算练基础的一道题,这么好的一道题,可惜我太菜了做不出来,网上大部分题解不是我这个东西,但是。。还是找个时间写写那等玄学方法好。这种DP,以前碰到过,那个浙江的波浪,还没打代码的,啊赶紧要写了。
另外,这个方法很神奇啊,可能有人问那个我的数字在与任意一个块连接起来的时候,为啥只管两边的,你想想如果加在中间,肯定不合法啊是吧,我以后加的数越来越大了啊,这个数一边比它小一边比它大啊。。
我觉得这种方法最最重要的,一是它的块的位置是灵活的,只有一个相对的位置,你不用管太多譬如它到底在原序列什么地方啊,你根本不用想这些,你想让它中间有多少空就有多少空,二是它加数字的时候是从小到大的,所以啊,很多神奇的性质就满足了,自己慢慢领会吧

代码

//miaomiao 2017.2.1
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<iostream>
#include<algorithm>

using namespace std;

#define LL long long
#define Set(a, v) memset(a, v, sizeof(a))
#define For(i, a, b) for(int i = (a); i <= (int)(b); i++)
#define Forr(i, a, b) for(int i = (a); i >= (int)(b); i--)

#define N (4200+5)

LL f[2][N][3];

int main(){
    int n, P;
    scanf("%d%d", &n, &P);

    int a = 1;
    f[1][1][0] = 1;
    For(i, 1, n-1){
        Set(f[a^1], 0);
        For(j, 1, min(i, n-i+1)) For(o, 0, 2)
            if(f[a][j][o]){
                if(j > 1) f[a^1][j-1][o] = (f[a^1][j-1][o]+f[a][j][o]*(j-1)%P)%P;
                if(o < 2) f[a^1][j][o+1] = (f[a^1][j][o+1]+f[a][j][o]*(2-o)%P)%P;
                f[a^1][j+1][o] = (f[a^1][j+1][o]+f[a][j][o]*(j+1-o)%P)%P;
            }
        a^=1;
    }
    printf("%lld\n", (f[a][1][0]+f[a][1][1]+f[a][1][2])%P);

    return 0;
}
posted @ 2017-02-01 23:50  Miao_miao  阅读(104)  评论(0编辑  收藏  举报