闇の連鎖(最近公共祖先+树上的差分)

题目描述

传说中的暗之连锁被人们称为 Dark。
Dark 是人类内心的黑暗的产物,古今中外的勇者们都试图打倒它。
经过研究,你发现 Dark 呈现无向图的结构,图中有 N 个节点和两类边,一类边被称为主要边,而另一类被称为附加边。
Dark 有 N – 1 条主要边,并且 Dark 的任意两个节点之间都存在一条只由主要边构成的路径。
另外,Dark 还有 M 条附加边。
你的任务是把 Dark 斩为不连通的两部分。
一开始 Dark 的附加边都处于无敌状态,你只能选择一条主要边切断。
一旦你切断了一条主要边,Dark 就会进入防御模式,主要边会变为无敌的而附加边可以被切断。
但是你的能力只能再切断 Dark 的一条附加边。
现在你想要知道,一共有多少种方案可以击败 Dark。
注意,就算你第一步切断主要边之后就已经把 Dark 斩为两截,你也需要切断一条附加边才算击败了 Dark。

输入格式

第一行包含两个整数 N 和 M。
之后 N – 1 行,每行包括两个整数 A 和 B,表示 A 和 B 之间有一条主要边。
之后 M 行以同样的格式给出附加边。

输出格式

输出一个整数表示答案。

数据范围

N≤100000,M≤200000,数据保证答案不超过231−1

输入样例
4 1
1 2
2 3
1 4
3 4
输出样例
3

题目分析

不会用倍增法求最近公共祖先的话,可以先看看这个题:祖孙询问

我们可以发现:这n-1条主要边,构成了一棵树。那么,如果加上了一条附加边(u,v)之后,就会使得u和v之间构成了一个环
如果我们斩断了u和v之间的一条主要边,那么如果要想将图斩为不连通的两部分,我们就需要将附加边(u,v)斩断。

我们可以将每一条附加边(u,v)称为:将(u,v)之间的主要边路径覆盖了一次
然后我们只需要统计出每一条主要边被覆盖了几次即可。

  1. 如果一条主要边被覆盖了0次,说明斩断这条主要边之后,就已经将图斩为了两部分。所以我们只需要任选一条附加边斩断即可,ans+=m。
  2. 如果一条主要边被覆盖了1次,说明斩断这条主要边之后,还需要斩断1条附加边才能够使图变为两部分,ans+=1。
  3. 如果一条主要边被覆盖的次数大于1,说明斩断这条主要边之后,还需要斩断多条附加边才能将图斩为两部分,但我们只能斩断一条附加边,因此斩断这条主要边无解,ans+=0。

因此问题就转化为了:如何快速的标记附加边(u,v)之间的主要边路径上的点。因为主要边组成的图是一棵树,因此u和v之间的路径即为:u到p路径+p到v的路径(p为u和v的最近公共祖先)
这个覆盖操作我们可以用树上的差分来完成:要覆盖u到v之间的路径,我们只需要让d[a]++,d[b]++,d[p]-=2即可

代码如下
#include <iostream>
#include <cstdio>
#include <cmath>
#include <string>
#include <cstring>
#include <map>
#include <queue>
#include <vector>
#include <set>
#include <algorithm>
#define LL long long
#define ULL unsigned long long
#define PII pair<int,int>
#define x first
#define y second
using namespace std;
const int N=1e5+5,INF=0x3f3f3f3f;
int n,m;
int h[N],e[N*2],ne[N*2],idx;
int depth[N],fa[N][17];
int d[N];
int ans;
void add(int a,int b)
{
    e[idx]=b;
    ne[idx]=h[a];
    h[a]=idx++;
}
void bfs()				//求fa[][]和depth[]数组的模板
{
    memset(depth,0x3f,sizeof depth);
    queue<int> q;
    depth[0]=0; depth[1]=1;
    q.push(1);
    while(q.size())
    {
        int u=q.front();
        q.pop();
        for(int i=h[u];~i;i=ne[i])
        {
            int v=e[i];
            if(depth[v]>depth[u]+1)
            {
                depth[v]=depth[u]+1;
                q.push(v);
                fa[v][0]=u;
                for(int k=1;k<=16;k++)
                    fa[v][k]=fa[fa[v][k-1]][k-1];
            }
        }
    }
}
int lca(int a,int b)	//求出两点最近公共祖先的模板
{
    if(depth[a]<depth[b]) swap(a,b);
    for(int k=16;k>=0;k--)
        if(depth[fa[a][k]]>=depth[b])
            a=fa[a][k];
    
    if(a==b) return a;
    for(int k=16;k>=0;k--)
        if(fa[a][k]!=fa[b][k])
        {
            a=fa[a][k];
            b=fa[b][k];
        }
    return fa[a][0];
}
int dfs(int u,int fa)
{
    int res=d[u];			//记录u节点被覆盖的次数
    for(int i=h[u];~i;i=ne[i])
    {
        int v=e[i];
        if(v!=fa)
        {
            int sum=dfs(v,u);			//dfs计算出v节点被覆盖的次数(即为u-v边被覆盖的次数)
            if(sum==0) ans+=m;			//分类讨论
            else if(sum==1) ans++;
            res+=sum;					//前缀和累加
        }
    }
    return res;				//最后的res即为u节点被覆盖的次数
}
int main()
{
    memset(h,-1,sizeof h);
    scanf("%d %d",&n,&m);
    for(int i=1;i<n;i++)			//用主要边建图
    {
        int u,v;
        scanf("%d %d",&u,&v);
        add(u,v); add(v,u);
    }
    bfs();							//预处理出求最近公共祖先需要的数组
    for(int i=0;i<m;i++)			//处理m条附加边
    {
        int a,b;
        scanf("%d %d",&a,&b);
        int p=lca(a,b);				//求出a和b的lca
        d[a]++,d[b]++,d[p]-=2;		//标记a到b的路径(差分)
    }
    dfs(1,-1);						//dfs恢复所有标记并计算答案
    printf("%d\n",ans);
    return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lwz_159

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值