半岛·体育中国官方网 经常用美团app买电影票,不禁对它的推荐选座功能产生了好奇,于是打算自己实现一个类似的算法,美团app的推荐选座界面如下

介绍

我经常用美团app买电影票,不禁好奇它的推荐选座功能,所以我打算自己实现一个类似的算法,美团app的推荐选座界面如下

如何检测座位上是否有人_座位数怎么算_算法判断座位是否有人

您最多可以选择 5 个席位,此演示的席位选择屏幕如下所示

算法判断座位是否有人_如何检测座位上是否有人_座位数怎么算

上图是点击5人推荐选座后选择的座位(绿色),这个演示和美团app的区别在于可以持续推荐选座,而美团app点击推荐选座,必须买票才能继续选座。

此 demo 使用 Vue-cli 构建,点击这里 github 地址,克隆后可以直接通过 npm start 进行具体操作

算法思维过程

对于这个推荐的座位算法半岛·BOB官方网站,我尝试了不同的电影放映来推荐座位选择,总结了以下几点

(1) 推荐算法首先从 theater 中间行最后一行的中间开始搜索

如下图所示

算法判断座位是否有人_如何检测座位上是否有人_座位数怎么算

可以确定这个逻辑是一样的,在尝试了其他几个会话之后也是一样的

(2) 先向后排方向搜索,后排搜索完成后再从中间起始位置搜索前排如下图所示,

这在大多数情况下都是正确的,偶尔会有所不同

如何检测座位上是否有人_座位数怎么算_算法判断座位是否有人

(3) 后排搜索完成后,每行都会有一个结果(每行的结果是离中轴线最近的一组座位),这些结果中离中轴线距离最小的结果会作为最终结果,而不是离屏幕越近的

大多数时候也是这样,有些事情不对劲,很奇怪

(4) 只考虑并排和相邻的座位,不要在一排或一排中间分开,例如过道

这个可以理解,毕竟坐一排绝对比观影体验好很多

电影院座位数据结构

可以肯定的是,使用了二维数组 seatArray 来表示剧场座位,并且需要注意剧场座位的分布是不规则的,因此需要确定 seatRow 和 seatCol 来确定剧场座位的数组大小,分别代表行数和列数,对于那些没有座位的地方半岛·BOB官方网站, seatArray 对应的位置填入 -1,下面是 seat 的具体值和代表的含义

-1 非座位
0  可选座位   (白色)
1  已选座位   (绿色)
2  已购票座位 (红色)

然后初始化 seats in mounted,初始值全部为 0(可选 seats),如下代码所示

    //初始座位数组
      initSeatArray: function(){
        let seatArray = Array(this.seatRow).fill(0).map(()=>Array(this.seatCol).fill(0));
        this.seatArray = seatArray;
        //均分父容器宽度作为座位的宽度
        this.seatSize = this.$refs.innerSeatWrapper
                        ? parseInt(parseInt(window.getComputedStyle(this.$refs.innerSeatWrapper).width,10) / this.seatCol,10)
                        :0;
        //初始化不是座位的地方
        this.initNonSeatPlace();
      },
      
      //初始化不是座位的地方
      initNonSeatPlace: function(){
      	for(let i=0;i<9;i++){
          this.seatArray[i][0]=-1;
        }
        for(let i=0;i<8;i++){
          this.seatArray[i][this.seatArray[0].length-1]=-1;
          this.seatArray[i][this.seatArray[0].length-2]=-1;
        }
        for(let i=0;i<9;i++){
          this.seatArray[i][this.seatArray[0].length-3]=-1;
        }
        for(let i=0;i

初始化后,使用一个 double 循环构建 HTML 结构,两个 v-for 嵌套循环整个结构,如下面的代码所示

 <div class="inner-seat-wrapper" ref="innerSeatWrapper" >
      <div v-for="row in seatRow">
        
        <div v-for="col in seatCol"
             v-if="seatArray.length>0"
             class="seat"
             :style="{width:seatSize+'px',height:seatSize+'px'}">
          <div class="inner-seat"
               @click="handleChooseSeat(row-1,col-1)"
               v-if="seatArray[row-1][col-1]!==-1"
               :class="seatArray[row-1][col-1]===2?'bought-seat':(seatArray[row-1][col-1]===1?'selected-seat':'unselected-seat')">
          div>
        div>
      div>
div>

上面的 inner-seat class 的 div 是具体的 seat div,v-if 表示如果是 -1,也就是 aisle 之类的,就不会被渲染了,那么:class a sentence 控制了 seat 状态对应的 class 的值,嵌套的三目运算符控制, 对于每个座位绑定,单击事件 handleChooseSeat(row-1, col-1) 以切换状态

  //处理座位选择逻辑
  handleChooseSeat: function(row,col){
  	let seatValue = this.seatArray[row][col];
  	let newArray = this.seatArray;
      	//如果是已购座位,直接返回
        if(seatValue===2) return
        //如果是已选座位点击后变未选
        if(seatValue === 1){
          newArray[row][col]=0
        }else if(seatValue === 0){
          newArray[row][col]=1
        }
        //必须整体更新二维数组,Vue无法检测到数组某一项更新,必须slice复制一个数组才行
        this.seatArray = newArray.slice();
  },

这里需要注意的是,要改变 data 中的二维数组,必须先缓存二维数组,修改后,会重新分配二维数组,否则修改不会生效,因为 vue 无法检测到数组的变化。

推荐座位的具体代码

首先,将事件 smartChoose 绑定到每个推荐座位的按钮

代码如下

  //推荐选座,参数是推荐座位数目
  smartChoose: function(num){
        //找到影院座位水平垂直中间位置的后一排
        let rowStart = parseInt((this.seatRow-1)/2,10)+1;
        //先从中间排往后排搜索
      	let backResult = this.searchSeatByDirection(rowStart,this.seatRow-1,num);
      	if(backResult.length>0){
      	    this.chooseSeat(backResult);
            return
        }
      	//再从中间排往前排搜索
        let forwardResult = this.searchSeatByDirection(rowStart-1,0,num);
        if(forwardResult.length>0) {
            this.chooseSeat(forwardResult);
            return
        }
        //提示用户无合法位置可选
        alert('无合法位置可选!')
  },

第一步,找到剧院座位水平和垂直中间位置的后排,然后调用 this.searchSeatByDirection 朝那个方向搜索,先从中间行到后排,再从中间行到前排。如果任意方向找到搜索结果,都会直接返回,否则会提示用户没有合法位置,使用 chooseSeat 来更改座位的状态

重点介绍 searchSeatByDirection 的实现,代码如下

//向前后某个方向进行搜索的函数,参数是起始行,终止行,推荐座位个数
  searchSeatByDirection: function(fromRow,toRow,num){
    /*
     * 推荐座位规则
     * (1)初始状态从座位行数的一半处的后一排的中间开始向左右分别搜索,取离中间最近的,如果满足条件,
     *    记录下该结果离座位中轴线的距离,后排搜索完成后取距离最小的那个结果作为最终结果,优先向后排进行搜索,
     *    后排都没有才往前排搜,前排逻辑同上
     * (2)只考虑并排且连续的座位,不能不在一排或者一排中间有分隔
     * */
    /*
     * 保存当前方向搜索结果的数组,元素是对象,result是结果数组,offset代表与中轴线的偏移距离
     * {
     *   result:Array([x,y])
     *   offset:Number
     * }
     */
    let currentDirectionSearchResult = [];
    //确定行数的大小关系,从小到大进行遍历
    let largeRow = fromRow>toRow?fromRow:toRow,
        smallRow = fromRow>toRow?toRow:fromRow;
    //逐行搜索
    for(let i=smallRow;i<=largeRow;i++){
      //每一排的搜索,找出该排里中轴线最近的一组座位
      let tempRowResult = [],
          minDistanceToMidLine=Infinity;
      for(let j=0;j<=this.seatCol - num;j++){
        //如果有合法位置
        if(this.checkRowSeatContinusAndEmpty(i,j,j+num-1)){
          //计算该组位置距离中轴线的距离:该组位置的中间位置到中轴线的距离
          let resultMidPos = parseInt((j+num/2),10);
          let distance = Math.abs(parseInt(this.seatCol/2) - resultMidPos);
          //如果距离较短则更新
          if(distance//该行的最终结果
            tempRowResult = this.generateRowResult(i,j,j+num-1)
          }
        }
      }
      //保存该行的最终结果
      currentDirectionSearchResult.push({
        result:tempRowResult,
        offset:minDistanceToMidLine
      })
    }
    //处理后排的搜索结果:找到距离中轴线最短的一个
    //注意这里的逻辑需要区分前后排,对于后排是从前往后,前排则是从后往前找
    let isBackDir = fromRow < toRow;
    let finalReuslt = [],minDistanceToMid = Infinity;
    if(isBackDir){
    	//后排情况,从前往后
      currentDirectionSearchResult.forEach((item)=>{
        if(item.offset < minDistanceToMid){
          finalReuslt = item.result;
          minDistanceToMid = item.offset;
        }
      });
    }else{
    	//前排情况,从后往前找
      currentDirectionSearchResult.reverse().forEach((item)=>{
        if(item.offset < minDistanceToMid){
          finalReuslt = item.result;
          minDistanceToMid = item.offset;
        }
      })
    }
    //直接返回结果
    return finalReuslt
  },

代码有点长,但逻辑并不难,就是执行前面的规则,对于每一行搜索,都可能有多个合理的座位结果

如果建议有 5 个座位,首先判断 1-5 的位置是否合理,如果合理,则记录从中间位置(3 号)到中心轴的距离和座位结果数组,然后向右移动一个位置,检查 2-6 的位置是否合理,如果合理, 比较 2-6 号位置的中间位置(4 号)与前一个轴的距离,取最短的那个,更新座位结果数组。在此迭代之后,将确定该行的最终结果,并将每行的最佳结果存储在 currentDirectionSearchResult 数组中

然后,遍历完后排方向的所有行后,得到一个由每行的最佳结果组成的数组:currentDirectionSearchResult半岛·体育网站平台登陆,然后按照规则遍历数组,取最靠近中心轴的那个作为最终结果

这个算法可以优化,直接从中间到 2 边搜索,找到然后返回,虽然写起来有点麻烦,但效率绝对很高。应该注意的是,在前排的情况下,currentDirectionSearchResult.reverse() 应该反转为数组,因为对于前排,前排的后半部分是首选的(没有人愿意坐在第一行!),这与后排相反

最后

这个算法有点问题,如下所示

如何检测座位上是否有人_座位数怎么算_算法判断座位是否有人

最左边的 2 个绿色座位是最后一次点击推荐 2 个座位的结果,但位置不如另一个箭头处的 2 个位置合理,说明算法其实并不完美,可能是上面的分析不到位,其实美团的算法也存在问题, 如下图所示

如何检测座位上是否有人_算法判断座位是否有人_座位数怎么算

这个推荐的合理位置应该是 4 个位置向左移动一个方格,这是中心位置,这个推荐有偏移量,不知道为什么,网上没有具体的算法逻辑,只能靠猜想和实验

关键词:

客户评论

我要评论