P5999 [CEOI2016] kangaroo 蓝 题解
前言
有些题目照常 DP 不是很好做,感觉像是区间 DP,但是怎么设状态都不好转移,那么可以考虑一种维护块儿的 DP,就是这道题要用到的知识点。
背景分析
如果每次跳跃的点的编号形成一个序列的话,那么这个序列满足“波浪形”,即 \(a_1 < a_2,\ a_2>a_3, \ a_3<a_4 \dots\) 或者 \(a_1>a_2,\ a_2<a_3, \ a_3>a_4 \dots\)
那么转化问题,求 \(1\sim n\) 的所有排列中,满足该排列构成的序列是“波浪形”的排列有几个。
前置知识
该题这种 DP 维护的是一个个块,可以设状态 \(f_{i,j}\) 表示填好了 \(1 \sim n\) 中的数,共有了 \(j\) 个块。
比如:\(f_{7,3}\) 表示前 7 个数中共有 3 个块,这 3 个块里面的信息可以是 \([1][2][3,4,5,6,7]\),也可以是 \([4,1,5][2,3][6,7]\),怎样都行。
每新填一个数,这个数可以开一个新块儿,也可以根据这个数合并之前的块儿,我们具体分析。
加新块
如果加数字 \(i\) 之前已经有了 \(j - 1\) 个块,这些块儿会形成 \(j\) 个空,那么这个数字 \(i\) 单独形成的块儿就可以查到这 \(j\) 个空中的一个,所以 \(f_{i,j} = f_{i-1,j-1} \times j\)
合并块
如果加数字 \(i\) 之前已经有了 \(j + 1\) 个块,这些块儿会形成 \(j+2\) 个空,但是,如果你把数字 \(i\) 放到最前面或者最后面,那么因为最前(后)面没有块儿,就无法合并,所以实际上合法的空只有 \(j\) 个。
所以方程是 \(f_{i,j} = f_{i-1,j+1} \times j\)
这道题目
我们依旧是填数,从 \(1\) 填到 \(n\),那么对于当前数 \(i\),之前填的数都比这个数小,之后填的数都比这个数大,那么加新块、合并块这两个操作都是合法的,无论你放在哪里(此时还不考虑 \(s,t\) 的影响)
考虑到 \(s\) 和 \(t\),由于最终的块一定合并成了一个,且以 \(s\) 开头、以 \(t\) 结尾,那么他会影响到加新块的操作。
为什么?
如果 \(s\) 已经填好了,那么 \(s\) 所在的位置必然是第一个块的首部,此时你若加新块,必然不能加到 \(s\) 所在块的前面去,因为这样做会使最后合并的块无法满足 \(s\) 开头。
如果 \(t\) 已经填好了,那么 \(t\) 所在的位置必然是最后一个块的尾部,此时你若加新块,必然不能加到 \(t\) 所在块的后面去,因为这样做会使最后合并的块无法满足 \(t\) 结尾。
那么加新块的状态转移方程为:\(f_{i,j} = f_{i-1,j-1}(j - [i>s] - [i>t])\)
\([\cdots]\) 表示如果中括号内条件满足返回 1,否则返回 0
若 \(i>s\) 满足,则其表示填 \(i\) 的时候,\(s\) 已经填好了,那么第一个块的前面就不能再放了,所以要减一,\(i>t\) 同理。
对于合并块的操作无影响。
那么对于 \(i=s,i=t\) 的情况,该怎么办呢?我们拿 \(i=s\) 作说明,\(i=t\) 同理。
\(i=s\),如果要加新块,必然要加到最前面,所以只有一个位置合法,转移方程 \(f_{i,j} = f_{i-1,j-1} \times 1\);如果要合并块,你也只能把 \(s\) 放到第一个块的最前面,这样做块数是不变的,且只有一个合法位置,转移方程 \(f_{i,j} = f_{i-1,j}\)
Code
#include <iostream>
#include <cstring>
using namespace std;
typedef long long LL;
const LL N = 2e3 + 10;
const LL mod = 1e9 + 7;
LL n, s, t;
LL f[N][N];
int main()
{
cin >> n >> s >> t;
f[1][1] = 1;
for (LL i = 2; i <= n; i ++ )
{
for (LL j = 1; j <= i; j ++ )
{
if (i == s || i == t)
{
f[i][j] = (f[i][j] + f[i - 1][j - 1] + f[i - 1][j]) % mod;
continue;
}
f[i][j] = (f[i][j] + f[i - 1][j - 1] * (j - (i > s) - (i > t))) % mod;
f[i][j] = (f[i][j] + f[i - 1][j + 1] * j) % mod;
}
}
cout << f[n][1] << endl;
return 0;
}
尾声
自己做不会,看题解也挺蒙的,搞了一下午才明白。
完结撒花!