2021牛客暑期多校第七场F题(xay loves trees)题解

题目大意

你有两棵树,它们的根节点都为1,且都有 n 个节点。你需要找到 {1,2,⋯,n} 的最大子集,使得:
1.在第一棵树上,集合中的节点是相连的,且对于集合中的任意两个节点 u,v,u,v 中的一个节点是另一个节点的祖先。
2.在第二棵树上,对于集合中的任意两个节点,u,v 中的任意一个节点都不是另一个节点的祖先。
请输出集合中元素的最大数量。

输入描述

第一行包含一个整数 t ( 1 ≤ t ≤ 10 ) (1 ≤ t ≤ 10) 1t10表示测试用例的数量。测试用例的描述如下:
每一组测试用例的第一行包含一个整数 n ( 1 ≤ n ≤ 3 × 1 0 5 ) (1 ≤ n ≤ 3×10^5) (1n3×105)
接下来 n-1 行每行包含两个整数 u i , v i ( 1 ≤ u i , v i ≤ n ; u i ≠ v i ) u_i,v_i(1≤u_i,v_i≤n;u_i≠v_i) ui,vi(1ui,vin;ui=vi)表示第一棵树上的一条边。
再接下来 n-1 行每行包含两个整数 u i , v i ( 1 ≤ u i , v i ≤ n ; u i ≠ v i ) u_i,v_i(1≤u_i,v_i≤n;u_i≠v_i) ui,vi(1ui,vin;ui=vi)表示第二棵树上的一条边。
保证给出的所有图都是树。
保证所有测试用例中 n 的总和不超过 3 × 1 0 5 3×10^5 3×105

输出描述

对于每一组测试用例打印单个整数,表示最大子集中元素的数量。

样例输入

3
5
2 1
2 5
5 3
2 4
2 1
1 5
1 3
3 4
5
5 3
3 1
5 4
3 2
5 3
5 1
3 4
3 2
5
5 3
5 1
5 2
5 4
5 3
3 1
5 2
3 4

样例输出

3
1
2

思路

由题意可知,我们需要先根据第二棵树,得到判断两两节点是否构成祖孙的依据。直接用暴力的手段记录每个节点的祖先,时间复杂度达到了 O ( n 2 ) O(n^2) O(n2),一定会超时,故我们需要尽量去寻找时间复杂度为 O ( n ) O(n) O(n)的方法。

这就需要我们掌握一个知识点:树的DFS序。我们从根节点开始DFS整棵树,在进栈处和出栈处设置数组,分别记录每一个节点进栈和出栈的顺序。然后我们利用桶的思想,得到每一个节点进出栈的时间(即进栈时间表是线程的前半段,出栈时间表是线程的后半段)。

我们可以得到如下两个结论:
1.如果两个节点构成祖孙,那么这两个节点同在一条树链上,且祖先节点比子孙节点先入栈,子孙节点比祖先节点先出栈。
2.如果两个节点不构成祖孙,那么这两个节点不在一条树链上,且其中一个节点相对另外一个节点既先进栈,又先出栈。

我们根据以上两个结论查表,比较两个节点的进出栈时间,即可判断两个节点是否构成祖孙。建表的时间复杂度为 O ( n ) O(n) O(n)

然后我们在第一棵树上进行搜索。由于要构造的最大子集中的节点在第一棵树上是相连的,还要满足在第二棵树上两两不构成祖孙的条件,并且最后要求的是子集可以容纳节点个数的最大值,容易想到利用树上滑动窗口维护这一子集。

由树的DFS序,每次搜索会先彻底搜索完整条树链,再向上回溯。那么滑动窗口则在整条树链上进行滑动,以满足在第一棵树上两两构成祖先的关系。用传统的双指针模拟窗口,在树上则显得不易操作。因此我们用双向队列来维护滑动窗口。在每次向滑动窗口中加入节点前,我们先将右指针指向队首,如果指向的节点与要新加的节点不构成祖孙,则右指针向左滑动,直到不满足条件为止。然后我们加入新节点,并更新答案,即滑动窗口当前的长度。

至于左指针,就是右指针向左移动到达的极限位置。在正向搜索完整条树链后,我们利用树的DFS序,将队列中的节点从队尾弹出,队首树链的公共部分保留,并接着搜索该公共部分的其它树链分支。

我们用静态邻接表vector来保存树的边集。不要忘了将给出的有向边转换成无向边。具体的实现方式见代码。

考点

树的DFS序
树上滑动窗口的维护

AC代码(含注释)

#include<bits/stdc++.h>
using namespace std;
const int maxn=3e5+5;
vector<int> e1[maxn],e2[maxn];//树1,树2
int n,u,v,ans;
int inorder[maxn],outorder[maxn];//进栈序,出栈序
int incnt,outcnt;
int innode[maxn],outnode[maxn];//进栈节点,出栈节点
deque<int> q;//双向队列维护滑动窗口
void iodfs(int cur,int fa){//记录树2的DFS序
    innode[incnt++]=cur;//记录进栈节点
    for(auto nxt:e2[cur]){//访问当前节点的邻接节点
        if(nxt!=fa)iodfs(nxt,cur);//如果邻接节点不是父节点,继续DFS
    }
    outnode[outcnt++]=cur;//记录出栈节点
}
void get_ancestor(){//建立进出栈时间表
    iodfs(1,0);
    for(int i=0;i<=n-1;i++){//利用桶的思想统计进出栈的顺序
        inorder[innode[i]]=outorder[outnode[i]]=i;
    }
}
bool is_ancestor(int u,int v){//根据进出栈时间表判断两节点是否构成祖先
    if(inorder[u]<inorder[v]&&outorder[u]>outorder[v])return true;
    if(inorder[u]>inorder[v]&&outorder[u]<outorder[v])return true;
    if(inorder[u]<inorder[v]&&outorder[u]<outorder[v])return false;
    if(inorder[u]>inorder[v]&&outorder[u]>outorder[v])return false;
}
void dfs(int cur,int fa,int left){//DFS树1
    q.push_back(cur);//将当前节点加入队尾
    ans=max<int>(ans,q.size()-left);//更新答案
    for(auto nxt:e1[cur]){//访问当前节点的邻接节点
        if(nxt!=fa){//如果邻接节点不是父节点
            int right=q.size();//初始化右指针
            while(right>left){//尝试向左移动
                if(is_ancestor(q[right-1],nxt)==false)right--;//树2中不是祖先,就左移扩大滑动窗口长度
                else break;//否则终止
            }
            dfs(nxt,cur,right);//右指针左移到极限成为左指针
        }
    }
    q.pop_back();//回溯时从队尾弹出路径上的节点
}
int main(){
    int t;
    cin>>t;
    while(t--){
        cin>>n;
        for(int i=1;i<=n;i++){//释放容器的内存空间
            e1[i].clear();
            e2[i].clear();
        }
        for(int i=1;i<=n-1;i++){
            cin>>u>>v;
            e1[u].push_back(v);//有向边转无向边
            e1[v].push_back(u);
        }
        for(int i=1;i<=n-1;i++){
            cin>>u>>v;
            e2[u].push_back(v);//有向边转无向边
            e2[v].push_back(u);
        }
        q.clear();//释放队列的内存空间
        incnt=0,outcnt=0,ans=0;//初始化
        get_ancestor();//根据树2建表
        dfs(1,0,0);//根据树1搜索
        cout<<ans<<endl;
    }
    return 0;
}

心得

这道题目考察了树上的区间信息维护,虽然还有线段树和主席树等做法,但难度过大,不易理解。这里给出的方法是容易理解的,也是相当巧妙的。本题在实际比赛时拉开的差距非常大,做出本题后排名从一千上升至一百的队伍大有人在。大概算是区域赛银牌难度。值得仔细思考。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

keguaiguai

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

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

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

打赏作者

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

抵扣说明:

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

余额充值