题面
很玄幻的一题dp.
现在有一块一块的子串,他们的相对顺序是已知的但最终一定不相邻 (也就是说中间至少有一个数)
考虑从小到大插入数字,每一个数字有3种基本转移
1. 在某个空隙新建一块,
2. 并入某块(放最左或最右)。
3. 合并相邻两块
贡献系数好算,比如说并入一块那么系数就是0,假如新开系数就是-2 (因为可以知道旁边是比他大的还是比他小的)
但是有一个问题,边界贡献比较特殊,所以要开两个特殊块:边界 L,R
顾名思义,规定边界的左右不能再放。
那么要加几条转移
1. 在某个空隙新建一块普通块
3. 新建L或R
4. 并入某普通块(放最左或最右)
5. 并入目前的最左或最右,并将其升级为L或R
(因为最终的最左或最右不一定小,要支持在他旁边插数)
6. 并入L或R
7. 合并相邻两块
这样我们状态就变成了
f[做到第几个数][总贡献][块数][是否有L][是否有R]
表示这样的方案数
是
N∗(N2)∗N∗4
的复杂度,转移虽然是O(1)的,但实际上有7条。所以比较慢了
最终的答案就是sum f[n][k][1][1][1],最后再除上一个排列就得到概率了. (强行套上概率的,明明可以直接加mod,还是比较慢的缘故?)
这样为什么不会重呢? 考虑到一块中的元素是从小到大被加入的,每一块就相当于一颗笛卡尔树。转移的意义分别是
1. 在某个空隙新建一块, (以当前为根新建一棵)
2. 并入某块(放最左或最右)。 (以当前为根,将原有笛卡尔树视为左或右子树)
3. 合并相邻两块 (以当前为根,左右分别为两颗子树)
因为一个序列对应着的笛卡尔树只有一棵,在从小到大加入前提下,显然一棵笛卡尔树有且仅有一种构造方法。 因此是不重不漏的。
Demo
//#pragma GCC optimize(2)
#include <cstdio>
#include <iostream>
#include <cstring>
using namespace std;
// typedef __float128 fff;
typedef double fff;
const int N=101;
fff f[2][N][N*N][2][2];
int n,m,k,e,o;
int main() {
freopen("river.in","r",stdin);
freopen("river.out","w",stdout);
cin>>n>>m>>k;
e=n*n/2;
f[o][0][e][0][0]=1;
for (int i=0; i<n; i++) {
memset(f[1-o],0,sizeof f[1-o]);
for (int j=0; j<=i; j++) {
if (n-i+1<j-1) break;
for (int k=0; k<=e*2; k++) {
for (int u=0; u<=1; u++) for (int v=0; v<=1; v++) {
fff p = f[o][j][k][u][v];
if (p<1) continue;
//create
if (k-i>0) {
if (!u) f[1-o][j+1][k-(i+1)][1][v]+=p;
if (!v) f[1-o][j+1][k-(i+1)][u][1]+=p;
if (k-2*i>0)
f[1-o][j+1][k-2*(i+1)][u][v]+=p*(j+1-u-v);
}
//add
if (j) {
f[1-o][j][k][u][v]+=p*(j*2-u-v);
//start-add
if (!u) f[1-o][j][k+(i+1)][1][v]+=p;
//end-add
if (!v) f[1-o][j][k+(i+1)][u][1]+=p;
//merge
if (j>1) f[1-o][j-1][k+2*(i+1)][u][v]+=p*(j-1);
}
}
}
}
o=1-o;
}
fff ans = 0;
for (int i=m+e; i<=e*2; i++)
ans+=f[o][1][i][1][1];
for (int i=1; i<=n; i++) ans/=i;
// cout<<ans<<endl;
printf("0.");
for (int i=1; i<k; i++) {
ans*=10;
printf("%d",((int)ans)%10);
}
if ((int)(ans*100)%10>=5) printf("%d",(int)(ans*10)%10+1); else printf("%d",(int)(ans*10)%10);
}