【kruskal最大生成树+最大公共祖先lca】P1967 货车运输

题目描述

A 国有 n 座城市,编号从 1 到 n,城市之间有 m 条双向道路。每一条道路对车辆都有重量限制,简称限重。现在有 q 辆货车在运输货物, 司机们想知道每辆车在不超过车辆限重的情况下,最多能运多重的货物。

输入输出格式

输入格式:

输入文件名为 truck.in。

输入文件第一行有两个用一个空格隔开的整数 n,m,表示 A 国有 n 座城市和 m 条道

路。 接下来 m 行每行 3 个整数 x、 y、 z,每两个整数之间用一个空格隔开,表示从 x 号城市到 y 号城市有一条限重为 z 的道路。注意: x 不等于 y,两座城市之间可能有多条道路

接下来一行有一个整数 q,表示有 q 辆货车需要运货。

接下来 q 行,每行两个整数 x、y,之间用一个空格隔开,表示一辆货车需要从 x 城市运输货物到 y 城市,注意: x 不等于 y

输出格式:

输出文件名为 truck.out。

输出共有 q 行,每行一个整数,表示对于每一辆货车,它的最大载重是多少。如果货

车不能到达目的地,输出-1。


大致思路

/* 因为要求的是每个司机的载重限制的最大值,所以建一棵最大生成树,也用kruskal来做,但是边权从大到小排序,

生成的最大树上两点间的最小边权就是最大载重。

不用加两条边,加一条边就可以。

生成了树之后,处理处每个点的lca,以便求最大载重。

怎么求lca?

在跑Kruskal将两个树合并的时候,在两棵树根节点之间连一条边,把连的边保存下来

让b树的根的爸爸指向a树的根 ——>

Add_edge(father[fx],fy,edge1[i].w);

father[fy]=fx;

如果一个点的father等于他本身,那么这就是一个树根。然后从这个dfs求lca,因为可能有多棵树,所以for循环枚举一遍。

怎样判断能不能到达?

如果两个点所属的连通块不同——>find(x)!=find(y),则无法到达。

那么怎么求最大载重呢?

在dfs求lca时候,记录下每个点到他的父亲节点的距离,进行一次询问的时候,求出两个点的lca,然后一直沿着父亲节点跳,直到跳到lca为止,跳的时候取min。


我的语言阐述思路

这是道难题,花了我两个多小时(照着题解读)也没AC。虽然暂时放置bug,但是仍想说下思路,思路很厉害。

本题给我们的是一个图,我们首先想到的可能是求出各个点之间的最短路径,但这一想就不现实,因为多源最短路径都要O(N^3)的时间复杂度(FLOYD),而单源最短路径(DIJKSTRA堆优化, SPFA)一遍也会TLE,所以我们应换一种思路思考这道题。

思路其实也就是说,根据题意构建好图后,需要用kruskal来构建最大生成树,然后针对这个最大生成树来求两点间的最近公共祖先(联想树的结构,运输沿途一定会通过这个最近公共祖先的!),然后找这两个点到最近公共祖先之间的权值最小的那条边即可。抓住性质:“最小生成树的最长边一定是所有生成树中最小的,最大生成树的最短边一定是所有生成树中最大的”。正好符合想要尽可能多装货物的题设。

之前用到kruskal通常是求最大连通子图的最短路长度==》最小生成树的权值的,不需要把真正的最小生成树构建出来,而这道题,因为我需要用lca求最大公共祖先,那么一定需要把树构造出来!(不然你怎么dfs去把每个点的父节点和深度初始化)!那么就是在并查集Merge的同时相应地建边,重新做个edge[MAX_M]出来。

其次还有个重点是,你需要注意可能题目给的图压根就不连通,所以kruskal构造出来的生成树有几个,那么其实遍历一遍把满足father[i]==i的点作为根节点,去dfs该根节点对应的连通块就好了。

倍增时我们可以参考之前的题目的经验,用一个二维数组f存储,其中f[i,j]表示第i个节点往上(根节点方向)跑2^j个点到达的点,但这样我们还是无法知道两点之间的最小边(题目要求的答案),那么就在初始化深度和父节点的时候顺带初始化数组dis[i]表示i结点和它的第一个父节点的边的权值。最后从两个点分别依次一个一个向前跳到公共祖先处,每次跳的时候更新最小边权即可。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define N 10005
using namespace std;
int n,m,num_edge1,num_edge,q,x,y,z;
int head1[N],head[N],father[N],deep[N];
int fa[N][16],dis[N],chudu[N];
struct Edge
{
    int u,v,w,next;
    bool operator < (Edge a) const        //边权从大到小排序,生成最大树 
    {
        return w>a.w;
    }
}edge1[N*5],edge[N];
void read(int &num)
{
    num=0;
    char c=getchar();
    for(;!isdigit(c);c=getchar());
    for(;isdigit(c);c=getchar()){num=num*10+c-'0';}
}
void add_edge(int u,int v,int w)
{
    edge1[++num_edge1].u=u;
    edge1[num_edge1].v=v;
    edge1[num_edge1].w=w;
    edge1[num_edge1].next=head1[u];
    head1[u]=num_edge1;
}
void Add_edge(int u,int v,int w)
{
    edge[++num_edge].u=u;
    edge[num_edge].v=v;
    edge[num_edge].w=w;
    edge[num_edge].next=head[u];
    head[u]=num_edge;
}
int find(int x)
{
    if(father[x]!=x) father[x]=find(father[x]);
    return father[x];
}
void Kruskal()
{
    for(int i=1;i<=n;i++) father[i]=i;     //注意克鲁斯卡尔前要初始化并查集 
    sort(edge1+1,edge1+m+1);  
    for(int i=1;i<=m;i++)      //最大生成树没有限制只能有n-1条边! 
    {
        int fx=find(edge1[i].u),fy=find(edge1[i].v);
        if(fx==fy) continue;
        Add_edge(father[fx],fy,edge1[i].w);        //生成的树,保存下来,用来dfs处理lca 
        father[fy]=fx;
    }
}
void dfs(int u)        //处理lca 
{
    for(int i=1;i<=14;i++)
    {
        fa[u][i]=fa[fa[u][i-1]][i-1];
    }
    for(int i=head[u];i;i=edge[i].next)
    {
        if(fa[u][0]==edge[i].v) continue;
        fa[edge[i].v][0]=u;
        dis[edge[i].v]=edge[i].w;        //该点到第一个父亲节点的距离 
        deep[edge[i].v]=deep[u]+1;
        dfs(edge[i].v);
    }
}
int get_lca(int x,int y)    //求lca 
{
    if(deep[x]<deep[y]) swap(x,y);
    int h=deep[x]-deep[y];
    for(int i=14;i>=0;i--)
    {
        if(h&(1<<i))
            x=fa[x][i];
    }
    if(x!=y)
    {
        for(int i=14;i>=0;i--)
        {
            if(fa[x][i]!=fa[y][i])
            {
                x=fa[x][i];
                y=fa[y][i];
            }
        }
        x=fa[x][0];
    }
    return x;
}
int work(int x,int y)        //求最大载重  //求x到f之间的最小权值的边(注:f是x的祖先 
{
    int a=999999999,b=999999999;
    int lca=get_lca(x,y);
    //printf("lca:  %d\n",lca);
    for(int i=x;i!=lca;i=fa[i][0])        //a往lca跳 //一个一个的跳,一条边一条边依次的看最小是多少
    {
        a=min(a,dis[i]);
    }
    for(int i=y;i!=lca;i=fa[i][0])        //b往lca跳 //一个一个的跳,一条边一条边依次的看最小是多少
    {
        b=min(b,dis[i]);
    }
    return min(a,b);    //最大载重即边权的最小值 
}
int main()
{
    read(n),read(m);
    for(int i=1;i<=m;i++)
    {
        read(x),read(y),read(z);
        add_edge(x,y,z);
        //printf("z:  %d\n",z);
        //printf("W:  %d\n",edge1[i].w);
    }
    Kruskal();
    for(int i=1;i<=n;i++)
    {
        if(find(i)==i)    若如此,则该点为根节点(不管有多少个连通块  
        {
            fa[i][0]=i;
            //printf("root:  %d\n",i);
            dfs(i);
        }
    }
    read(q);
    /*for(int i=1;i<=n;i++)
    {
        //printf("hh:  %d %d\n",fa[i][0],dis[i]);
    }*/
    for(int i=1;i<=q;i++)
    {
        read(x),read(y);
        if(find(x)!=find(y))    //不在同一棵树中,不能相互到达 
        {
            printf("-1\n");
            continue;
        }
        printf("%d\n",work(x,y));
    }
    fclose(stdin);
    fclose(stdout);
    return 0;
}

当然,我不知道比赛的时候会不会遇到这种题,因为确实码这个代码要花挺长时间的,最主要最后还不一定能跑对。但是思路确实是很有用的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值