P3304 [SDOI2013] 树的直径

58 篇文章 0 订阅
题意

传送门 P3304 [SDOI2013]直径

题解

树中最远两点间的距离为树的直径。树上各直径的中点(不一定恰好是某个节点,可能在某条边的内部)是唯一的。

D F S DFS DFS 求解某条树的直径,找到直径的中点,即可将各直径划分为两段进行求解。设 c n t [ x ] cnt[x] cnt[x] 为节点 x x x 的儿子中可以到达直径在这一侧的长度的数量。若直径中点为节点 x x x,当 c n t [ x ] ≥ 3 cnt[x]\geq 3 cnt[x]3,树的直径无必须边;若直径中点在边 ( x , y ) (x,y) (x,y) 内部,则边 ( x , y ) (x,y) (x,y) 为树的直径的必须边。

上述两种情况分别用点或边将树划分为数个连通分量,在各连通分量中 D F S DFS DFS 求解必须边即可。具体而言,从划分位置的必须边向 c n t [ x ] > 0 cnt[x]>0 cnt[x]>0 的节点(子树中存在直径的部分路径)进行搜索,当 c n t [ x ] ≥ 2 cnt[x]\geq 2 cnt[x]2 时(子树上不存在必须边)终止搜索,搜索过的边即树的直径的必须边。总时间复杂度 O ( N ) O(N) O(N)

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 200005;
int N, res, pre[maxn], cnt[maxn];
int tot, head[maxn], to[maxn << 1], nxt[maxn << 1], cost[maxn << 1];
ll ds[maxn], mx[maxn];

inline void add(int x, int y, int z)
{
    to[++tot] = y, cost[tot] = z, nxt[tot] = head[x], head[x] = tot;
}

void dfs(int x, int f, ll w, int &t)
{
    ds[x] = w;
    if (ds[x] >= ds[t])
        t = x;
    for (int i = head[x]; i; i = nxt[i])
    {
        int y = to[i], z = cost[i];
        if (y != f)
            dfs(y, x, w + z, t), pre[y] = x;
    }
}

void dfs2(int x, int f, ll w, ll t)
{
    mx[x] = w, cnt[x] = w == t;
    for (int i = head[x]; i; i = nxt[i])
    {
        int y = to[i], z = cost[i];
        if (y != f)
            dfs2(y, x, w + z, t), mx[x] = max(mx[x], mx[y]), cnt[x] += mx[y] == t;
    }
}

void dfs3(int x, int f)
{
    ++res;
    if (cnt[x] > 1)
        return;
    for (int i = head[x]; i; i = nxt[i])
    {
        int y = to[i];
        if (y != f && cnt[y])
            dfs3(y, x);
    }
}

int main()
{
    scanf("%d", &N);
    for (int i = 1, x, y, z; i < N; ++i)
        scanf("%d%d%d", &x, &y, &z), add(x, y, z), add(y, x, z);
    int s = 1, t = 0;
    dfs(s, 0, 0, t);
    s = t, t = 0;
    dfs(s, 0, 0, t);
    ll dm = ds[t];
    int p;
    for (p = t; ds[pre[p]] * 2 > dm; p = pre[p])
        ;
    if (ds[pre[p]] * 2 == dm)
    {
        memset(cnt, 0, sizeof(cnt));
        dfs2(pre[p], 0, 0, dm / 2);
        if (cnt[pre[p]] < 3)
            for (int i = head[pre[p]]; i; i = nxt[i])
            {
                int y = to[i];
                if (cnt[y])
                    dfs3(y, pre[p]);
            }
    }
    else
    {
        --res;
        memset(cnt, 0, sizeof(cnt));
        dfs2(pre[p], p, 0, ds[pre[p]]);
        dfs3(pre[p], p);
        memset(cnt, 0, sizeof(cnt));
        dfs2(p, pre[p], 0, dm - ds[p]);
        dfs3(p, pre[p]);
    }
    printf("%lld\n%d\n", dm, res);
    return 0;
}

设直径的两个端点为 u , v u,v u,v,根据树的直径的最长性,任何从 u , p u,p u,p 之间分叉离开直径的子树,其最远点与 p p p 的距离都不会比 u u u p p p 的距离更远。

根据直径的最长性,可以更高效地求解树的直径的必须边。设 s ⋯ t s\cdots t st 为树的某条直径,设 x x x 为直径上的一点, L [ x ] , R [ x ] L[x],R[x] L[x],R[x] 分别代表 s ⋯ x s\cdots x sx x ⋯ t x\cdots t xt 的长度。根据必须边的性质,其一定处于任一直径上,那么处理 s ⋯ t s\cdots t st 即可。

当直径上某个节点可以引出 1 1 1 条长度等于与直径端点距离的分叉,那么这个节点至端点上的边都不可能是必须边。 D F S DFS DFS 求解直径上各节点到子树端点的最远近距离,设其为 m x [ x ] mx[x] mx[x]。从 s s s t t t 扫描,找到第一个满足 R [ x ] = m x [ x ] R[x]=mx[x] R[x]=mx[x] 的点 r r r,再从 r r r s s s 扫描找到第一个满足 L [ x ] = m x [ x ] L[x]=mx[x] L[x]=mx[x] 的位置 l l l,此时区间 [ l , r ] [l,r] [l,r] 左右两侧的边都不可能是必须边,答案为 [ l , r ] [l,r] [l,r] 间的边数。总时间复杂度 O ( N ) O(N) O(N)

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 200005;
int N, res, pre[maxn], num, rec[maxn];
int tot, head[maxn], to[maxn << 1], nxt[maxn << 1], cost[maxn << 1];
ll ds[maxn], mx[maxn];
bool in[maxn];

inline void add(int x, int y, int z)
{
    to[++tot] = y, cost[tot] = z, nxt[tot] = head[x], head[x] = tot;
}

void dfs(int x, int f, ll w, int &t)
{
    ds[x] = w, pre[x] = f;
    if (ds[x] >= ds[t])
        t = x;
    for (int i = head[x]; i; i = nxt[i])
    {
        int y = to[i], z = cost[i];
        if (y != f)
            dfs(y, x, w + z, t);
    }
}

void dfs2(int x, int f, ll w)
{
    mx[x] = w;
    for (int i = head[x]; i; i = nxt[i])
    {
        int y = to[i], z = cost[i];
        if (y != f && !in[y])
            dfs2(y, x, w + z), mx[x] = max(mx[x], mx[y]);
    }
}

int main()
{
    scanf("%d", &N);
    for (int i = 1, x, y, z; i < N; ++i)
        scanf("%d%d%d", &x, &y, &z), add(x, y, z), add(y, x, z);
    int s = 1, t = 0;
    dfs(s, 0, 0, t);
    s = t, t = 0;
    dfs(s, 0, 0, t);
    ll dm = ds[t];
    for (int i = t; i; i = pre[i])
        rec[++num] = i, in[i] = 1;
    for (int i = 1; i <= N; ++i)
        if (in[i])
            dfs2(i, 0, 0);
    int l, r;
    for (r = 1; r <= num && ds[rec[r]] > mx[rec[r]]; ++r)
        ;
    for (l = r; l && dm - ds[rec[l]] > mx[rec[l]]; --l)
        ;
    printf("%lld\n%d\n", dm, r - l);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值