BZOJ3244:[Noi2013]树的计数 (树的遍历)

6 篇文章 0 订阅
3 篇文章 0 订阅

题目传送门:http://www.lydsy.com/JudgeOnline/problem.php?id=3244


题目分析:一道超级难想的题,我YY了好几天都不会做,最后只好%一波网上大神的题解QAQ。

由于编号没有什么用,我们将BFS序强行设为1~n,并对应地改DFS序。现在我们考虑对着BFS序分层,每一层对应BFS序上的一个区间。然后要分析出一下三个结论:
①:1号点单独分一层。
②:如果i号点在DFS序中的位置比i+1号点要后,那么i号点与i+1号点之间一定隔了一层。
③:考虑DFS序上相邻的两个点(a,b),很明显b的深度不会超过a的深度+1。所以如果 a<b ,应该限制a与b之间的分层数不超过1。

那么如何分层才能满足上述条件呢?我们先将①,②条件处理完,构造一个前缀和数组。然后逐一处理③条件,如果a与b之间已经有某个地方分了层,其它地方就一定不能分层。这可以通过 O(1) 地打标记,再离线扫一遍处理好。

那么最终的情况就是:某些相邻点对(i,i+1)之间一定要分层,某些一定不能,还有一些不确定。对于不确定的点对,它们之间分不分层,对其它点没有影响,所以它们对答案的贡献是0.5。

那么其实还有一个疑问:③条件其实是限制了某个区间的分层数小于等于1,可如果最后这个区间的所有相邻点对是否分层都是不确定的,那么每个点对对答案的贡献就不可以是0.5。但事实上这种情况是不会发生的。因为对于DFS序上相邻的点对(a,b),且 a<b ,如果 a+1<b ,那么它们之间肯定有个地方已经强制分了层,这就导致这个区间的其它地方一定不能分层。如果 a+1=b ,那么该点对产生0.5的贡献是不会有问题的。

注意在B站上提交,要输出3行答案,分别是ans-0.001,ans,ans+0.001。


下面扯一下我一开始拿到这题的一个 O(n2) 的想法:

首先还是重编号,然后考虑分治。令Solve(L,R)返回一个double数组a,其中a[i]表示DFS序上L~R的一段组成若干棵树,其中最高的深度为i的概率。则很明显令ans=Solve(2,n), 1+ni=1ans[i]i 就是答案。

再考虑一下怎么求Solve(L,R)。令x=DFS序的第L位,先取出L~R中从x开始的极长连续上升子串,设其长度为len。那么这一段组成高度为i的树,概率为 Cilen2len ,这个可以预处理一个杨辉三角。然后再取出从x开始的极长连续上升子序列,那么x+len-1以后的部分都和x+len-1高度相同,且它们之间划分出了一些子区间。递归求出这些子区间高度为某个数的概率,然后用容斥算一下这些子区间最高高度为i时候的概率,存进b数组。最后将b数组和杨辉三角的数组卷一下就可以了。由于预处理和分治的时间是 O(n2) ,卷积的总时间不会超过点对的数量,也是 O(n2) 的,所以能过85%的数据。

然而事实上这样是有问题的,因为划分的子区间之间会相互影响。举个例子:
DFS[]={1,2,5,3,6,7,4,8,9,10}
划分成子区间{6,7}和{8,9,10}的时候如果前者的深度为2,后者的深度为1,就会和BFS序冲突。
尽管如此,这个方法还是拿了30分。或许是数据太弱了吧……


CODE:

#include<iostream>
#include<string>
#include<cstring>
#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<stdio.h>
#include<algorithm>
using namespace std;

const int maxn=200100;

int bfsx[maxn];
int dfsx[maxn];

int val[maxn];
int id[maxn];

int sum[maxn];
int cnt[maxn];

int n;

int main()
{
    freopen("3244.in","r",stdin);
    freopen("std.out","w",stdout);

    scanf("%d",&n);
    for (int i=1; i<=n; i++) scanf("%d",&dfsx[i]);
    for (int i=1; i<=n; i++) scanf("%d",&bfsx[i]);

    for (int i=1; i<=n; i++) val[ bfsx[i] ]=i;
    for (int i=1; i<=n; i++) dfsx[i]=val[ dfsx[i] ],id[ dfsx[i] ]=i;

    for (int i=1; i<n; i++) if (id[i]>id[i+1]) sum[i]=1;
    sum[1]=1;
    for (int i=2; i<n; i++) sum[i]+=sum[i-1];
    for (int i=1; i<n; i++)
    {
        int a=dfsx[i],b=dfsx[i+1];
        if (a<b)
        {
            b--;
            if (sum[b]-sum[a-1]>=1) cnt[a]++,cnt[b+1]--;
        }
    }

    for (int i=n-1; i>=2; i--) sum[i]-=sum[i-1];
    int now=0;
    for (int i=1; i<n; i++)
    {
        now+=cnt[i];
        if ( !now && !sum[i] ) sum[i]=2;
    }
    int temp=2;
    for (int i=1; i<=n; i++)
        if (sum[i]==1) temp+=2;
        else if (sum[i]==2) temp++;
    double ans=(double)temp/2.0;
    printf("%.3lf\n%.3lf\n%.3lf\n",ans-0.001,ans,ans+0.001);

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值