算法题——华为OD机试——学生交换顺序/小朋友分组最少调整次数 #Java #不服气

题目介绍

题目描述

n 个学生排成一排,学生编号分别是 1 到 n,n 为 3 的整倍数。

老师随机抽签决定将所有学生分成 m 个 3 人的小组(n == 3 * m) ,

为了便于同组学生交流,老师决定将小组成员安排到一起,也就是同组成员彼此相连,同组任意两个成员之间无其它组的成员。

因此老师决定调整队伍,老师每次可以调整任何一名学生到队伍的任意位置,计为调整了一次, 请计算最少调整多少次可以达到目标。

注意:对于小组之间没有顺序要求,同组学生之间没有顺序要求。

输入描述

第一行输入初始排队顺序序列

第二行输入分组排队顺序序列

输出描述

最少调整多少次数

用例
输入4 2 8 5 3 6 1 9 7
6 3 1 2 4 8 7 9 5
输出1
说明

分组分别为:6,3,1一组,2,4,8一组,7,9,5一组

初始排队顺序中,只要将5移动到1后面,变为:

4 2 8 3 6 1 5 9 7

即可满足分组排队顺序要求。

因此至少需要调整1次站位。

输入8 9 7 5 6 3 2 1 4
7 8 9 4 2 1 3 5 6
输出0
说明
输入7 9 8 5 6 4 2 1 3
7 8 9 4 2 1 3 5 6
输出1
说明

背景

这是上周末我做的200分值的题目,我当时在考场上没做出来。0/200分。

华为机试——150分钟2D1C的3道题考试,可以使用本地IDE。 

HR为我分享了题库,指明了一些学习方向。

这是原文链接,付费,不可转载。

华为OD机试 - 学生重新排队(Java & JS & Python & C & C++)_学生重新排队华为机考-CSDN博客

但是这一题,考试的cases可能可以通过,但是思路和代码是不正确的,原文评论也有说明。

因此这个系列,都是参考答案,不能当标准答案。

我的实力自我评价,是代码功底欠缺,但是数学思维很强,而且这题很有思路,所以考场上想要完美做出来的。但是本题数学证明还是需要一点群论的,我感受到数学知识的欠缺。

盘外失算,笔记本的氮化镓充电器老化充不进电/原装的可以用但没带回家,最后20分钟就没写了,是实力和准备的双重不完备。

今天重新做,已经有思路的情况下缝缝补补了100分钟。

没有机试环境的cases,自己写了几个cases可以通过。

思路

如果参考上文的博主,本题有两个难点:

  1. 如何快速判断两个小朋友是否为一组
  2. 如何调整站队,才能花费次数最少

——和我想的完全不一样——

第一步思考,工程化与理论化分开

首先理解题意,读题划重点

【将小组成员安排到一起】#排序 【对于小组之间没有顺序要求,同组学生之间没有顺序要求】#组内无序 这道题数学建模是大道至简型的,因此有很大简化操作空间。(不妨假设都有要求)【n 为 3 的整倍数】难度大幅度降低,不需要对零碎的情况进行处理。(不妨假设不是整数倍)

【调整任何一名学生到队伍的任意位置,计为调整了一次】涉及变动的数组是工程化问题,很复杂。

观察输入形式

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

在形式上暗含了第二行按照 index/3 分组

2行 >> 最终处理成数组 >> 两行建立映射关系 >> 发现可以第一行简化成第二行的i分组index

第二步尝试,自己写几个case看看答案,从形式上感受规律。

第三步切入

从单个小组开始讨论

x为当前组的3个数,y为x之间的若干的数字

是否需要关注移动对象/目的地/顺序未知

x y x y x 移动2次

x y x x 移动1or2次

x x y x 移动1or2次

x x x 移动0次

从最终处理来看(学棋先学残)

  • 假设【状态Q】——此时所有小组都分的差不多了——就最后一组还没处理,只要处理这3个数字就完成了——条条大路通罗马,这就是你只要再向前迈一步就是罗马的地方——无论如何都会到这一步的。
  • 无论如何处理,之前排好的3个1组,就算每组都没有和最终排序对齐,也都跟随最后3个数字的变化移动到对齐的位置上。之前被这3个数字隔开的其他组也都随着这3个数字的移动合并了。因此只要每组都3消了,最终顺序是不会受到影响的。
  • 对3个数字形成的每个连续部分的左右两侧分情况讨论,就会发现,此时一旦进行移动操作,左右两侧的数字要么是2个完整的3消,要么合并成3消,其他情况说明还没到【状态Q】,只会被其他组的数字影响,不会被最后一组数字影响。
  • 【在状态Q的基础上就可以拓展了】,1x11,3个数字里面有2个连续的,移动2个会不会在整体上比移动1个更划算,因为这是二维的,无论如何都是持平的——把3个数字看作一个整体。现在让他们都消失,此时其他组需要变动x次,现在放回去,整体最终的变动最多需要变动x+2次。我们已经把一个发散的情况缩小到收敛了,就可以使用计算机了。

从整体来看

通过最终处理感性思考——很轻松的提出猜想——两组之间不会相互影响

而且两组数字之间

通过case——很轻松的否定上述猜想——两组之间会相互影响

这是参考答案思路的答案

这是我的答案

我认为正确的思路——如何量化并且规避两组之间在多大程度上影响——【问题P】后进行的操作可以节省先进行的操作

最终工程化与理论化的汇合

如果数学上可以证明,那么很多算法都不用被发明!

理论部分结束。

我们依靠的就是自动化的计算——DFS找最优解——因为被节省的操作的情况一定被所有情况包含,筛选即可!

工程化实现

子任务划分

  • 对2行输入进行分组映射,便于处理
  • 根据每个分组的index来实现,所以有n!个路径
  • 也可以结果导向型DFS,我选择抽象出DFS,我的做法感觉是不对的,或者没有深入GET到DFS的精髓,没法拓展DFS的更细枝干。解耦合在这个尺度上的取舍,AI也不知道。
  • 分类讨论,严谨的分类讨论应该是3个组所有情况提取共性,但是我没有做,我直觉上感到,如果施加一个场以向左合并为默认,1x11向右合并,1x1x1的向左合并带来的【问题P】会被DFS移除误差。
  • 相同组的3个数的index来判断是否连续。
  • 维护正在进行DFS的List使用倒序remove,删除2个即可,留下1个代表经过操作,3个合体了,但不是消除,有本质区别。

JAVA代码实现

   /**
     * 抽象dfs
     * @param nums 要排序的原始数组
     * @param current 当前排序
     * @param visited 在当前branch是否使用过
     * @param depth 当前深度
     * @param permutations 生成的排序 的集合
     */
    private void generatePermutations(int[] nums, int[] current, boolean[] visited, int depth, List<int[]> permutations) {
        if (depth == nums.length) {
            permutations.add(current.clone());
            return;
        }

        for (int i = 0; i < nums.length; i++) {
            if (!visited[i]) {
                visited[i] = true;
                current[depth] = nums[i];
                generatePermutations(nums, current, visited, depth + 1, permutations);
                visited[i] = false;
            }
        }
    }

    /**
     *
     * @param n
     * @return 计算 n 的阶乘
     */
    private int factorial(int n) {
        if (n == 0) return 1;
        int fact = 1;
        for (int i = 1; i <= n; i++) {
            fact *= i;
        }
        return fact;
    }

    /**
     * main函数调用这个函数,方便管理,所有题目的类都叫这个,然后反射调用
     */
    public void toMain(){
        Scanner sc = new Scanner(System.in);
        //  流处理输入 第一行输入 经过字符串分割成String[] 集体从String映射Int straeam构建Array
        int[] nums = Arrays.stream(sc.nextLine().split(" ")).mapToInt(Integer::parseInt).toArray();
        int n = nums.length;
        int[] arrange = Arrays.stream(sc.nextLine().split(" ")).mapToInt(Integer::parseInt).toArray();
        //  第二行数字简化为Map 映射 第一行数字:组号
        HashMap<Integer,Integer> map = new HashMap<>();
        for (int i = 0; i < arrange.length; i++) {
            map.put(arrange[i],i/3);
        }
        //  第一行数字也被简化
        nums = Arrays.stream(nums).map(map::get).toArray();
        //  List方便dfs增减 Array不方便,只需要对进行操作转List就可以了
        //  Java8有更简单的写法
        ArrayList<Integer> array_nums= new ArrayList<>();
        Arrays.stream(nums).forEach(array_nums::add);
        //  全阶乘大小
        int totalPermutations = factorial(n/3);
        //  全方案 步骤数量
        int[] results = new int[totalPermutations];

        //  init dfs params 参考形参
        boolean[] visited = new boolean[n/3];
        List<int[]> permutations = new ArrayList<>();
        int[] indexs = new int[n/3];
        for (int i = 0; i < n/3; i++) {
            indexs[i] = i;
        }
        generatePermutations(indexs, new int[n/3], visited, 0, permutations);

        //  开始根据路径 计算步骤数量
        for (int m = 0;m < totalPermutations; m++) {
            ArrayList<Integer> array_nums_copy = (ArrayList<Integer>)array_nums.clone();
            results[m] = 0;
            for (int i=0; i< permutations.get(m).length;i++) {
                int[] temp_3_counts = new int[3];
                byte temp_index = 0;
                for (int j=0 ;j< array_nums_copy.size();j++) {
                    if(array_nums_copy.get(j)==permutations.get(m)[i]){
                        temp_3_counts[temp_index++]=j;
                        if (temp_index>2){
                            break;
                        }
                    }
                }
                //  每组本质上分3种情况 2 1 0 次操作形成1个3消
                //  每组相互之间的影响在dfs维度上抵消
                /** 具体是如何排布的不需要关心,除非题目要求不同组的排列对结果的权重不同 */
                byte steps = 2;
                //  12相邻
                if (temp_3_counts[0]==temp_3_counts[1]-1){
                    steps--;
                }
                // 23相邻
                if (temp_3_counts[1]==(temp_3_counts[2]-1)){
                    steps--;
                }
                // 23相邻且12不相邻 1x11
                if (temp_3_counts[1]==(temp_3_counts[2]-1)||
            temp_3_counts[0]!=temp_3_counts[1]-1){
                    array_nums_copy.remove(temp_3_counts[1]);
                    array_nums_copy.remove(temp_3_counts[0]);
                //  1x1x1 11x1 111
                //  1x1x1 为什么也是留下左边的呢,因为其他dfs都是留下左边的
                }else{
                    array_nums_copy.remove(temp_3_counts[2]);
                    array_nums_copy.remove(temp_3_counts[1]);
                }
                results[m] += steps;
            }
        }
        // 计算最小移动次数
        int minMoves = Integer.MAX_VALUE;
        for (int i : results) {
            minMoves = Math.min(minMoves, i);
        }
        System.out.println(minMoves);
    }

CASE

到了测试这一步我才发现这类题目的答案,我们人类的直观感受,怎么知道超长的case最优解是多少,除非有更高层的理论指导——得到公式,否则只能也是写出来的代码答案相同。

因此,参考答案的完全通过率,就是因为生成答案的代码和他一样也是错误的,最多持平。

我的答案也需要经得起验证。欢迎攻击我的代码。

我比较没有把握的是在处理1x1x1的时候很直觉,全部合并左边。

总结

1.华为机试并不是要一个正确的答案,只是要一个能跑对很多case的答案,和这个世界上大多数IT工作一样,实用主义而非完美主义。

2.需要交换群的知识,但是我感觉,我能接触到的群论知识,在这个时代应用不广、理论高度不足,比如我回答不了该在思考的哪一步把重心转移到深度搜索,直觉告诉我未来5年内会有更大一统的理论。所以在数学领域我就不展开了——我也不知道严格证明的流程是什么。

3.如果华为的工程师入职都能把这种题做出正确答案来(正确答案用来机试确实杀鸡宰牛刀),绝对是超一流IT公司。这道题拿不到分没有部门面试的。

4.还有画图,欢迎留言让我画什么示意图(证明部分的),我暂时想不到要画什么图来说明。

  • 19
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值