蓝桥杯:城邦

题目链接

问题描述

答案提交

        本题答案是:4046。

运行限制

思路分析

代码(Java)


问题描述

        小蓝国是一个水上王国, 有 2021 个城邦, 依次编号 1 到 2021。在任意两 个城邦之间, 都有一座桥直接连接。

        为了庆祝小蓝国的传统节日, 小蓝国政府准备将一部分桥装饰起来。

        对于编号为 a 和 b 的两个城邦, 它们之间的桥如果要装饰起来, 需要的费 用如下计算:

        找到 a 和 b 在十进制下所有不同的数位, 将数位上的数字求和。

        例如, 编号为 2021 和 922 两个城邦之间, 千位、百位和个位都不同, 将这些数位上的数字加起来是 (2+0+1)+(0+9+2)=14 。注意 922 没有千位, 千位看成 0 。

        为了节约开支, 小蓝国政府准备只装饰 2020 座桥, 并且要保证从任意一个 城邦到任意另一个城邦之间可以完全只通过装饰的桥到达。

        请问, 小蓝国政府至少要花多少费用才能完成装饰。

        提示: 建议使用计算机编程解决问题。

答案提交

        这是一道结果填空的题,你只需要算出结果后提交即可。本题的结果为一 个整数, 在提交答案时只填写这个整数, 填写多余的内容将无法得分。

        本题答案是:4046。

运行限制

  • 最大运行时间:1s
  • 最大运行内存: 256M

思路分析

        首先,他的花费计算是,两个数A,B,如果A和B相对应的位数不同,就相加。

        如:2017和917这两个数的花费是:(2+0)+(0+9),因为个位他们都是7,所以省略了,十位也是同理,然后917没有千位,默认是0。所以2017和917的花费就是(2+0)+(0+9)=11

        然后再看题目,很明显跟图有关,一共有2021个节点,题目要求我们让所有节点都能连通起来,让我们使用2020条边,同时要求花费最少。

        判断是否连通需要用到并查集,然后在确保花费最少可以用贪心的策略,每次选择花费最少的一条边,让两个节点相连。

        条件这么清楚了,很明显的要用到最小生成树了,这道题就是经典板子题。

        如果不了解什么是最小生成树,可以搭配视频理解,我下面简单说一下。

        视频链接:最小生成树(Kruskal(克鲁斯卡尔)和Prim(普里姆))算法动画演示

         首先,需要是一个带权的图,在本题中,两个城堡之间如果需要连接起来,那么需要花费一定费用,将两座城堡之间建立连接。那么,换个思路来,两个城堡是图中的节点,然后他们之间互相连接的边,就是这两座城邦建立连接所需要的花费;而且,我们需要任意一个城堡都能到达另一个城堡,那么,就代表着这些节点之间需要互相连通,这样他们才能互相到达另一边。

        因此可以把题目的城堡当作连通带权无向图来处理。

        题目条件也给出了,一共有2021个城堡,也就是一共有2021个节点。我们需要用2020座桥把他们连接起来,也就是一共有2020条边。并且我们要让花费最小。

        所谓的最小成本了:就是n个顶点,用n-1条边把一个连通图连接起来,并使得权值的和最小。所以,我们把构成连通图的最小代价生成树叫做最小生成树。

        而最小生成树,有两个经典的算法,Kruskal和Prim,这里我们使用Kruskal比较容易理解。

        我们可以将节点x与节点y的花费存起来,用一个类(结构体)来存储节点x,节点y,节点x与y的连通花费。

class Node{
        // x <-> y
        int x;  //节点x
        int y;  //节点y
        int cost;   //连接x和y的花费
        public Node(int x, int y, int cost) {
            this.x = x;
            this.y = y;
            this.cost = cost;
        }
    }

        然后将其存入一个数组之中,之后遍历1-2021,算出所有节点互相之间连通需要的花费,并存入数组之中,后续按照连通花费,进行升序排序,这样保证前面的连通花费是最少的,我们根据Kruskal的思想,从最少连通花费进行连接,把x和y连接起来。

        将1-2021之间所有节点与边的关系添加到数组之后,我们就需要对数组进行排序了,要完成Kruskal算法之中花费从小到大排序,也就是升序,我们就需要对数组进行排序。

        但是我们开的数组肯定是远大于我们实际需要使用到的大小,所以我们需要用一个变量记录使用到哪个位置,然后排序的时候选择从0-变量记录的位置,然后使用升序排序即可(一般默认排序都是升序排序)。之后就是判断连通的问题了。

        既然要判断是否连通,那么就要用到并查集来检查连通了。

        如果对并查集不太懂,可以看:【算法与数据结构】—— 并查集 (写的非常好)。

        这里贴上代码,不展开介绍了。

    int[] pre;
    public void initPre(){
        //并查集找连通
        pre = new int[2022];
        //初始化并查集
        for(int i = 0;i<pre.length;i++){
            pre[i] = i;
        }
    }
    public int find(int x){
        //做一下路径压缩
        if(pre[x] == x) return x;
        return pre[x] = find(pre[x]);
    }
    public void join(int x,int y){
        int fx = find(x);
        int fy = find(y);
        if(fx!=fy)
            pre[fx] = fy;
    }

        如果将x和y连通,将产生环,该如何判断呢?

        我们需要通过并查集找到节点x它的父亲是谁,和y的父亲进行比较,如果他们的父亲相同,代码x已经可以和y同时到达同一个地点,如果我们再让x和y进行连接,那么肯定会产生环。

        如:

         如果我们的x是1,y是3,我们通过并查集的find()可以知道他们的父亲都是4,如果再让他们连通,则肯定会产生环。

        所以我们需要判断x和y的父亲是不是同一个,如果不是,那么就可以使用并查集的join()将他们连接起来,并且将连接他们的花费记录到答案之中,因为这是构造连通的最优解。

        之后根据定义,我们只需要成功连通2020个边(n个节点需要n-1个边),就完成了1-2021之间的连通,他们之间就可以从任意一个节点到另一个节点,并且花费是最小的了。

代码(Java)

        不知道为什么在蓝桥那边提交显示超时,我在本地跑就只用了380±20ms。

import java.util.*;
// 1:无需package
// 2: 类名必须Main, 不可修改

public class Main {
    static int[] pre;
    public static int find(int x){
        //做一下路径压缩
        if(pre[x] == x) return x;
        return pre[x] = find(pre[x]);
    }
    public static void join(int x,int y){
        int fx = find(x);
        int fy = find(y);
        if(fx!=fy)
            pre[fx] = fy;
    }

    static class Node{
        // x <-> y
        int x;  //节点x
        int y;  //节点y
        int cost;   //连接x和y的花费
        public Node(int x, int y, int cost) {
            this.x = x;
            this.y = y;
            this.cost = cost;
        }
    }
    public static void main(String[] args) {
        //程序开始时间
        long startTime = System.currentTimeMillis();

        //并查集找连通
        pre = new int[2022];
        //初始化并查集
        for(int i = 0;i<pre.length;i++){
            pre[i] = i;
        }
        //空间换时间
        Node[] minTree = new Node[2021*2021];
        int index = 0;
        //将边的信息存储到数组中
        for(int i = 1;i<2021;i++){
            for(int j = i+1;j<=2021;j++){
                int cost = cost(i,j);
                minTree[index++] = new Node(i,j,cost);
            }
        }
        //按照花费进行升序排序,排序范围是0-index,也就是存储有边信息的部分
        Arrays.sort(minTree,0,index,(n1,n2)->(n1.cost-n2.cost));
        //总花费
        int res = 0;
        //依次取出顶部(前面)的边,判断该x和y添加进去是否会产生环,如果不会就使用这条边;如果会就丢弃这条边
        for(int i = 0;i<index;i++){
            Node node = minTree[i];
            if(find(node.x)!=find(node.y)){
                join(node.x,node.y);
                res += node.cost;
            }
        }

        System.out.println(res);

        //程序结束时间
        long endTime = System.currentTimeMillis();
        System.out.println("程序执行耗时: "+(endTime-startTime)+" ms");
    }

    public static int cost(int x,int y){
        int res = 0;
        while(x>0 || y>0){
            int a = x%10;
            int b = y%10;
            if(a!=b) res += a+b;
            x /= 10;
            y /= 10;
        }
        return res;
    }
}

        程序运行结果:

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值