题目介绍
题目描述
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可以通过。
思路
如果参考上文的博主,本题有两个难点:
- 如何快速判断两个小朋友是否为一组
- 如何调整站队,才能花费次数最少
——和我想的完全不一样——
第一步思考,工程化与理论化分开
首先理解题意,读题划重点
【将小组成员安排到一起】#排序 【对于小组之间没有顺序要求,同组学生之间没有顺序要求】#组内无序 这道题数学建模是大道至简型的,因此有很大简化操作空间。(不妨假设都有要求)【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.还有画图,欢迎留言让我画什么示意图(证明部分的),我暂时想不到要画什么图来说明。