相信不少朋友都听说过这道经典面试题

NxN匹马,每匹马速度恒定且均不同,有N个赛道,每次比赛一次就可以知道这N个赛道的每匹马,那匹快、那匹慢,请问我要求最快的前M匹马,至少需要进行几次比赛?
(不允许记录每匹马的速度,只能通过多次比较来确认)

具体来说,N,M有以下几种常见的情况

N=4,M=4,即16匹马,4个赛道,求前4名,最少进行几次比赛?
N=5,M=5,即25匹马,5个赛道,求前5名,最少进行几次比赛?
N=8,M=4,即64匹马,8个赛道,求前4名,最少进行几次比赛?
N=9,M=4,即81匹马,9个赛道,求前4名,最少进行几次比赛?

网上关于这个经典面试题的博文不少,基本都是采用一种“矩阵画图法”的方式来推导的,但更有意思的是,对于特定N=8,M=4这个最常见的场景,有的人说是需要10次,有的人说是需要11次,甚至有的人说是只需要9次,那么到底它的答案是多少呢?这样的问题有没有一种通用的解法呢?下面我来写一下我的理解,如有错误,欢迎大家评论纠正

N=8,M=4,即64匹马,8个赛道,求前4名,最少进行几次比赛?

拿这个问题来说,我同样用“矩阵画图法”来推导

1)下面是一个矩阵,起初它们都是白色状态,表示没有进行任何比较,次数为0总共次数为0

2)每一匹马必须至少参与一次比较,否则我是没有办法知道它是前M名,还是M名外,那么我不防每一组进行一次比较,A1-A8一次,B1-B8一次,…,最后H1-H8一次,次数为8总共次数为8

用黄色来表示:已经参与过比较,但是不确定它是第几名

这里我们不妨假设,每一组中1号马最快,8号马最慢,即A1 < A2 < A3 < … < A8,…,H1 < H2< H3 < … < H8

注意,我现在已经能确定每一组中每匹马的速度关系了,但不同组之间的速度关系依然无法确定,因此也无法确定谁是第一名

3)接下来,很显然,我将A1,B1,…,H1进行一次比较,是非常正确的一个做法(否则,请举一个更好的例子),这样子就一定可以确定第一名是谁了,次数为1总共次数为9

用红色来表示:已经参与过比较,且确定它是第几名

这里我们不妨再假设,A1 < B1 < C1 < … < H1

此时我们已经能够确定,每一组中每匹马的速度关系,并且不同组之间最快的马的速度关系,已经可以确定第一名是A1,但还无法确定第二、三、四名是谁

稍等等,仔细观察其实可以发现,有一些马已经可以排除在答案外了,比如A5,因为比它快的马有4匹(A1、A2、A3、A4),它最快也才是第5名,我们要求的是前4名

用蓝色来表示:已经参与过比较,且确定它在答案之外

那么这样一来,突然一下就排除很多马了!并且确定了A1是最快的马,真正需要比较的只有A2、A3、A4、B1、B2、B3、C1、C2、D1这9匹马

9匹马,8个赛道,求出第二、三、四名,需要进行多少次?有的人说2次,有的人说只需要1次,这就是总和答案是11次,还是10次的主要争论原因

不管怎么说,最多为2次,这个肯定没有问题,很明显,你只需要从9匹马中选出8匹马进行一次比较,然后再把剩下的那一匹马参与比较即可,这就是说2次的解法

那么说1次是什么情况呢?说1次其实是用特例来说的,比如我将D1作为那匹不参与第一次比较的马,其余8匹进行比较,并且得到结果是C1是第8名,且已知C1比D1快,那么还有比较D1吗?显然不需要了

不管剩下的9匹马,你选择那一匹作为不参与第一次比较的马,你总能找到一些特例,使得最后一次比较没有必要,但是!同样的,你也一定能找到一些特例,使得最后一次比较必须执行!(我这里就不举例子了,喜欢钻研的朋友可以自己研究研究)

所以,对于网上争论,到底是10次还是11次,其实本质上是对问题中 “至少需要几次比赛” 的 “至少” 这二字定义的争论。如果你认为至少含义是“最少”,那么是10次,但如果你认为至少含义是“所有情况下最少的最大”,那么就是11次

那么根据我个人的理解,至少含义是“最少”是说不通的,最终答案是11次

好,那么这个问题有没有通解呢?有没有一种解法,可以对任何N、M都可求出一个结果呢?

熟悉图论算法朋友,一定能很快看出上面“矩阵画图法”其实与图论息息相关,图论中是有顶点与边的,每个矩阵元素就是一个顶点,顶点与顶点之间存在一个单向边的关系,from => to 表示 from点的值大于to点的值

依然以下面这幅图来说

图中存在如下的关系边

  • A1 <= A2 <= A3 <= A4
  • A1 <= B1 <= B2 <= B3
  • A1 <= B1 <= C1 <= C2
  • A1 <= B1 <= C1 <= D1

此时我们已经确定了A1是第一名,且可以发现第二名一定在A2与B1中,那么要求剩下的第二、三、四名,不妨就拿D1作为不参与第一次比较的马,其余的8匹马进行比较,来看一下图中的边关系会如何变化

(其实从上面这幅图中也可以看出来,不参与第一次比较的马,还可以选择A4、B3、C2)

我们拿A2、A3、A4、B1、B2、B3、C1、C2进行比较,假设比较结果是:A2 < A3 < A4 < B1 < B2 < B3 < C1 < C2,那么我们就可以画出现在的边关系,如下

  • A1 <= A2 <= A3 <= A4 <= B1 <= B2 <= B3 <= C1 <= C2
  • C1 <= D1

此时,我们是可以确认D1不需要参与第二次比较的了,因为它一定在第4名之外

但是,如果我比较的结果是:A2 < C1 < A3 < A4 < B1 < B2 < B3 < C2 呢?那么边关系就要变为如下

  • A1 <= A2 <= C1 <= A3 <= A4 <= B1 <= B2 <= B3 <= C2
  • C1 <= D1

此时D1就必须要参与第二次比较了,否则我没法确认到底是D1还是A3是第四名

至此,我们可以推导出通解算法如下

1、将N组赛马进行组内比较,次数为N,总共次数为N
2、将每组的第一名赛马进行比较,次数为1,总共次数为N+1
3、通过1、2建图,得到一个DAG(有向无环图)
4、在图中可以找到第一名的点,即出度为0的顶点(不妨设为A1点)
5、在图中可以找到前K名的点,是唯一到达A1点需要1,2,…,K-1步的点
(唯一很重要,然后到达A1点需要1步是第二名,需要2步是第三名,…)
6、如果K>=M,则寻找结束,否则执行下一步
7、排除掉A1及前K名的点,剩下的点中以到达A1点步数排序,最少最优先
8、排序后,在剩下的点中选择前N名,进行一次比赛,次数+1,并且修改边的指向
9、重新执行5

上面是一个算法思路,实际编码中有一些技巧,比如第5步寻找前K名节点如何寻找?第7步如何对剩下的点进行排序?第8步如何修改边的指向?这都是值得思考的问题,具体可以见我的实现代码,如下

我采用了偏暴力的实现方法,且多次随机生成数据,求所有结果最小值的最大值

package raceproblem;import java.util.*;public class RaceProblemSolution {/*** main方法*/public static void main(String[] args) {RaceProblemSolution solution = new RaceProblemSolution();solution.randomTest(1, 1, 1);solution.randomTest(4, 2, 2);solution.randomTest(9, 3, 3);solution.randomTest(16, 4, 4);solution.randomTest(25, 5, 5);solution.randomTest(64, 8, 4);solution.randomTest(81, 9, 4);}/*** 检查输入是否合法*/void check(int nHorse, int nRace, int nWin) {if (nHorse <= 0 || nRace <= 0 || nWin <= 0)throw new RuntimeException("nHorse <= 0 || nRace <= 0 || nWin <= 0");if (nHorse != nRace * nRace)throw new RuntimeException("nHorse != nRace * nRace");if (nHorse < nWin)throw new RuntimeException("nHorse < nWin");}/*** 生成足够数量次数 arr随机数组,传入solve方法求赛跑次数*/void randomTest(int nHorse, int nRace, int nWin) {check(nHorse, nRace, nWin);System.out.printf("nHorse: %d, nRace: %d, nWin: %d, ", nHorse, nRace, nWin);int[] arr = new int[nHorse];for (int i = 0; i < nHorse; i ++) {arr[i] = i;}Random random = new Random(new Random().nextInt(100));int min = Integer.MAX_VALUE;int max = Integer.MIN_VALUE;for (int i = 0; i < 10000; i ++) {for (int j = 0; j < nHorse * 5; j ++) {int t0 = random.nextInt(nHorse);int t1 = random.nextInt(nHorse);int t = arr[t0];arr[t0] = arr[t1];arr[t1] = t;}int res = randomTest(arr, nHorse, nRace, nWin);min = Integer.min(min, res);max = Integer.max(max, res);}System.out.printf("min: %d, max: %d\n", min, max);}/*** 求赛跑次数的核心方法*/int randomTest(int[] arr, int nHorse, int nRace, int nWin) {// 先将 arr数组 转成 nodes数组int res = 0;Node[] nodes = new Node[nHorse];for (int i = 0; i < nHorse; i ++) {nodes[i] = new Node(null, arr[i]);}// 对每 nRace 匹马进行赛跑,分成 nRace 组for (int i = 0; i < nHorse; i += nRace) {Arrays.sort(nodes, i, i + nRace, Comparator.comparingInt(node -> node.speed));for (int j = i + 1; j < i + nRace; j ++) {nodes[j].prev = nodes[j-1];}}res += nRace;// 将 nRace 组中,每组第一名的马进行赛跑,然后计算每匹马的 rankif (nHorse > 1) {Node[] nts = new Node[nRace];for (int i = 0, t = 0; i < nHorse; i += nRace) {nts[t++] = nodes[i];}Arrays.sort(nts, Comparator.comparingInt(node -> node.speed));for (int i = 1; i < nRace; i ++) {nts[i].prev = nts[i-1];}buildRank(nodes);Arrays.sort(nodes, Comparator.comparingInt(node -> node.rank));res += 1;}// 循环,直到找到前 nWin 匹马为止while (true) {// 判断是否已经找到了前 nWin 匹马boolean ok = true;int t = 0;for (int i = 0; i < nWin; i ++) {if (nodes[i].rank != i) {ok = false;t = i - 1; // 注意是 i-1,改为 i 会 final check errorbreak;}}if (ok && nWin < nodes.length && nodes[nWin].rank != nWin) {ok = false;t = nWin - 1; // 注意是 nWin-1,改为 nWin 会 final check error}if (ok)break;// 对 nodes[t, t + nRace) 中的马进行一次赛跑Arrays.sort(nodes, t, t + nRace, Comparator.comparingInt(node -> node.speed));for (int i = t; i < t + nRace; i ++) {nodes[i].prev = nodes[i-1];}// 重新计算每匹马的 rankbuildRank(nodes);Arrays.sort(nodes, Comparator.comparingInt(node -> node.rank));res += 1;}// 最终进行一次数据校验,保证结果正确for (int i = 0; i < nWin; i ++) {if (nodes[i].speed != i) {print(nodes);throw new RuntimeException("final check error");}}return res;}/*** 计算每匹马的 rank*/void buildRank(Node[] nodes) {for (Node node : nodes) {node.succ.clear();}Node root = null;for (Node node : nodes) {Node prev = node.prev;if (prev != null)prev.succ.add(node);elseroot = node;}root.rank = 0;Queue<Node> q = new LinkedList<>();q.add(root);while (!q.isEmpty()) {Node x = q.poll();for (Node suc : x.succ) {suc.rank = x.rank + 1;q.add(suc);}}}void print(Node[] nodes) {for (Node node : nodes) {Node prev = node.prev;System.out.printf("(%s, %d, %d), ", prev == null ? "n" : String.valueOf(prev.speed), node.speed, node.rank);}System.out.println();}/*** 每匹马的类,也是图论中的顶点类*/static class Node {Node prev; // 前驱节点,最多只有一个final List<Node> succ; // 所有后继节点final int speed; // 马的速度,固定int rank; // 马的排名,会变化Node(Node prev, int value) {this.prev = prev;this.succ = new ArrayList<>();this.speed = value;this.rank = 0;}};
}

输出结果

nHorse: 1, nRace: 1, nWin: 1, min: 1, max: 1
nHorse: 4, nRace: 2, nWin: 2, min: 4, max: 4
nHorse: 9, nRace: 3, nWin: 3, min: 5, max: 6
nHorse: 16, nRace: 4, nWin: 4, min: 7, max: 8
nHorse: 25, nRace: 5, nWin: 5, min: 8, max: 9
nHorse: 64, nRace: 8, nWin: 4, min: 10, max: 11
nHorse: 81, nRace: 9, nWin: 4, min: 11, max: 11

从输出结果来看,N=8,M=4的情况,最少是进行10次,最多是进行11次,因此在“至少”的定义是“所有情况下最少的最大”前提下,答案是11次

小结:本文以N=8,M=4的情况阐述了赛马经典面试题中如何通过“矩阵画图法”求解,讨论了问题中“至少”的定义,并且阐述了如何使用图论的方法求通解,验证了通解的结果与理论结果一致

图论法求解经典面试题:NxN匹马,N个赛道,求最快前M匹马,至少需要几次比赛?相关推荐

  1. 25马5跑道,求最快的五匹马的需要比赛的次数

    25匹马分5组,每组比一次. 然后5个组的冠军再比一次. 共进行了6次比赛,结果如下: 可知此时已经排好序,A1是25匹马中跑的最快的,同时,对角线下面的图片不可能进入前五名因为显然,A1>B1 ...

  2. 64匹马8条跑道找最快的4匹马

    假设跑道一样,马体力无限,速度均衡.有64匹马只有8条跑道,找最快的4匹马,至少要跑多少次? 答案:10-11次. 这类题,都是根据已知条件用尽量少的成本推导出尽量多的已知条件来进行最尽筛选 1.分8 ...

  3. 反转单链表(三种方法)(三指针法)(头插法)(递归)经典面试题

    剑指 Offer 24. 反转链表 定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点. 示例: 输入: 1->2->3->4->5->NULL 输出 ...

  4. 64匹马8个跑道选出最快的4匹马,最快需要几次比赛

    既然问这个问题,肯定是不计时赛马. 1.64匹马分8组,每组竞赛,这样每组内的马有了排序. (+8) 2.选每组的第一名出来竞赛,前四名的组去掉后四匹马,后四名的组全去掉,于是剩下前四名的组,每组4匹 ...

  5. 25 匹马 5 条赛道,最快需要几轮求出前 3 名?

    请点赞关注,你的支持对我意义重大.

  6. 游戏必备组件有哪些_面试必备:2019Vue经典面试题总结(含答案)

    点击右上方红色按钮关注"web秀",让你真正秀起来 面试必备:2019Vue经典面试题总结(含答案) 一.什么是MVVM? MVVM是Model-View-ViewModel的缩写 ...

  7. 腾讯面试题:64匹马,8赛道,找出最快的4匹最少要几次?

    本文转载自 小K算法 01 故事起源 有64匹马,8条赛道,要找出最快的4匹马,最少要几次呢? 补充: 1.不能计时哈,不然就没有意义了,题目就是要考察逻辑推理 2.默认马的速度不变哈,这是理想的数学 ...

  8. 64匹马,8个赛道,找出跑得最快的4匹马(面试题详解)

    首先,可以将马分为8组,每组各跑一次,然后淘汰掉后四名,这里淘汰后四名是因为只需要跑的最快的四匹马. 然后取8次跑的第一名进行比赛,然后淘汰掉后四名所在的组的所有马,因为,后四名所在的组的第一名没有跑 ...

  9. 64匹马,8赛道,找出跑得最快的4匹马,至少比赛9场

    遇到这种问题, 首先先不要尝试思考具体的方式, 先用算法找上下限, 接下来不断通过验证和分析去缩短已经确定的上下限(因为你的上下限计算方式可能不对). 这里先给一个简单的题: 4个矿泉水瓶可以换一瓶矿 ...

  10. 智力题:36匹马,6条跑道,没有计时器,至少需要多少次选出最快的三匹马

    智力题:36匹马,6条跑道,没有计时器,至少需要多少次选出最快的三匹马 1.将马分成六组进行比赛,比赛六次,六组马分别都是有序的. 2.分别将六组马中跑得最快的马挑出来,让这六匹马再进行第七次比赛,将 ...

最新文章

  1. Python,C++中点云 .las转.pcd
  2. poj 1964 Cow Cycling(dp)
  3. 让win7系统高速运行的优化技巧
  4. Nginx流量拦截算法
  5. [转载]WPF窗口跳转及window和page区别
  6. oracle19c安装[ins-35180]无法检查可用内存
  7. java使用jeids实现redis2.6的list操作(4)
  8. 基于Java+SpringBoot+vue+element实现新冠疫情物资管理系统详细设计
  9. 电子计算机工程学,电子计算机工程学荣誉工学士资料.ppt
  10. Android:QQ登录页面
  11. python精度_通过Python可以达到的最高时间精度范围是多少?
  12. iPhone4S使用红雪最新iOS5平刷和降级教程
  13. 【源码分享】短信平台插件74cms_v4.1_骑士人才系统
  14. AndroMDA中的用例图和活动图介绍(MagicDraw)
  15. AutoCAD将DWG图纸转为PNG图片
  16. 糖尿病视网膜病变研究的基准:分割、分级和可转移性笔记
  17. 数学建模学习笔记——预测类型1
  18. 以太网没有有效的ip怎么解决
  19. Excel无法打开文件新建 XLSX 工作表.xlsx,因为文件格式或文件扩展名无效。请确定文件未损坏解决办法【笔记】
  20. c语言实现动态二维数组

热门文章

  1. linux下制作dos启动u盘启动,在Linux系统下创建FreeDOS可启动U盘
  2. 路由器设置显示服务器拒绝访问,路由器设置服务器拒绝访问
  3. mplay readme
  4. 深圳 计算机网络与管理,深圳计算机网络管理员路由与交换班
  5. html边框双箭头,纯CSS如何绘制双箭头
  6. 福昕阅读器注册无法连接服务器,福昕pdf阅读器 10安装使用教程(附注册机)
  7. WORD-如何解除WORD文档的锁定
  8. 因子分析法(Factor Analysis)是什么分析
  9. 小众即时通信工具专项整治启动,关停“比邻”“聊聊”“密语”等9款违法App...
  10. tan和cot的梗_cot和tan的关系