CF592D:Super M(树的直径 & 虚树)

题意:给一棵树,标记其中M个点,找一条路径通过这M个点且路程要最小,若有多个答案取起点最小的那条路径(输出:起点和路程)。
首先假如起点为 s s ,终点为t,总路程就是 2mdist(s,t) 2 m − d i s t ( s , t ) m m 为边的数量,那么dist(s,t)的最大值就是树的直径。
法①:显然路程符合一棵虚树,那么我们把虚树建出来,跑一下树的直径,维护一下最小的那个起点即可,这里找直径我用一次 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 s 出发找到最深的点t dist(s,t) d i s t ( s , t ) 就是树的直径(本题 ost o 、 s 、 t 均为标记点)。现在要找编号最小的直径,只需要 s s t都找最小的那个即可。原因:假设 s s 为直径中最小的编号,设点集合v={v1,v2,v3...vk},其中 <vi,s>,1ik < 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 点,结果显然是s;若第一次 DFS D F S 找不到 s s 点,必然找到点集v中的点。证明略~

# 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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值