题目链接:https://ac.nowcoder.com/acm/contest/5667/C

在这里插入图片描述
在这里插入图片描述

题意

给你一棵无根树,每次可以在树中选择2个节点连成一条链,要求选择最少的链覆盖所有的边(注意是覆盖所有的边而不是点)。输出最少需要几条链以及每条链的两个端点。

思路

首先基本都能够想到,链的端点尽量取叶子节点是最优的。因为叶子是每个子树中最深的节点,每次将叶子之间互相连接,覆盖的边显然会更多。因此,只要找出叶子,再按一定的策略(这个策略十分重要,见下文)两两相连,就能得到最优解。

如果只是要求叶子,而不管顺序,那么就不用dfs了,直接找度数为1的点就行了。但是为什么要dfs序对叶子进行排序呢?我觉得是要考虑到树的结构。如果我们按深度优先遍历树的顺序搜索到每个叶子,那么直观地看,叶子的排序就是从树的“左下角”的点到“右下角”的点,这样方便后续选择一定的策略来连接叶子。

那么接下来的问题是什么策略连接dfs序的叶子能得到最优解呢?
比赛时,我第一个想到的 ~~wrong answer~~ 策略是将叶子首尾相连,比如第一个叶子和最后一个叶子相连。但其实这样肯定有反例:
在这里插入图片描述
如上图,叶子节点的dfs序为2, 5, 6, 4。若按照叶子首尾相连的策略,则2,4相连,5,6相连,显然,1到3这条边没有被覆盖,所以1,3还要连一条链(而3不是叶子)。这种策略得到链的个数为3,并不是最优解。
实际上,对于这个图来说,链的最少个数为2,策略是2,6相连,5,4相连,这样所有链的端点都是叶子。

看看官方题解的做法:
在这里插入图片描述
我们继续分析一下,为什么正确的策略是取出的两个叶子的dfs序号差被固定成s/2。
(首尾相连策略实际上就是dfs序号和固定为n+1,肯定与答案不同啊!)

对于任意一个有子树的节点x,必须要保证它子树的点至少有一个能链接到x的父节点上。
这个结论是显然的,否则无法覆盖所有边。那么选择策略就是将按dfs顺序搜索到的叶子,假设有s个,分成两部分进行配对,若取第1个叶子,配对的就是第1+s/2个叶子。为什么跨s/2个进行相互配对呢,因为这样就固定了两个叶子之间的宽度(两个叶子dfs序号的差值),宽度固定为s/2就能保证所有的边都能被覆盖;而如果是首尾相连的策略,那么越往后宽度越小,最后两个叶子可能只在内部连接而没有向上走出去,即它们上层最近的根的上面那条边可能就未被覆盖,只要固定成s/2的dfs序号差,那么就可以保证所有边都被覆盖,因为刚才说的那条未被覆盖的边至少会被左边那个叶子向上走出去而覆盖。

还有几个细节需要注意:
(1)特判n<3。
(2)选择开始搜索的根节点,必须是度大于1的非叶子节点。
(3)如果叶子节点是奇数,那么最后一个叶子要和根相连。

最后吐槽一下自己,比赛的时候我还想着是不是和每个叶子的深度有关,然后打算玄学 ~~瞎猜~~ 一波,改成先按叶子的深度排序,深度相同的再按dfs序排序,再首尾相连,还是wrong answer... 实际上dfs序就体现出了叶子的深度吧,因为毕竟是深度优先搜索,每个子树肯定都是先搜到左下角的叶子。

AC代码

#include <bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int n,x,y,root,d[N];
vector<int>g[N],ans;
void dfs(int u,int fa)
{
    if(d[u]==1) // 叶子节点,度为1
    {
        ans.push_back(u);
        return;
    }
    for(int v:g[u])
    {
        if(v==fa)continue;
        dfs(v,u);
    }
}
int main()
{
    ios::sync_with_stdio(false);
    cin>>n;
    for(int i=1;i<n;i++)
    {
        cin>>x>>y;
        d[x]++;
        d[y]++;
        g[x].push_back(y);
        g[y].push_back(x);
    }
    // 一定要注意特判n<3!
    if(n==1)
    {
        printf("0\n");
        return 0;
    }
    if(n==2)
    {
        printf("1\n1 2\n");
        return 0;
    }
    for(int i=1;i<=n;i++)
        if(d[i]>1){root=i;break;}
    dfs(root,0);
    int s=ans.size(); // s表示叶子节点个数
    int half=(int)ceil(1.0*s/2.0); // half表示s/2向上取整
    printf("%d\n",half);
    if(s&1)ans.push_back(root); // 奇数就把根节点放最后,与最后一个叶子配对
    for(int i=0;i<half;i++)
        printf("%d %d\n",ans[i],ans[i+half]);
    return 0;
}
/*
hack 根节点
5
2 1
3 2
4 3
5 3
ans:
2
1 5
4 2

hack 首尾相连
7
2 1
3 1
5 3
4 3
6 5
7 5
ans:
2
2 7
6 4

hack n=2
2
1 2
ans:
2 
1 2

hack 奇数叶子
8
1 2
1 3
1 4
3 5
3 6
6 7
6 8
ans:
3
2 8
5 4
7 1
*/

另外,我再瞎猜想一下,如果题目改成必须要求三个叶子连接时走过的边来组成一条链的话,那么每两个叶子之间的dfs序的距离差就固定成s/3了?(我随便说的,不一定对哈)