洛谷题单指南-图论之树-P1099 [NOIP 2007 提高组] 树网的核

原题链接:https://www.luogu.com.cn/problem/P1099

题意解读:计算树中其他点与树的直径上一段长度不超过s的路径上最近的点的最大距离的最小值。

解题思路:

在思考此题之前,需要了解树的直径,先来做这道题:https://www.luogu.com.cn/problem/B4016

树的直径详解

1、定义

树的直径是指树中任意两个节点之间最长的路径长度,这条路径也被称为树的最长链。在一棵树中,可能存在多条直径,但它们的长度是相等的。

2、性质

中点性质:树的直径的中点(如果直径长度为偶数,则是中间两个节点;如果为奇数,则是中间一个节点)是树的重心之一(当树有两个重心时,直径中点刚好是这两个重心)。

端点性质:从树的任意一个节点出发,找到距离它最远的节点 u,再从节点 u 出发,找到距离 u 最远的节点 v,那么节点 u 到节点 v 的路径就是树的一条直径。

3、求解方法

主要有两种常见的方法来求解树的直径:

a、两次深度优先搜索(DFS)或广度优先搜索(BFS)

从树中任意一个节点 s 开始进行 DFS 或 BFS,找到距离 s 最远的节点 u。

再从节点 u 开始进行 DFS 或 BFS,找到距离 u 最远的节点 v。节点 u 到节点 v 的路径就是树的一条直径,直径的长度就是两次搜索中记录的最大距离。

注意:如果图中存在负权边,两次DFS法将无法应对。

b、树形动态规划(DP)

对于树中的每个节点 u,计算以 u 为根的子树中,从 u 出发的最长路径 d1[u] 和次长路径 d2[u]。

树的直径就是所有节点 u 的 d1[u] + d2[u] 中的最大值。

100分代码-B4016-两次DFS法:(其中注释内容为输出直径路径的代码)

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

const int N = 100005;
vector<int> g[N]; 
int n;

int st; //搜索的起点
int ed; //距离起点最远距离的点
int maxdist; //距离起点最远的距离
int from[N]; //记录路径

void dfs(int u, int parent, int depth)
{
    if(depth > maxdist)
    {
        ed = u;
        maxdist = depth;
        //from[u] = parent; 记录路径
    }
    for(auto v : g[u])
    {
        if(v == parent) continue;
        dfs(v, u, depth + 1);
    }
}

int main()
{
    cin >> n;
    int u, v;
    for(int i = 1; i <= n; i++)
    {
        cin >> u >> v;
        g[u].push_back(v);
        g[v].push_back(u);
    }
    st = 1;
    dfs(st, 0, 0); //从1开始搜索路径最长的节点
    maxdist = 0;
    st = ed;
    //memset(from, 0, sizeof(from)); 清空路径
    dfs(st, 0, 0); //搜索直径长度
    cout << maxdist << endl;
    /*
    输出直径上的点
    do
    {
        cout << ed << " ";
        ed = from[ed];
    } while(ed != 0);
     */

    return 0;
}

100分代码-B4016-树形DP法:(其中注释内容为输出直径路径的代码)

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

const int N = 100005;
vector<int> g[N]; 
int dist1[N]; //dist1[u]表示以u为根的子树中,以u为起点的最长路径长度
int dist2[N]; //dist2[u]表示以u为根的子树中,以u为起点的次长路径长度
//int to1[N]; //to1[u]表示以u为根的最长路中,u的下一个节点
//int to2[N]; //to2[u]表示以u为根的次长路中,u的下一个节点
int ans; //直径长度
//int pos; //直径所在树的根
int n;

void dfs(int u, int parent)
{
    dist1[u] = dist2[u] = 0;
    for(auto v : g[u])
    {
        if(v == parent) continue;
        dfs(v, u);
        int t = dist1[v] + 1;
        if(t > dist1[u])
        {
            dist2[u] = dist1[u];
            //to2[u] = to1[u];
            dist1[u] = t;
            //to1[u] = v;
        }
        else if(t > dist2[u])
        {
            dist2[u] = t;
            //to2[u] = v;
        }
    }
    if(dist1[u] + dist2[u] > ans)
    {
        ans = dist1[u] + dist2[u];
        //pos = u;
    }
}

//输出直径的点
/*
void showpath()
{
    vector<int> path;
    int t = pos;
    while(t != 0)
    {
        path.push_back(t);
        t = to1[t];
    }
    reverse(path.begin(), path.end());
    t = to2[pos];
    while(t != 0)
    {
        path.push_back(t);
        t = to2[t];
    }
    for(auto v : path) cout << v << " ";
    cout << endl;
}
*/

int main()
{
    cin >> n;
    int u, v;
    for(int i = 1; i < n; i++)
    {
        cin >> u >> v;
        g[u].push_back(v);
        g[v].push_back(u);
    }
    dfs(1, 0);
    cout << ans << endl;
    //showpath();
    return 0;
}

回到本题,要计算与直径上一段路径的距离最大值,而直径可能不止一条,需要证明两个性质:

性质1:如果树有多条直径,直径一定相交

证明:反证法。假设有两条直径,且直径不相交,则有如下情况:

a+b = c+d,分别为两条直径,x是两条直径之间的通路(因为树一定连通),设a<=b,c<=d,a小于等于直径的一半,d大于等于直径的一半,一定有d>=a,那么一定有b+x+d >= b+x+a > a + b,那么就找到一条比a+b更长的路径,产生矛盾,所以直径一定相交,也就是多条直径一定有公共的一段。

性质2:到树网的核的偏心距的最小值一定在多条直径相交的区域

证明:设a+x+b = c+x+d,是两条直径,并且a<=b, c<=d

先来证明a一定等于c,b一定等于d。反证法,假设a!=c,设a<c,那么一定有c+x+b>a+x+b,与直径是最长路径产生矛盾,所以a=c,b=d。

再来证明核在x区域才会带来最小的偏心距。

假设核在a上,偏心距(到直径上一段路径的距离最大值)主要看三部分:核两端与直径两端的距离,核中间分支出来的最远距离,a中红色分支出来的最远距离一定不会超过到直径端点的距离,否则从分支走路径更长,因此核如果在a上,偏心距一定是到核的端点到直径两端点的距离最大值。同理,如果核在x上,偏心距也看三部分,但是核的两端点与直径两端点距离较大值一定比核在a上的最大值要小,也就是核在x上对答案的贡献是最优的。

因此,无论取哪条直径来取核,对答案最优贡献都在公共相交区域,这样一来,任取一条直径来枚举核就可以。

由于核这段路径在直径上,有两种情况:

1、<=s的路径存在,必须是一段路径,路径以外的点与路径的距离又分两种情况:

第一种:路径两端点显然距离直径两端点最远,因为路径在直径上

第二种:路径除两端点之外中间的点能到的最远距离,就是路径中间形成分支的部分,可以把直径上所有点经过非直径能到的最远距离通过dfs预处理出来

对于距离一段路径的最大值,可以结合上面两种情况综合计算。

2、<=s的路径不存在,是直径上的每个点,每个点能到的最远距离必然是直径上的两端点

因此,可以分三步走:

1、寻找直径,将直径中的点、直径每段长度分别保存

2、判断<=s的路径是否存在,如果存在则枚举所有符合要求的路径,计算路径两端点以及路径中间点的最大偏心距,更新答案

3、如果<=s的路径不存在,则枚举直径上所有点,计算所有点的最大偏心距,更新答案

100分代码-P1099:

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

const int N = 305;

int g[N][N];
int n, s;
int st, ed, maxdist; //起始点,结束点,最大距离
int fa[N]; //父节点
int diamond[N], cnt1; //直径中所有点
int section[N], cnt2, minw = 1e9; //直径中每一段的长度,最小的一段长度
int vis[N]; //对直径中点进行标记
int maxoffset[N]; //直径上的点沿着非直径能到的最大距离
int offset; //偏心距
int ans;

//计算以u为根的子树,从u能到的最远的点
void dfs1(int u, int parent, int dist)
{
    fa[u] = parent; //记录路径
    if(dist > maxdist)
    {
        maxdist = dist;
        ed = u;
    }
    for(int v = 1; v <= N; v++)
    {
        if(g[u][v] == -1 || v == parent) continue;
        dfs1(v, u, dist + g[u][v]);
    }
}

//计算从u开始,不经过直径上的点,能到的最远距离
void dfs2(int u, int parent, int dist)
{
    offset = max(offset, dist);
    for(int v = 1; v <= N; v++)
    {
        if(g[u][v] == -1 || vis[v] || v == parent) continue;
        dfs2(v, u, dist + g[u][v]);
    }
}

int main()
{
    memset(g, -1, sizeof(g));
    cin >> n >> s;
    int u, v, w;
    for(int i = 1; i < n; i++)
    {
        cin >> u >> v >> w;
        g[u][v] = g[v][u] = w;
    }
    st = 1;
    dfs1(st, 0, 0);
    st= ed, maxdist = 0;
    dfs1(st, 0, 0);
    int t = ed;
    while(t != 0)
    {
        vis[t] = true; //标记直径上的点
        diamond[++cnt1] = t; //记录直径中的点
        if(fa[t] != 0) section[++cnt2] = g[fa[t]][t]; //记录直径的每一段长度
        minw = min(minw, section[cnt2]);
        t = fa[t];
    }

    //直径上的点沿着非直径能到的最大距离
    for(int i = 1; i <= cnt1; i++)
    {
        offset = 0;
        dfs2(diamond[i], 0, 0);
        maxoffset[diamond[i]] = offset;
    }

    ans = maxdist;

    if(minw <= s) //树网的核是直径上的一段
    {
        int left = 0, right = 0;
        for(int i = 1; i <= cnt2; i++)
        {
            int center = 0;
            left += section[i - 1]; //i之前所有段的长度和
            int maxo = 0;
            for(int j = i; j <= cnt2; j++)
            {
                center += section[j]; //i~j的长度和
                if(j > i) maxo = max(maxo, maxoffset[diamond[j]]); //i~j一段中间的点沿着非直径能到的最远距离,不包括两边端点
                if(center > s) break;
                right = maxdist - left - center; //j之后的长度和
                ans = min(ans, max(maxo, max(left, right))); //树网的和是i~j一段的情况
            }
        }
    }
    else //树网的核是直径上的某点
    {
        for(int i = 1; i <= cnt2; i++)
        {
            int left = 0, right = 0;
            for(int i = 1; i <= cnt2; i++)
            {
                left += section[i - 1];
                right = maxdist - left;
                ans = min(ans, max(left, right));
            }
        }
    }

    cout << ans;

    return 0;
}

进一步优化

由于在枚举核的过程复杂度为O(n^2),如果数据范围扩大将无法通过

优化的关键在于贪心+双指针:

贪心1:当枚举核路径时,左端点固定,右端点越远越好,如果右端点远到路径和超过s,左端点再往前移动,因为更多的点参与到计算最小的偏心距,对答案只会更优。

贪心2:不需要额外考虑是取一个点还是取一段区域的情况,只需要枚举所有最长可能的路径(包括一个点的情况),然后在左端点距离直径开头的距离、右端点距离直径结尾的距离、直径所有点分支出来的最远距离的最大值,三者中求最大值即可得到最大偏心距,然后取最小者。因为缩到一个点一定是可能的情况,这样每个点只考虑分支的最远距离,路径区间只考虑距离直径两端点的距离即可。

存储图也改为邻接表。整体复杂度为O(n)

100分代码:https://www.acwing.com/problem/content/description/353/

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

const int N = 500005;

struct Node
{
    int v, w;
};
vector<Node> g[N];
int n, s;
int st, ed, maxdist; //起始点,结束点,最大距离
int fa[N], fd[N]; //父节点,到父节点的距离
int diamond[N], cnt1; //直径中所有点
int section[N], cnt2, minw = 1e9; //直径中每一段的长度,最小的一段长度
int sum[N]; //section的前缀和
int vis[N]; //对直径中点进行标记
int offset, maxoffset; //偏心距, 最大偏心距
int ans;

//计算以u为根的子树,从u能到的最远的点
void dfs1(int u, int parent, int dist)
{
    fa[u] = parent; //记录路径
    if(dist > maxdist)
    {
        maxdist = dist;
        ed = u;
    }
    for(auto nd : g[u])
    {
        int v = nd.v, w = nd.w;
        if(v == parent) continue;
        fd[v] = w; //记录到父节点距离
        dfs1(v, u, dist + w);
    }
}

//计算从u开始,不经过直径上的点,能到的最远距离
void dfs2(int u, int parent, int dist)
{
    offset = max(offset, dist);
    for(auto nd : g[u])
    {
        int v = nd.v, w = nd.w;
        if(v == parent || vis[v]) continue;
        dfs2(v, u, dist + w);
    }
}

int main()
{
    cin >> n >> s;
    int u, v, w;
    for(int i = 1; i < n; i++)
    {
        cin >> u >> v >> w;
        g[u].push_back({v, w});
        g[v].push_back({u, w});
    }

    st = 1;
    dfs1(st, 0, 0);
    st= ed, maxdist = 0;
    dfs1(st, 0, 0);

    int t = ed;
    while(t != 0)
    {
        vis[t] = true; //标记直径上的点
        diamond[++cnt1] = t; //记录直径中的点
        if(fa[t] != 0) 
        {
            section[++cnt2] = fd[t]; //记录直径的每一段长度
            sum[cnt2] = sum[cnt2 - 1] + section[cnt2]; //前缀和
        }
        minw = min(minw, section[cnt2]);
        t = fa[t];
    }

    //直径上的点沿着非直径能到的最大距离
    for(int i = 1; i <= cnt1; i++)
    {
        offset = 0;
        dfs2(diamond[i], 0, 0);
        maxoffset = max(maxoffset, offset);
    }

    ans = maxdist;
    for(int i = 0, j = 0; i <= cnt2; i++) //i尽可能往后走
    {
        while(sum[i] - sum[j] > s) j++; //如果j~i距离超过s,j再往前,确保
        int left = sum[j];
        int right = sum[cnt2] - sum[i];
        ans = min(ans, max(maxoffset, max(left, right))); 
    }

    cout << ans;

    return 0;
}

 

posted @   五月江城  阅读(25)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?
历史上的今天:
2024-03-04 洛谷题单指南-搜索-P1443 马的遍历
2024-03-04 洛谷题单指南-搜索-P2392 kkksc03考前临时抱佛脚
2024-03-04 洛谷题单指南-搜索-P1219 [USACO1.5] 八皇后 Checker Challenge
2024-03-04 洛谷题单指南-二分查找与二分答案-P3743 kotori的设备
点击右上角即可分享
微信分享提示