树型DP:求树的直径:1.两次bfs,2.树型dp : 巡逻

树的直径的定义

设树上任意两点x, y之间的距离为dis(x, y),那么树的直径就是求这棵树中的Max {dis(x,y)}

方法介绍:

1.树形dp
优点:代码简单,可以求出以当前节点为根时的最长链,O(N)时间复杂度
缺点:不能求出最长链的路径

2.两次bfs(或dfs)
优点:可以求出路径,每次使用path数组存其前驱节点即可。
缺点:时间复杂度O(2N), 在负权边中无法使用。

两次bfs的思路:

首先需要知道的一个知识是:求某个树的最长路径的方式就是, 先随便选一个点,求这个点到其他任意一个节点中,距离最远的点u. 然后在通过u找距离u最远距离的点v。 则u->v的长度就是其中一条最长路径。(也就是u一定为某一个最远距离的起点。)

证明:

 

 结论:所以随便一个点A,离A距离最远的点U必定为树中其中某一条最长路径的端点。

知道了这个知道可以了解到为什么需要两次bfs或者dfs了。

先随机选择一个点,通过bfs/dfs找到离这个点最远距离的点。确定这个点之后,在通过这个最远距离的点,通过bfs/dfs找到离它最远距离的点,这两个点之间的距离就是树的直径了了。边权为非负数才行。

树型DP:

则是通过集合的思想求其直径:边权可正可负。

其思想是,遍历所有的点,将当前所遍历到的点都当作挂钩,求经过这个挂钩但是不经过父结点的最长路径。(因为此树型dp依旧使用到了dfs的思想, 为了防止死循环,如:一直A->B,B->A......  所以不能让其dfs能够回父结点.) 

思路:

1.如果全部为正权边的时候

随机选择一个点:

但是要注意未必包含了A的就是最长路径,除非此树皆为正权边,无负权边,才满足。

证明:如果边全为正权边,为什么这样的思路是正确的?

 假设u到v1的距离最大为d1, u到v2的距离第二大,d2.  而此树的一条最长路径为x->y.

1.如果最长路径x->y经过u点。

则x->y的路径就是 x -> u -> y 。

而u-> x 的路径d3, u -> y的路径d4

一定d1 + d2  >= d3 + d4.  所以如上所说,求u的子树的两个最大值相加成立。

2.如果最长路径x -> y  不经过u点。

由于边全为正权边。 x -> k(某个中间点) -> y 。 而又是一个连通图, 所以 k 实际上一定可以到u.  

假设 k -> x 为 d3,

k -> y 为d4 . 

所以 u - > k - > x 的路径 d5 > d3

而 u - > k - > y 的路径 d6 > d4;

d3 + d4 为最长路径。

但是d1 + d2 >= d5 + d6 > d3 + d4. 所以可以说明d1 + d2要远比d3 + d4更可以成为最长路径。

从而可证,如果全为正权边,必定经过u.

 如此题全部为正权边:题目链接为:http://poj.org/problem?id=2631

使用树型dp时,最后加一个x == 1 也是对的:

int dp(int x , int father)
{
    int distmax = 0;
    int d1 = 0;
    int d2 = 0;
    for(int i = h[x] ; i != -1 ; i = ne[i])
    {
        int j = e[i];
        if(j == father)
        {
            continue;
        }
        int d = dp(j,x) + w[i];
        distmax = max(distmax , d);
        if(d > d1)
        {
            d2 = d1;
            d1 = d;
        }
        else if(d > d2)
        {
            d2 = d;
        }
    }
    /*
    if(x == 1)  // 如果为正权边,只需要看第一个即可。
    {
        ans = max(ans , d1 + d2);
    }
    */
    return distmax;
}

 但是如果存在负权边的话呢?

上面证明的情况2就未必会成立了。

 u->K为负数, 而x - > y 的最长路径不经过u时,是可行的。

此时的最长路径为x -> K -> y.

所以就需要把之前那题的树型dp的

/*
    if(x == 1)  // 如果为正权边,只需要看第一个即可。
    {
        ans = max(ans , d1 + d2);
    }
    */

去掉了。

因为可能最长路径为 以k作为挂钩,而非以u做为挂钩。

这也是为什么可以使用树型dp求解负权边情况的原因.

既用到了两次dfs,又用到了树型DP求最长路径的题目:

题目链接:https://www.acwing.com/problem/content/description/352/

题目:

在一个地区有 n 个村庄,编号为 1,2,…,n

有 n−1 条道路连接着这些村庄,每条道路刚好连接两个村庄,从任何一个村庄,都可以通过这些道路到达其他任一个村庄。

每条道路的长度均为 1 个单位。

为保证该地区的安全,巡警车每天都要到所有的道路上巡逻。

警察局设在编号为 1的村庄里,每天巡警车总是从警局出发,最终又回到警局。

为了减少总的巡逻距离,该地区准备在这些村庄之间建立 K 条新的道路,每条新道路可以连接任意两个村庄。

两条新道路可以在同一个村庄会合或结束,甚至新道路可以是一个环。

因为资金有限,所以 K 只能为 1 或 2。

同时,为了不浪费资金,每天巡警车必须经过新建的道路正好一次。

编写一个程序,在给定村庄间道路信息和需要新建的道路数的情况下,计算出最佳的新建道路的方案,使得总的巡逻距离最小。

输入格式

第一行包含两个整数 n 和 K。

接下来 n−1 行每行两个整数 a 和 b,表示村庄 a 和 b 之间有一条道路。

输出格式

输出一个整数,表示新建了 K 条道路后能达到的最小巡逻距离。

数据范围

3≤n≤100000,
1≤K≤2,
1≤a,b≤n

输入样例:

8 1 
1 2 
3 1 
3 4 
5 3 
7 5 
8 5 
5 6 

输出样例:

11

思路:

此题的思路,首先要遍历所有的道路,并且n个节点n - 1条道路,所以可以知道要遍历所有的道路,需要每条道路来一次回去一次,共 2 * (n - 1)条道路。

现在修一条新的道路,我们可以知道,修一条新的道路可以使得某两个节点之间的距离变为1。而在修建这个道路之前,遍历最长路径L, 需要经过 2 * L遍。

但是如果在其中一条最长路径的两个端点上连接一条边的话, 则将2 * L 变为了 L + 1条。

所以k == 1的情况很好考虑, 就是在最长路径的两端建一条路,使得路径变为 2 * (n - 1) -L + 1。

但是如果k == 2的情况呢?如果修建的第二条最长路径 与 第一条最长路径之间的无重复路径,那么很棒, 结果为 2 * (n - 1) - L1 +1 - L2 + 1;

但是如果第二条最长路径 与 第一条最长路径之间有重复道路会发生什么?

 如:修建了1号路后, A-B-E-F-C-A。

修建了2号路后,A-D-F,由于新建的路只能走一次,所以不能从F->D,也不能从F->E,

F-C-A。

所以会发现什么?我们修1的时候,就是为了只走一遍,F-C-A。

但是修建了2号路后,我们由于新建道路只能走一次,所以重复的走过的路,F-C-A又走了一遍。

原本这样创建是为了少走重复的路。但这样创建环中部分边重复了,于是导致原本走一遍重新变为了走两遍。

那么是不是说明这样的方法就是错的了呢?

这就有一个奇特的技巧:

将第一次走过的最长路径的环上所有边后期变为-1。然后在求一次最长路径。

为什么这样的情况是最优的呢?

根据我们的公式 : 2 * (n - 1) - L1 +1 - L2 + 1;

可以发现,-L1 + 1的含义并没有变。

 - L2 + 1中有一个新的含义,就是如果L2中存在重复的边,也就是新选的新最长路径,在走一次重复的边后,-1后,还能是一条最长路径L2,那就选择它,再走一遍这条路也没事。那么 - L2中那部分(-1)不就相当于含义中的重复路线又走了一遍嘛。所以是正确的。

参考文献:

1.https://www.cnblogs.com/gzh-red/p/11178619.html

2.https://b23.tv/L6Pmm1

代码实现:

# include <iostream>
# include <queue>
# include <cstring>
using namespace std;

const int N = 100010 , M = 200010;

int n , k;

int h[N],e[M],ne[M],w[M],idx;
int choose[N];
int dist[N];  // 以1最为根节点
int path[N]; // 存储访问此节点的前驱节点

void add(int a , int b , int wei)
{
    e[idx] = b;
    w[idx] = wei;
    ne[idx] = h[a];
    h[a] = idx++;
}

int bfs(int x)
{
    queue<int> q;
    memset(dist,0x3f,sizeof dist);
    memset(path,0,sizeof path);
    dist[x] = 0;
    q.push(x);
    path[x] = 0; // path[x],存x的父结点
    while(q.size())
    {
        int t = q.front();
        q.pop();
        for(int i = h[t] ; i != -1 ; i = ne[i])
        {
            int j = e[i];
            if(dist[j] == 0x3f3f3f3f) // 如果j没有被访问过
            {
                dist[j] = dist[t] + w[i];
                //t -> j,e[i] 的值为 w[i]
                path[j] = t;  // 因为我们后面要将w[i]改为-1,所以存入的是下标i,e[i] == j
                q.push(j);
            }
        }
    }

    // 找到距离x最远的节点
    int maxlen = 0;
    int aim = 1;

    for(int i = 1 ; i <= n ; i++)
    {
        if(dist[i] > maxlen)
        {
            aim = i;
            maxlen = dist[i];
        }
    }

    return aim;
}

void change(int x)
{
    
    int aim = x;

    //当前节点为 aim
    //父结点为 p[aim].first ,  父结点到aim的距离为 p[aim].second
    while(path[aim])  // 除了修改 aim -> path[aim]的距离外, 还要修改 path[aim] -> aim的距离
    {
        int t = path[aim];
        for(int i = h[t] ; i != -1; i = ne[i])
        {
            int j = e[i];
            if(j == aim)
            {
                w[i] = -1;
                break;
            }
        }
        for(int i = h[aim] ; i != -1 ; i = ne[i])
        {
            int j = e[i];
            if(j == t)
            {
                w[i] = -1;
                break;
            }
        }
        aim = t;
    }
}


int ans = 0;
int dp(int x , int father)
{
    int distmax = 0;
    int d1 = 0;
    int d2 = 0;
    for(int i = h[x] ; i != -1 ; i = ne[i])
    {
        int j = e[i];
        if(j == father)
        {
            continue;
        }
        int d = dp(j,x) + w[i];
        distmax = max(distmax , d);
        if(d > d1)
        {
            d2 = d1;
            d1 = d;
        }
        else if(d > d2)
        {
            d2 = d;
        }
    }
    ans = max(ans , d1 + d2);
    return distmax;
}




int main()
{
    memset(h,-1,sizeof h);
    scanf("%d %d",&n,&k);
    for(int i = 1 ; i <= n - 1 ; i++)
    {
        int a,b;
        scanf("%d %d",&a,&b);
        add(a,b,1);
        add(b,a,1);
    }
    
    if(k == 1)
    {
        int p = bfs(1);  //先随便挑一个点,找到离他最远的点
        int temp = bfs(p);  // 然后找到离p距离最远的点temp
        int l1 = dist[temp];  //输出的就是最长的直径
        printf("%d\n",2 * (n - 1) - l1 + 1);
    }
    else
    {
        int p = bfs(1);  //先随便挑一个点,找到离他最远的点
        int temp = bfs(p);  // 然后找到离p距离最远的点temp
        int l1 = dist[temp];  //输出的就是最长的直径
        change(temp);
        dp(1,-1);
        int l2 = ans;
        printf("%d\n",2 * (n - 1) - l1 - l2 + 2);
    }
    return 0;
}

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值