题意:给一棵树,标记其中M个点,找一条路径通过这M个点且路程要最小,若有多个答案取起点最小的那条路径(输出:起点和路程)。
首先假如起点为
s
s
,终点为,总路程就是
2m−dist(s,t)
2
m
−
d
i
s
t
(
s
,
t
)
,
m
m
为边的数量,那么的最大值就是树的直径。
法①:显然路程符合一棵虚树,那么我们把虚树建出来,跑一下树的直径,维护一下最小的那个起点即可,这里找直径我用一次
DFS
D
F
S
维护两个最大的儿子去做。
# include <bits/stdc++.h>
# define mp make_pair
# define pb push_back
# define A first
# define B second
# define pii pair<int,int>
using namespace std;
const int maxn = 2e5;
vector<int>G[maxn];
vector<pii>g[maxn];
int h[maxn], id[maxn], cnt, fa[maxn][22];
int a[maxn], st[maxn*2], root, n, m, tot_s=0;
void dfs(int cur, int pre)
{
h[cur] = h[pre] + 1;
fa[cur][0] = pre;
id[cur] = ++cnt;
for(int i=1; (1<<i)<=h[cur]; ++i)
fa[cur][i] = fa[fa[cur][i-1]][i-1];
for(int j : G[cur])
if(j != pre)
dfs(j, cur);
}
int lca(int v, int u)
{
if(h[v] > h[u]) swap(v, u);
for(int i=0; i<20; ++i)
if(h[u]-h[v]>>i & 1)
u = fa[u][i];
for(int i=20; i>=0; --i)
if(fa[v][i] != fa[u][i])
v = fa[v][i], u=fa[u][i];
return v == u?v:fa[v][0];
}
int dis(int u, int v){return h[u]+h[v]-2*h[lca(u,v)];}
void link(int u, int v)
{
if(u == v) return;
int d = dis(u,v);
tot_s += d;
g[u].pb(mp(v,d));
}
bool cmp(int x, int y){return id[x] < id[y];}
int ans=0, ans_id=1e9;
pii dfs2(int cur, int pre)
{
pii mxx=mp(0,cur), mx=mp(0,cur);
for(auto it:g[cur])
{
int to = it.A;
if(to != pre)
{
pii son = dfs2(to, cur);
son.A += it.B;
if(son.A > mxx.A || (son.A == mxx.A && son.B < mxx.B))
{
mx = mxx;
mxx = son;
}
else if(son.A > mx.A || (son.A == mx.A &&son.B < mx.B))
mx = son;
}
}
if(mxx.A + mx.A > ans)
{
ans = mxx.A + mx.A;
ans_id = min(mxx.B,mx.B);
}
else if(mxx.A + mx.A == ans)
ans_id = min(ans_id, min(mxx.B, mx.B));
return mxx;
}
void work()
{
int top=1;
sort(a+2, a+1+m, cmp);
st[top] = root;
for(int i=2; i<=m; ++i)
{
int l = lca(st[top], a[i]);
while(1)
{
if(h[l] >= h[st[top-1]])
{
link(l, st[top--]);
break;
}
link(st[top-1], st[top]); --top;
}
if(st[top] != l) st[++top] =l ;
if(st[top] != a[i]) st[++top] = a[i];
}
while(--top) link(st[top], st[top+1]);
dfs2(root, 0);
printf("%d\n%d\n",ans_id,tot_s*2-ans);
}
int main()
{
int u, v;
scanf("%d%d",&n,&m);
for(int i=1; i<n; ++i)
{
scanf("%d%d",&u,&v);
G[u].pb(v);
G[v].pb(u);
}
h[0] = -1;
for(int i=1; i<=m; ++i) scanf("%d",&a[i]);
if(m == 1)
return 0*printf("%d\n0\n",a[1]);
root = a[1];
dfs(root, 0);
work();
return 0;
}
法②:直接 DFS D F S ,找直径用传统的方法,从任意点 o o 出发找到最深的点,再从 s s 出发找到最深的点, dist(s,t) d i s t ( s , t ) 就是树的直径(本题 o、s、t o 、 s 、 t 均为标记点)。现在要找编号最小的直径,只需要 s s 和都找最小的那个即可。原因:假设 s s 为直径中最小的编号,设点集合,其中 <vi,s>,1≤i≤k < v i , s > , 1 ≤ i ≤ k <script type="math/tex" id="MathJax-Element-18"> ,1\le i\le k</script>均为树的直径;若第一次 DFS D F S 恰好找到 s s 点,结果显然是;若第一次 DFS D F S 找不到 s s 点,必然找到点集中的点。证明略~
# include <bits/stdc++.h>
# define mp make_pair
# define pb push_back
using namespace std;
const int maxn = 2e5;
vector<int>g[maxn];
bool mark[maxn];
pair<int,int>mx;
int tot;
int dfs(int cur, int pre, int dep)
{
if(mark[cur]) mx = max(mx, mp(dep, -cur));
int ok = mark[cur];
for(int to:g[cur])
{
if(to != pre)
{
int tmp = dfs(to, cur, dep+1);
ok |= tmp;
tot += tmp;
}
}
return ok;
}
int main()
{
int n, m, u, v;
scanf("%d%d",&n,&m);
for(int i=1; i<n; ++i)
{
scanf("%d%d",&u,&v);
g[u].pb(v);
g[v].pb(u);
}
for(int i=0; i<m; ++i)
{
scanf("%d",&u);
mark[u] = true;
}
if(m == 1)
return 0*printf("%d\n0\n",u);
dfs(u, 0, 0);
int s = -mx.second, all = tot;
mx = mp(0, 0);
dfs(s, 0, 0);
printf("%d\n%d\n",min(s, -mx.second), 2*all-mx.first);
return 0;
}