题意
一棵树,要遍历所有的边,不能往回走。走到尽头的时候可以传送到另一个点,首先要保证使用传送次数最少。
思路
所走的路径是最小链覆盖,由两个两个叶子的链组成,如果有奇数个子叶节点,那么会多一条从某一个叶子到祖先的一条特殊路径。
那么对于每一个叶节点,一定有且只有一条指向祖先的路径。对于一个非叶,会收到来自子节点上传的多条路径,那么这时两两合并一定最优,但是又必须要保证它向根的路径被覆盖。所以如果上传了奇数个路径那么正好把多余的一个继续上传,如果是偶数个路径则要一次上传两个。如果共有偶数个叶节点,那么在根上就会两两合并。如果是奇数个叶节点,就要考虑用多出的那条路径来省去一些偶数上传。
可以发现,对于上述过程在贪心回溯后,其实那个多出的路径已经连接到了根,那么我们就搜索一下可以用它来优化那些偶数路径。基本的想法是搜索从根出发的最长偶数(只可能是2)路径然后答案减去这个长度。但是最优解可能那个多出的路径根本每到根或者越过某些1流量的边继续优化下面的长偶路径了。所以在搜索过程中遇到偶数路径长度+1,遇到奇数路径长度-1,全局记录最大长度,答案减去这个最大长度即可。
下面论证这一做法对于多出路径为到根的正确性。如果不让那条奇数路径到根,那么我们需要从另一方向引两条路径来填补这条奇数路径来的方向,这正好是符合-1(即答案+1),而到了拐点,接下来搜索的最大“长度”正好表示了用这条路径改接所能达到的效果。请看图。
注意!搜索起点的根最好是非叶节点。
AC代码 C++
#include <stdio.h>
#include <string.h>
#include <vector>
using namespace std;
#define MAXN 100005
vector<int> G[MAXN];
bool eve[MAXN];
int ans;
int dmax;
int dfs(int u, int fa)
{
int i, v, cnt=0;
bool isleaf = true;
for(i=G[u].size(); i--;)
{
v = G[u][i];
if(v != fa)
{
cnt += dfs(v, u);
isleaf = false;
if(eve[v])
ans++;
}
}
if(isleaf || cnt & 1)
return 1;
eve[u] = true;
return 2;
}
void fd(int u, int fa, int d)
{
if(d > dmax)
dmax = d;
int i, v;
for(i=G[u].size(); i--;)
{
v = G[u][i];
if(v != fa)
fd(v, u, d + (eve[v] ? 1 : -1));
}
}
int main()
{
int t, n, i, x, y, cnt;
scanf("%d", &t);
while(t-- && scanf("%d", &n) > 0)
{
memset(eve, false, sizeof eve);
for(i=1; i<=n; i++)
G[i].clear();
for(i=1; i<n; i++)
{
scanf("%d%d", &x, &y);
G[x].push_back(y);
G[y].push_back(x);
}
ans = n - 1;
for(x=1; G[x].size()==1; x++);
cnt = dfs(x, 0);
if((cnt & 1) == 0)
{
printf("%d\n", ans);
continue;
}
dmax = 0;
fd(x, 0, 0);
printf("%d\n", ans - dmax);
}
return 0;
}