NOIP2013货车运输 题解

题目描述

 A 国有 n 座城市,编号从 1 到 n,城市之间有 m 条双向道路。每一条道路对车辆都有重量限制,简称限重。

 现在有 q 辆货车在运输货物,司机们想知道每辆车在不超过车辆限重的情况下,最多能运多重的货物。

输入

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

 接下来 m 行每行 3 个整数 x、y、z,每两个整数之间用一个空格隔开,表示从 x 号城市到 y 号城市有一条限

重为 z 的道路。注意:x 不等于 y,两座城市之间可能有多条道路。

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

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

注意:x 不等于 y。

 

 对于 30%的数据,0 < n < 1,000,0 < m < 10,000,0 < q < 1,000;
 对于 60%的数据,0 < n < 1,000,0 < m < 50,000,0 < q < 1,000;
 对于 100%的数据,0 < n <10,000,0 < m < 50,000,0< q < 30,000,0 ≤ z ≤ 100,000。

输出

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

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

样例输入

4 3
1 2 4
2 3 3
3 1 1
3
1 3
1 4
1 3

样例输出

3
-1
3

 

http://z6363636363.blog.163.com/blog/static/246696032201571053027123/

上面是《PascalOIERC语言常识》,(顺便安利一下我的博客,不要吐槽名字<_<)

 

 

贪心的思考一下,任意两点之间最大的载重一定在每个子图的最大生成树T上,因为如果不在T上,则这条路线最大的载重一定比原来路线的权值大,这与T是最大生成树相驳。

 

所以可以知道,首先构造最大生成树,然后对于每一组询问,查找最近公共祖先,输出最小边的权值即可,构造最大生成树相信都会吧,所以重点就落到了查找最近公共祖先上了,查找最近公共祖先,有两个常用的算法,离线的Tarjan算法,在线的树上倍增算法,什么是离线和在线呢?所谓的在线算法就是实时性的,比方说,给你一个输入,算法就给出一个输出,就像是http请求,请求网页一样。给一个实时的请求,就返回给你一个请求的网页。而离线算法则是要求一次性读入所有的请求,然后在统一得处理。而在处理的过程中不一定是按照请求的输入顺序来处理的。说不定后输入的请求在算法的执行过程中是被先处理的。

Tarjan算法和树上倍增算法

(再一次安利我的博客)

http://z6363636363.blog.163.com/blog/static/246696032201571083457357/#

这样看来,问题似乎已经完美解决,使用好理解的Tarjan算法就可以得到答案了,但是作为一个离线算法,Tarjan算法有一个劣势,就是会打乱输入数据的顺序,所以不好输出,做标记也许可以,但加大了思维负担,所以这里使用树上倍增算法,而树上倍增用到了位运算作为工具,所以。。。。位运算(最后一次安利。。。重要的东西要说三次)

http://z6363636363.blog.163.com/blog/static/246696032201571095240117/#

 

 

下面是我的C++代码:

#include<iostream>

#include <algorithm>//要使用C++内置的排序函数,必须要有这句

#include<cstdio>

#include<cstring>

 

#define inf0x7fffffff//定义一个认为的无限大的值,这是一个16进制数

 

using namespacestd;//上面的头文件什么的,可以无视

 

intn,m,q,cnt,tot,deep[10001],head[10001],f[10001],fa[10001][17],d[10001][17];

bool vis[10001];

struct edge{intx,y,v;}a[50001];//定义边的结构

struct e{intnext,to,v;}e[20001];//定义邻接表,关于邻接表,可以看ahalei的51CTO博客http://developer.51cto.com/art/201404/435072.htm

 

void ins(intu,int v,int w)

{e[++cnt].to=v;e[cnt].next=head[u];head[u]=cnt;e[cnt].v=w;}//向邻接表中加入一条边

void insert(intu,int v,int w)

{ins(u,v,w);ins(v,u,w);}//为了方便双向加入边,我这里写了一个驱动函数

int find(int x)

{returnf[x]==x?x:f[x]=find(f[x]);}//并查集的查,使用了三元表达式,翻译过来就是

/*

      if(f[x]==x)

             return x;

      else

             f[x]=find(f[x]);

             return f[x];

*/

bool cmp(edgea,edge b)

{returna.v>b.v;}//为了使用C++内置的一个排序函数,定义边的比较,

void dfs(intx)//dfs给树上的每一个节点做上深度的标记

{

    vis[x]=1;//标记当前节点已经走过

    for(int i=1;i<=16;i++)

    {

        if(deep[x]<(1<<i))break;//通过位运算判断是否已经到达树的最深处

        fa[x][i]=fa[fa[x][i-1]][i-1];//因为2^(j-1)+2^(j-1)=2^j,所以节点的2^(j-1)祖先的2^(j-1)祖先就是节点的2^j祖先

       d[x][i]=min(d[x][i-1],d[fa[x][i-1]][i-1]);//记录下到第i祖先的最小路径权值

    }

    for(int i=head[x];i;i=e[i].next)//遍历邻接表,这里使用了for循环的语法糖

    {

        if(vis[e[i].to])continue;//如果当前边的到达顶点已经走过,就继续下一个边

        fa[e[i].to][0]=x;//2^0=1,所以fa[i][0]是i节点的父亲,这里按照遍历边的顺序,把树建立(当前边的到达顶点的父亲就是出发顶点)

        d[e[i].to][0]=e[i].v;//到达顶点到父亲的权值就是边的权值

        deep[e[i].to]=deep[x]+1;//到达顶点的深度比父亲大1

        dfs(e[i].to);//递归遍历到达顶点

    }

}

int lca(intx,int y)//重点来了,这里是求树上两节点的最近公共祖先

{

    if(deep[x]<deep[y])swap(x,y);//先保证x节点的深度大于y节点

    int t=deep[x]-deep[y];//得到两节点深度的差值

    for(int i=0;i<=16;i++)

        if((1<<i)&t)x=fa[x][i];//通过位运算将x节点上推,直到x节点与y节点的深度相同(从0开始上推是为了处理x与y在同一深度的情况)

    for(int i=16;i>=0;i--)//从int位的最后一位开始倒推,找到深度最大的公共祖先即为最近公共祖先

    {

        if(fa[x][i]!=fa[y][i])

            {x=fa[x][i];y=fa[y][i];}//在x与y的祖先不同时,同步上推

    }

    if(x==y)return x;//处理一个边界情况,即如果在x上推到与y同等深度时,x与y相同,则公共祖先就是上推后的x

    return fa[x][0];//当同步上推完成时,x的父亲节点即为最近公共祖先(当然写y也行,不过我看x顺眼。。。)

}

int ask(intx,int f)//这里是询问x节点,到它的祖先节点的最小路径

{

    int mn=inf;

    int t=deep[x]-deep[f];//找到深度之差,确定上推范围

    for(int i=0;i<=16;i++)

    {

        if(t&(1<<i))//还是位运算

        {

            mn=min(mn,d[x][i]);//在上推中确定这条路线的最小权值

            x=fa[x][i];//上推

        }

    }

    return mn;//返回最小权值

}

int main(void)//这里是主程序的开始

{

   freopen("truck.in","r",stdin);

   freopen("truck.out,","w",stdout);//上面是文件

    memset(d,127/3,sizeof(d));//向记录路径权值的数组中填充一个大值

    scanf("%d%d",&n,&m);//读入,n,m

    for(int i=1;i<=n;i++)f[i]=i;//初始化并查集

    for(int i=1;i<=m;i++)

       scanf("%d%d%d",&a[i].x,&a[i].y,&a[i].v);//读入每一条边

    sort(a+1,a+1+m,cmp);//将每一条边按照规定好的比较方式排序,完成后将按照降序排序

    for(int i=1;i<=m;i++)

    {

        int x=a[i].x,y=a[i].y;

        int p=find(x),q=find(y);

        if(p!=q)

        {

            f[p]=q;

            insert(x,y,a[i].v);

            tot++;//记录使用的边的数量

         if(tot==n-1)break;//如果使用的边已经有n-1个,那么最大生成树已经构造成功,跳出循环

        }//这里是构造最大生成树。。。

    }

    for(int i=1;i<=n;i++)if(!vis[i]){dfs(i);}//以第一个节点为根节点,遍历并标记每个顶点的深度(树的根节点可以随意选择,并不影响)

    scanf("%d",&q);//读入询问次数

    for(int i=1;i<=q;i++)

    {

        int x,y;

       scanf("%d%d",&x,&y);//读入每一次询问

       if(find(x)!=find(y)){printf("-1\n");continue;}//如果询问的两个顶点不在同一个连通分量中,那么就无路连通,输出-1

        else

        {

            int t=lca(x,y);//找到询问的顶点的最近公共祖先

           printf("%d\n",min(ask(x,t),ask(y,t)));//从x节点到祖先节点,从y节点到祖先节点,找到最小的权值,即为答案

        }

    }

   

    return 0;//程序正确返回的标志

}

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
noip2013普及组初赛是全国信息学奥林匹克联赛的一场选拔赛。该比赛旨在选拔初学者,对编程和算法有一定基础的学生,通过比赛形式来考察他们的知识水平和解题能力。 比赛题目通常会涉及各个领域的算法和数据结构,如图论、动态规划、数论等。题目难度逐步增加,从简单的输出结果,到复杂的程序设计与代码实现,考察选手的逻辑思维和编程能力。 参赛选手需要通过自己的思考和编程实现来解决题目,同时时间也是一个重要因素。比赛中,选手需要在规定的时间内独立完成所有题目,对于复杂的题目需要迅速想出解题思路并进行编码。因此,在比赛中,选手的临场发挥和解题速度也是需要考虑的因素。 noip2013普及组初赛的结果将作为选拔阶段的一个重要依据,选出表现出色的选手进入到更高阶段的比赛,对于他们来说,这是一次展示自己实力的机会。 此外,noip2013普及组初赛,也给了参赛选手一个交流的平台。选手们可以通过比赛结交同好,相互切磋,共同进步。同时,比赛结束后,还有详细的解题分析和讲解,有助于参赛选手对自己在比赛中的不足进行反思与改进。 总之,noip2013普及组初赛是一个考察学生编程和算法能力的选拔赛,通过比赛的形式来选拔出优秀的选手。这对于参赛选手来说,是一次展示自己才华的机会,也是一个展示自己实力和提高自己能力的平台。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值