LG1399 [NOI2013]快餐店

发布于 2020-06-13  2.49k 次阅读


题面

题意分析

题目中会给出n个点,每个点都有一个父亲,这样形成一个图,让你在某一处(可以在点上,也可以在边上)安置一个快餐店使得整张图上离它最远的点最近。

很容易想到在题目最后至少有两个点离快餐店的距离相同,因为如果不相同,快餐店可以朝较远点的方向移动一段距离,最后答案更优。而由于快餐店离这两个点的距离都是最长的,所以这两个点之间的距离应该是所有点中最长的,即这两个点之间的路径就是直径。所以快餐店必然在直径的正中间以使得它离直径两端距离相同。

所以如果我们可以算出整张图中的直径,记直径长为$ans$,则最后答案就是$ans/2$,输出即可。

朴素算法

由于这整张图是一个基环树,我们可以考虑每次破掉环上的一条边使它变成一颗树,然后求出直径,最后统计所有的直径中最短的那个作为答案即可。

时间复杂度$O(n^2)$,实现较简单

正解

时间复杂度$O(n)$

首先我们可以把环上任意点所在的子树的参数存放在这个点上,然后对于这些点进行操作。

我们先考虑整个图上的直径可能存在的情况。
- 这个直径完全在环上某一点所在的子树中。
- 这个直径从环上某一点所在子树出发,到达该点后在环上走过一些边到达另一个点,进入该点所在子树并且结束。

第一种情况解决比较简单,我们只需要遍历环上的每一个点,求出它所在子树的直径计入答案即可。

对于第二种方法,我们容易发现直径在某子树内的部分一定是从该子树的根出发的最长路径,即树的最大深度。所以我们预先处理环上所有点所在子树的最大深度记为$dis_i$。

然后我们定义四个数组,暂且命名为$A,B,C,D$。记环上点数为$cnt$,环上点离1号点的距离为$pre_i$,离cnt号点的距离为$sub_i$则
- $A_i$表示环上$1\leq x\leq i$时$dis_x+pre_x$的最大值
- $B_i$表示环上$1\leq x\leq y\leq i$中$dis_x+dis_y-pre_x+pre_y$的最大值
- $C_i$表示环上$i\leq x\leq cnt$中$dis_x+sub_x$的最大值
- $D_i$表示环上$i\leq x\leq y\leq cnt$中$dis_x+dis_y+sub_x-sub_y$的最大值

四个数组分别存放的是,不完整的直径的右半边的最大值,处在i左侧的直径的最大值,不完整的直径的左半边的最大值,和处在i右侧的直径的最大值。

所以我们可以很自然的想到对于某一个i,在环上断开i和i+1之间的连边后直径应该可以分成三部分(其中tmp是1和cnt两点间边的长度):
- 只在$1\sim i$之间的直径,即$B_i$
- 只在$i+1\sim cnt$之间的直径,即$D_i$
- 一半在$1\sim i$之间,一半在$i+1\sim cnt$之间,通过1号点和cnt号点的连边连接的直径,即$A_i+C_{i+1}+tmp$

所以综合起来我们从i号点断开的直径长度即为$max(B_i,D_i,A_i+C_{i+1}+tmp)$,我们只需要循环枚举n次即可求出答案,时间复杂度$O(n)$。

然后对于找环操作,时间复杂度$O(n)$,因为每一个点只访问过一次;对于每颗子树上的求直径和求深度,时间复杂度均为$O(n)$;最后的dp操作也是$O(n)$。所以总时间复杂度为$O(n)$。

细节

但是这里还有一些细节。因为我们需要求出直径,所以在计算过程中(如最后dp过程中比较三种情况)需要使用max,但是最后在统计过程中(就是将结果统计进ans的时候)我们需要使用min。因为统计过程中接触的每一个答案都是在某一种情况下可以达到的直径长度,按照题面描述,最小的即为最优解,我们需要取min。

而且我最开始偷懒尝试将直径两种情况(在一棵子树中和在两棵子树中)的结果用一个answer变量一起解决,但是到最后我发现我只拿到了80分,有20分WA了并且都是答案偏小。后来明白是对于两个answer,我们肯定是要取max的(因为如果两者中较小的值是直径,那么较大者显然比它更长,所以较大者才应该是直径,所以取max),但是在answer1和answer2的内部统计中我们全部采用的是min,所以两个变量是不可以混用的。(小细节而已啦,只是自己犯过的一个错)

这些细节可以在下面的代码中看到。

代码

#include <bits/stdc++.h>
using namespace std;

const int maxn = 1e5 + 5;

struct edge
{
    int to, from, cost;
} graph[maxn << 1];
int last[maxn << 1], cnt = 0;

int fa[maxn], cost[maxn];
int ringDist[maxn]; //环上子树的前面的边
int ring[maxn], ringCnt = 0;
bool isRing[maxn]; //是否在环上
int dfn[maxn], timet = 0;
long long dis[maxn]; //子树最大深度

long long A[maxn], //前缀+子树最大深度
    B[maxn],       //前缀中两个子树的最大深度+两点距离
    C[maxn],       //后缀+子树最大深度
    D[maxn];       //后缀中两个子树的最大深度+两点距离

void add(int x, int y, int z)
{
    graph[++cnt].to = y;
    graph[cnt].cost = z;
    graph[cnt].from = last[x];
    last[x] = cnt;
    return;
}

void dfs(int p)
{
    dfn[p] = ++timet;
    for (int i = last[p]; i; i = graph[i].from)
    {
        int to = graph[i].to;
        if (to != fa[p])
            if (!dfn[to]) //正常递归
            {
                fa[to] = p;
                cost[to] = graph[i].cost;
                dfs(to);
            }
            else if (dfn[to] > dfn[p]) //找到环
            {
                for (; to != p; to = fa[to]) //绕一圈
                {
                    isRing[to] = true;
                    ring[++ringCnt] = to;
                    ringDist[ringCnt] = cost[to];
                }
                isRing[p] = true;
                ring[++ringCnt] = p;
                ringDist[ringCnt] = graph[i].cost;
            }
    }
    return;
}

long long ans = 0;

void work(int p, int fa)
{
    for (int i = last[p]; i; i = graph[i].from)
    {
        int to = graph[i].to;
        if (!isRing[to] && to != fa)
        {
            work(to, p);
            ans = max((long long)dis[p] + dis[to] + graph[i].cost, ans);
            dis[p] = max(dis[p], dis[to] + graph[i].cost);
        }
    }
    return;
}

int main()
{
    int n;
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
    {
        fa[i] = i;
        int from, to, dist;
        scanf("%d%d%d", &from, &to, &dist);
        add(from, to, dist);
        add(to, from, dist);
    }
    dfs(1);
    for (int i = 1; i <= ringCnt; i++)
        work(ring[i], 0); //对于环上的每一个点进行子树的信息统计
    long long sum = 0, maxx = 0;
    for (int i = 1; i <= ringCnt; i++)
    {
        sum += ringDist[i - 1];                          //sum是前缀
        A[i] = max(A[i - 1], dis[ring[i]] + sum);        //子树深度+前缀长度
        B[i] = max(B[i - 1], sum + maxx + dis[ring[i]]); //用之前最深的+当前子树深度+两子树之间距离
        maxx = max(maxx, dis[ring[i]] - sum);            //维护最深的子树深度
        //这里我们直接将dis和前缀一起存放,方便操作
    }
    sum = maxx = 0;
    int tmp = ringDist[ringCnt];
    ringDist[ringCnt] = 0; //切开环
    for (int i = ringCnt; i; i--)
    {
        sum += ringDist[i];                              //sum是后缀
        C[i] = max(C[i + 1], dis[ring[i]] + sum);        //子树深度+后缀长度
        D[i] = max(D[i + 1], sum + maxx + dis[ring[i]]); //用之前最深的+当前子树深度+两子树之间距离
        maxx = max(maxx, dis[ring[i]] - sum);            //维护最深的子树深度
    }
    long long ans2 = B[ringCnt]; //从1和cnt之间断开的情况
    for (int i = 1; i < ringCnt; i++)
        ans2 = min(max(max(B[i], D[i + 1]), A[i] + C[i + 1] + tmp), ans2); //从i拆开的三种情况
    printf("%.1lf", (double)max(ans, ans2) / 2.0);
    return 0;
}

我缓慢吐出一串啊吧啊吧并不再想说话