圣诞树

【问题描述】

noip 要到了,大家来到圣诞树前。这个圣诞树不仅仅是圣诞树,还有未卜先知的功能。
众 OIer 问圣诞树:
“不平凡的圣诞树,CCF 告诉我们 noip 中会有两道题目从 Openjudge 上选
择,你能不能告诉我是哪两道题。”圣诞树想了想直接说出答案并不妥:“中国有句古话叫‘闷声大发财’,我就什么也不说,这是最好的。但是我看到你们这么热情,一句话不说也不好,我就告诉你们点信息吧。你们看我是一个由 N 个结点组成的树,在树中任选着 3 个点,有多少种选择方案使得这三个点互相之间的距离相同?两个方案不同当且仅当一个点在第一种方案中被选择,第二种方案中没有被选择。”
“记你算出来方案数为 cnt,那么第一道题的题号就是 cnt%338 + 1,第二题的题目编号
是 (cnt+233)%338+1。”
可是 OIer 们手头并没有计算机,于是请你来告诉他们题目编号。

【输入】

第一行一个整数 N,表示树有 N 个点。
接下来 N-1 行,每行两个整数 u,v,表示树中有一条从 u 到 v 的边。

【输出】

一行,两个整数,分别为预测的第一题题号和第二题题号。

【输入输出样例】

Input
7
1 2
5 7
2 5
2 3
5 6
4 5
Output
6 239

【样例解释】
共有 5 种方案,
分别是{1,3,5},{2,4,6},{2,4,7},{2,6,7},{4,6,7}。
所以第一题的编号为 5%338 + 1 = 6;
第二题的编号为 (5+233)%338 + 1 = 239;
数据范围与约定:
对于 30%的数据:1 <= n <= 100
对于 60%的数据:1 <= n <= 1500
对于 100%的数据:1 <= n <= 5000

【题解】

和洛谷P3565 [POI2014]HOT-Hotels是同一道题,我还研究了好久写篇题解,然而到考试时就写挂了。对已做过题目的掌握程度有待加强。

树形DP,还是挺有难度的,毕竟是POI的题。

首先确认一下,由于这是一棵树,所以若有三个点两两距离相同,则一定会有一个交集点,它们到这个交集点的距离相同。

那么我们只要枚举这个中心点就行了;

我们以当前枚举的中心点为根节点,将整个无根树旋转,这棵树就变成了有根数。

然后位于同一深度的节点与根节点(中心点)的距离就自然是一样的啦,Hahahahahaha!

但是如果某一深度的两个节点的最经公共祖先不是我们枚举的根节点,就会出岔子。(画一下就看出来了)

就拿样例来说,画完后,当枚举到2为根节点,深度为2时,4、6、7是满足题意的。

所以我们最好的解决方法是在每一棵子树上各选一点凑成三点,这样就能保证他们的最经公共祖先为根节点了。

所以在枚举了根节点后,我们一次只计算其一棵子树。

OK,确认距离相等的思路就是这样的,下面进入数学课时间!

如何维护这个方案树呢?(我们已经确保了到根节点的距离相等了,不明白的童鞋参照上文在纸上画一下)

先从简单的开始吧(至少得有三棵子树是吧)

假设我们当前枚举的根节点为A,深度为i; //其实这些对于说明没卵用

那么三棵子树在这一深度上的节点数分别为a、b、c;

那么新增加的答案自然就是a*b*c;(小学的乘法原理)

如果这时候,第四个子树上的节点数为d,我们来看一下答案是怎么变化的。

有一下三种情况:d*a*b、d*a*c、d*b*c。我们合并一下:new_ans=d*(a+b+c);

再来一棵子树,上面的节点数为e,那么变化为:

有6种情况:e*a*b、e*a*c、e*b*c、e*a*d、e*b*d、e*c*d;

合并一下:new_ans=e*(a*b+a*c+b*c+d*a+d*b+d*c);

看出了什么规律了吗?

没看出,好吧;

如果用一个sigma_2维护a*b+b*c+a*c………等这一式子;每次新得到的节点数为K

那么新增加的答案数为sigma_2*K;

对于sigma_2,我们继续观察一下:

三个数到四个数:a*b+b*c+a*c–>a*b+b*c+a*c+a*d+b*d+c*d即新增加d*(a+b+c)

四个数到五个数:太长了………..自己写一下吧,新增加的为e*(a+b+c+d)

这下看出来了吧,如果记sigma_1为之前得到的节点总数(之前子树此层节点之和)

那么有以下规律:若新得到的节点数为K,那么sigma_2=K*sigma_1;然后sigma_1=sigma_1+K;

所以对于递推方案数的分析,我们就分析完了。但估计各位还是不太清楚具体实现,那我来总结一下:

设tot[i]为当前子树第i层的节点数,我们用c1[i]维护第i层(距离)的sigma_1 ,用c2[i]维护第i层(距离)的sigma_2;

那么有以下框架:

1.枚举根节点k

2.枚举每一棵子树,计算第i层的tot[i];

3.重头戏,计算部分:

- **ans=ans+tot[i]*c2[i];**
- **c2[i]=c2[i]+c1[i]*tot[i]**
- **c1[i]=c1[i]+tot[i]**
- **tot[i]=0//记得清零!!!!!**

特别提醒一下,每枚举一个新的根节点,要记得把c1、c2清零。

具体实现代码:

//设子树中第k层的节点数分别为a1、a2、a3、。。。。。。、an
// 那么3个时开始统计答案,有a1*a2*a3一种
//  当4个子树时:
//  有a4*a1*a2 a4*a2*a3 a4*a1*a3 三种(新增) 
// 合计:a4*(a1*a2+a2*a3+a3*a1);
//  用c2 维护a1*a2+a2*a3.....这一式子
//  关注到c2的变化:c2[前]a1*a2*a3-->c2[前]+(a1+a2+a3)*a4
// 以此类推 ;  令tot为此子树的节点个数 、 c1为这一层之前所有节点和(a1+a2+...+at) 
// 则可以得到递推式:
//   ans+=c2[i]*tot[i];
//   c2[i]+=tot[i]*c1[t];
//   c1[t]+=tot[i];         //算完后记得把tot[i]清零 
//注意枚举根节点,最后求一个总和      
#include<iostream>
#include<cmath>
#include<cstring>
#include<cstdio>
#include<cstdlib>
#include<algorithm>
#define fp(i,a,b) for(int i=a;i<=b;i++)
#define fq(i,a,b) for(int i=a;i>=b;i--)
#define il inline
#define re register
#define ll long long 
using namespace std;
const int N=5005;
int n,head[N]={},cnt=0,bian[N]={},deep,tot[N]={},c1[N]={},c2[N]={};
ll ans=0;
struct Edge
{
    int to,next;
}e[N<<1];
/* tot表示当前子树上每一层的节点数 c1表示每层已访问过的节点中选出一个的种数
c2表示每层已访问过节点中选出两个的种数
则每次ans+=c2[d]*tot[d] 答案必将是在新生成的这层中选一个,在之前选两个
c2[d]+=c1[d]*tot[d] 更新完答案后 节点视为已访问 选两个的种数增加:新生成的选一个,之前的选一个
c1[d]+=tot[d] 选一个的种树增加:就是新生成节点数*/
void add(int u,int v)
{
    e[++cnt]=(Edge){v,head[u]};head[u]=cnt;
}
il int gi()
{
   int x=0;
   short int t=1;
   char ch=getchar();
  while((ch<'0'||ch>'9')&&ch!='-') ch=getchar();
  if(ch=='-') t=-1,ch=getchar();
  while(ch>='0'&&ch<='9') x=x*10+ch-48,ch=getchar();
  return x*t;
}
void dfs(int u,int fa,int d)
{
    deep=max(deep,d);//deep是距离根节点的深度
    tot[d]++;
    for(int i=head[u];i;i=e[i].next)
    {
        int v=e[i].to;
        if(v==fa) continue;
        dfs(v,u,d+1);
    }
}
int main()
{
    freopen("hopetree.in","r",stdin);
    freopen("hopetree.out","w",stdout);
    n=gi();
    fp(i,1,n-1)
    {
        int u=gi(),v=gi();
        add(u,v);add(v,u);
        bian[u]++;bian[v]++;
    }
    fp(i,1,n)//枚举根节点
    {
        if(bian[i]<3) continue;
        memset(c1,0,sizeof(c1));
        memset(c2,0,sizeof(c2));
        for(int j=head[i];j;j=e[j].next)//枚举每一颗子树
        {
            int v=e[j].to;
            dfs(v,i,1);
            fp(d,1,deep)
            {
                ans+=c2[d]*tot[d];
                c2[d]+=c1[d]*tot[d];
                c1[d]+=tot[d];
            }
            memset(tot,0,sizeof(tot));
        }
    }
    printf("%lld %lld\n",(ans+1)%338,(ans+233)%338+1);
    fclose(stdin);
    fclose(stdout);
    return 0;
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值