小王子(LCA的运用)

题目链接:https://ac.nowcoder.com/acm/problem/206091
描述

题目背景
“如果我沿着这条路一直往上面去,我就可以摘到那一颗星星。”
“可是物理好难啊。”
“那我还是去炸星星好了。”
“就算是要到月亮上去罚站也没关系呀。”
题目描述
十二月的风凌冽。
他站在阳台上,嘴里叼着一根从隔壁房间偷来的草莓味棒棒糖,眯着眼试图从漫天飞雪中嗅到一丝秋天的气息。黑与白构成不分明的界限,混沌地勾勒出陌生的形状。
倒像是一堆散乱的线条。
他又眯着眼看那清凌凌的夜空。
像是那么多的星星。
随手把剩下的棍子扔向,他打了个哈欠,想起厨房里的《赢在微点》。
再看,是N颗星星牵着N-1条线。
是N-1条白色的线束缚着N颗星星,将他们束缚成一个星团。
“你为什么盯着星星看这么久?”
“我喜欢一颗小星星,所以遥望星空,试图用大脑向宇宙发送一些信号。”
“那宇宙给你反馈了吗?”
“没呢。”
“希望那颗小星星可以在宇宙来临之前,先跑到我面前哦。”
“那就换一颗嘛,天上的星星多得是。”
——可那线条好少。
——那便多一点好了。
于是他依稀看见M条黑色的线与N-1条白色的线交错,不重合也不缠绕。都只是轻轻地拉着那些星星,并不捆绑,却不知怎的令他们无法脱身。
“宇宙给你反馈了吗?”
“没有。”
“那就换一颗嘛,天上的星星多得是。”
“环游整个星系,我大概找不到更亮的星星。”
——那就去摘啊。自己的星星自己摘。
——可是我的宇宙飞船可能会遇到流星,可能会用光所有的燃料。
——可是宇宙里的信号表示,那颗小星星在靠近。星河万顷,都是见面礼。
——可他有点慢啊。
——那就把他炸下来。
他看着那些不存在的线条与星星,决定用他仅有的两发导弹之中的一发去毁灭一条白线,使那些星星分成两部分。
想着他在物理书上写下:“#define 炸星星 使那些星星分成两部分”。
然后失望地发现有些时候无法成功地炸星星。
于是他决定用仅剩的一发他本决定用来保证晚年幸福的导弹去毁灭一条黑线。
不是每一个月亮都能遇到宇航员的。
但他还是想知道他有多少种使他的小星星在下一秒便出现在他怀里的方案。
可他要送什么见面礼给他的小星星呢?
他只有两发导弹、一本《赢在微点》、一本《普通高中课程标准实验教科书物理必修2》和一根草莓味棒棒糖。
草莓味棒棒糖已经被他吃完并丢弃了,而星星想必也不会喜欢物理书与物理教辅。
所以他决定就算一发导弹一条白线就能把他的小星星带到他面前,他也要再炸一条黑线给他的小星星。
也许这样会让他的爱与企盼再长大一点点吧。
至少看起来长大一点点。
顺便,把方案数也送给他的小星星。
星星被分成了两部分,那么下面的星星会下坠吧。
如果他们没有遇见像他一样的人,也许他们会遇见饥饿的小怪兽,小怪兽也许会嚼碎这些小星球。
“我就藏在漫天的星光里呀。”
他不怕外星人把他那些星星抢走的。
抢不走的.

输入

第一行包含两个整数N和M;
之后N-1行,每行包括两个整数X和Y,表示X和Y之间有一条白线;
之后M行,每行包括两个整数X和Y,表示X和Y之间有一条黑线。

输出

输出一个整数表示炸掉一条白线和黑线使得星星被分成两部分的方案数。

题意:
给定一颗N节点的树,通过N-1条白边连接,又有M条黑边连接(不与白边重复),现要摧毁黑白边各一条,问有多少种方案能将这颗树分成两部分。

思路:
一开始尝试用了边双连通来做,缩点后的方案数为(边双连通分量数-1)*m(分量之间的边必为白边,如果是黑边则黑边之间必有白边会构成一个边双连通分量),然后对于分量内的方案,如果有一条黑边则方案数为该分量内的白边数,0条黑边为白边数×黑边总数。然而这是不全面的,如果只切一条边是可以用边双联通来想的,但这里是切两条边,切了一条边后原边的边双连通就不一定还是“原班人马”。
从树的角度出发在树上的任意两点间(不与白边重复)增加一条黑边(u->v),就会构成一条(u->lca(u,v)->v)的回路这个回路里的任意一个点想要脱离就需要切断与其父节点连接的边和这条黑边。由此可知当一个点受两条黑边影响时由于只能去掉一条所以无法脱离。当某一点没有受到黑边影响时只要切断与其父节点的边就能脱离。
对于LCA的求解,采用了数链剖分(刚学想再熟悉一下 其他的方法还没学)。
简单介绍下数链剖分:
将一个父节点的子结点分成两类:重儿子,和轻儿子。(按子节点所在子树的节点数来分,节点数最多的为重儿子)对于一个结点它至多有一个重儿子(若最大值有多个,则随便选一个即可)。重儿子间的边构成重链,反之为轻链。重链上的点可以直接跳到链的顶端,轻链也可以,不过它的顶端为自己。这或许就是按子树节点数来划分轻重儿子的原因之一吧。使尽可能多的点可以“坐电梯”,从而达到优化时间效率的目的。
在求lca(u,v)时根据两点链顶的深度来选择哪个向上“跳”(链顶相同,则说明在同一条链上则不进入循环),若 u所在链顶的深度较v的要深,则u跳到所在链顶节点的父节点。
对于为什么是按链顶的深度跳,我的理解是:对于u,v所在两条不同的链,如果链顶的深度一样,那么说明两者的lca还在链顶的上面,至少是链顶的父节点,所以二者随意一个跳就行了(一起跳应该也可以毕竟一个跳完后新链顶的深度肯定比另一个要浅下次一定是另一个跳,但编程麻烦,不考虑,还是老老实实一个一个跳吧)。若u链顶的深度要深,则为了最后收敛于lca,需u跳。从lca的位置来考虑:二者的lca一定不在u所在的链上,而在链顶的上面,所以u跳,而v就不一定了因为它链顶的深度是小于u的,lca可能就在v所在链上,所以一定是链顶深的跳。
跳到最后,二者在同一条链上了,考虑轻链和重链两种情况:
轻链:因为就一个点所以lca就是两者所在的点;
重链:u,v深度(跳完后的,不是原来的u,v)浅的为lca。

#include<bits/stdc++.h>
using namespace std;
const int Ms=2e5+5;
struct Enode
{
    int v,nex;
}E[Ms*2];
struct Tnode
{
    int fa,son,deep,sz,top;
}T[Ms];
int head[Ms],a[Ms],m;
long long ans=0;
void dfs1(int u,int fa)
{
    int mx=0,v;
    T[u].deep=T[fa].deep+1;
    T[u].fa=fa;
    T[u].sz=1;
    for(int i=head[u];i;i=E[i].nex)
    {
        v=E[i].v;
        if(v==fa)continue;
        dfs1(v,u);
        T[u].sz+=T[v].sz;
        if(T[v].sz>=T[mx].sz)mx=v;
    }
    T[u].son=mx;
}
void dfs2(int u,int top)
{
    int v;
    T[u].top=top;
    if(T[u].son)dfs2(T[u].son,top);
    for(int i=head[u];i;i=E[i].nex)
    {
        v=E[i].v;
        if(v==T[u].fa||v==T[u].son)continue;
        dfs2(v,v);
    }
}
int qlca(int x,int y)
{
    int tx=T[x].top,ty=T[y].top;
    while(tx!=ty)
    {
        if(T[tx].deep>=T[ty].deep)
        {
            x=T[tx].fa;
            tx=T[x].top;
        }
        else
        {
            y=T[ty].fa;
            ty=T[y].top;
        }
    }
    return T[x].deep>T[y].deep?y:x;
}
void solve(int u)
{
    int v;
    for(int i=head[u];i;i=E[i].nex)
    {
        v=E[i].v;
        if(v==T[u].fa)continue;
        solve(v);
        a[u]+=a[v];
    }
    if(T[u].fa)//减去连接其父节点的白边
    {
        if(a[u]==1)ans++;//有黑边包住该点,只能删除该条黑边
        else if(a[u]==0)ans+=m;//没有黑边包住,即删掉连接父节点的白边后就分成两部分,删除任意一条黑边即可
    }
}
void add_edge(int u,int v)
{
    static int cnt=1;
    E[cnt]=(Enode){v,head[u]};
    head[u]=cnt++;
    E[cnt]=(Enode){u,head[v]};
    head[v]=cnt++;
}
int main()
{
    int n,u,v,ff;
    scanf("%d%d",&n,&m);
    for(int i=1;i<n;i++)
    {
        scanf("%d%d",&u,&v);
        add_edge(u,v);
    }
    dfs1(1,0);
    dfs2(1,0);
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d",&u,&v);
        ff=qlca(u,v);
        if(ff!=u)a[u]++,a[ff]--;
        if(ff!=v)a[v]++,a[ff]--;
    }
    solve(1);
    cout<<ans<<endl;
}

若有什么错误,欢迎指正^ _ ^ 。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值