单调栈(笛卡尔树 区间DP)

【转载】作者:GuxueAkioi 

学习之用,为防丢失,故此转载,万望见谅,若有侵权,联系删除。

题意:一开始有一个长度为 n 的未知排列。给出一个序列 a,其中 a[i] 表示单调栈在第 i 个数入栈后的 size 大小,a[i] = -1 表示 size 大小未知,根据序列 a 输出可能的初始排列的方案数。答案对 1e9+7 取模。

规模:T组数据,T<=20,1<=n<=100。

分析:首先考虑没有-1的情况,根据单调栈的性质我们不难发现最右边的 size==1 的位置其实就是原排列中 1 的位置。将这个位置记为 mid。而找到以后会把原序列分成左右两部分,这两部分中左边的size不变,但是右边的size就要--,因为1已经确定位置了,所以后面能动的数就少了1。然后再到两边找 size==1 的位置分段。这样递归下去,形态就等同一个小根笛卡尔树。因为左右分开以后大小就独立了,所以我们可以从所有数中任选一些数分给左边/右边,方案数即为 C(size,mid-l)。因为分段1次就是O(n),分n次,复杂度O(n^2)。

那么有-1又怎么办呢?其实是一样的,枚举所有的 -1,如果在 mid 左边就不可选,在 mid 右边就可选,然后暴力分段搜。为了保证复杂度,采用记搜,f[l][r][k] 表示区间 [l,r],前面已经分了 k 次段,即我们要在 [l,r] 中找最右边的、大小为 k+1 的继续进行分段。这样定状态就已经可以保证唯一了。然后方案数就是 f[l][r][k]=f[l][mid-1][k]*f[mid+1][r][k+1]*C(r-l,mid-l)。每个状态转移要花 O(n) 的复杂度枚举,所以一共是 O(n^4) 的复杂度,但是因为枚举区间自带大概 1/2 常数,部分状态又转移不到,所以实测跑得飞快。可以对于每个区间 [l,r] 预处理出 mid ,常数大概可以再*1/2,就可以获得和 @nkxjlym 巨佬一样的飞快速度!

总结:发现笛卡尔树模型以后就是简单暴力记搜题,其实难度不大。

#include<bits/stdc++.h>
using namespace std;
const int mo=1000000007;
int n,a[110];
long long f[110][110][110],C[110][110];
long long ddfs(int l,int r,int k)
{
	if(f[l][r][k]!=-1) return f[l][r][k];
	if(l>r) return f[l][r][k]=1;
	if(l==r) return f[l][r][k]=(a[l]==k||a[l]==-1);
	f[l][r][k]=0;
	int mid=0,len=r-l;
	for(int i=r;i>=l;i--)
		if(a[i]==k){mid=i;break;}
	if(!mid)
	{
		for(int i=r;i>=l;i--)
			if(a[i]==-1)
				(f[l][r][k]+=ddfs(l,i-1,k)*ddfs(i+1,r,k+1)%mo*C[len][r-i])%=mo;
	}
	else
	{
		for(int i=r;i>mid;i--)
			if(a[i]==-1)
				(f[l][r][k]+=ddfs(l,i-1,k)*ddfs(i+1,r,k+1)%mo*C[len][r-i])%=mo;
		(f[l][r][k]+=ddfs(l,mid-1,k)*ddfs(mid+1,r,k+1)%mo*C[len][r-mid])%=mo;
	}
	return f[l][r][k];
}
int main()
{
	C[0][0]=1;
	for(int i=1;i<=100;i++)
	{
		C[i][0]=1;
		for(int j=1;j<=i;j++)
			C[i][j]=(C[i-1][j-1]+C[i-1][j])%mo;
	}
	int T;
	scanf("%d",&T);
	while(T--)
	{
		memset(f,-1,sizeof(f));
		scanf("%d",&n);
		for(int i=1;i<=n;i++) scanf("%d",&a[i]);
		printf("%lld\n",ddfs(1,n,1));
	}
	return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值