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

本文详细探讨了一道经典的赛马问题,如何在不知道每匹马速度的情况下,通过最少的比赛次数确定前M匹马。通过矩阵画图法,解释了N=8,M=4的情况可能需要10次或11次比赛的争议,并提出了一种通用的图论算法解法,涉及到有向无环图(DAG)的构建和遍历。最终得出结论,对于特定情况,比赛次数可能是10次或11次,取决于对“至少”的定义。文章还展示了使用Java实现的算法代码,验证了理论结果的一致性。

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

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);
    }

    /**
     * 检查输入是否合法
     */
    
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值