LeetCode855:考场就座

  • 如题

    • image-20220926112215054
  • 思路分析:

    • 我们每次挑选座位的时候,需要里最近的人距离最大,也就是说如果要坐在两个人之间,应该坐在中点的位置

    • 因此我们可以将两个座位抽象成一个线段,第一个人是线段开始位置start,第二个人是线段结束位置end,线段长度是end-start

    • 此时我们就应该坐在线段的中点,离最近的人的距离就是线段长度length/2,因此我们要找出里最近的人的距离最大的位置=找出最大的那条线段

    • 此时就应该考虑能够动态找出最值的数据结构,一般是堆和平衡二叉树,但是堆只能操作堆顶元素,平衡二叉树可以操作根结点也可以操作孩子结点。由于我们不但需要添加座位,还需要随机离开某座位的时候删除该座位,因此只能操作最值的堆不可用,应该用平衡二叉树

    • java中操作平衡二叉树的数据结构有TreeSetTreeMap,这里使用TreeSet

    • 由于我们存放的是线段,这里可以用一个专门的内部类来表示,属性就包含线段开始位置start线段结束位置end,和线段长度end-start

      • 注意:两个座位形成的线段之间不能有其他座位
      static class Site {
          private int start;
          private int end;
          private int length;
      
          public Site(int start, int end, int length) {
              this.start = start;
              this.end = end;
              this.length = length;
          }
      }
      

      也可以使用数组表示,但是可读性没那么好

  • 代码

    class ExamRoom {
        // 因为需要用leave()删除座位,例如此时座位是[1,3,5],我们要删除座位3,那么[1,3]和[3,5]应该合成[1,5],
        // 此时需要根据座位号3找出[1,3]和[3,5],然后在平衡二叉树中删除它,左边的线段以3为end,右边的线段以3为start
        // 因此我们用两个hashMap分别保存以3为start的线段和以3为end的线段
        private Map<Integer, Site> startMap;
        private Map<Integer, Site> endMap;
        //平衡二叉树
        private TreeSet<Site> tree;
        //将构造器中的n作为属性,供其他方法使用
        private int N;
    
        public ExamRoom(int n) {
            N = n;
            startMap = new HashMap<>();
            endMap = new HashMap<>();
            
            //我们每次需要找一个新座位的时候,就是找出一条长度最大的线段,然后坐在该线段中间length/2的位置,因此我们比较器直接比较length/2的大小
            
            //注意:0和N两个最左和最右座位,因为最开始选的时候必须选0,此时还无法构成线段,因此考虑加入两个哨兵座位-1和N,他们构成初始线段[-1,N],
            //而我们实际座位应该在[0,N-1]之间
            
            //注意:普通情况下我们比较器比较两个元素的length/2的大小,因为我们选了这条线段就必须坐中间length/2的位置,保证离两端最远,但是
            // 如果有一个元素的开始或结束位置是哨兵位置,那么我们选该线段就不用坐中间,我们坐哨兵位置边上,因为哨兵不是座位,因此此时比较的是length-1(除开哨兵)
            tree = new TreeSet<>((a, b) -> {
                int lengthA = 0;
                int lengthB = 0;
                //如果a包含哨兵
                if (a.start == -1 || a.end == N) lengthA = a.length - 1;
                else lengthA = a.length / 2;
                //如果b包含哨兵
                if (b.start == -1 || b.end == N) lengthB = b.length - 1;
                else lengthB = b.length / 2;
                //如果长度一样就按起始位置start从小到大排序
                if (lengthA == lengthB)return a.start - b.start;
                //否则按长度从大到小排序
                return lengthB - lengthA;
            });
            //初试线段,起始结束位置都是哨兵,线段长度是end-start
            Site start = new Site(-1, N, N + 1);
            //初始化tree和两个map
            tree.add(start);
            startMap.put(start.start, start);
            endMap.put(start.end, start);
        }
    
        public int seat() {
            //选座位的时候挑出最大线段,坐中间,此时产生会两个新的线段[start,mid],[mid,end],记录下来
            Site site1 = null;
            Site site2 = null;
            //挑出最大线段
            Site site = tree.pollFirst();
            //上面提到过,如果线段包含哨兵,那么就不是坐中间,而是做哨兵边上
            if (site.start == -1) {//start是哨兵
                site1 = new Site(-1, 0, 1);
                site2 = new Site(0, site.end, site.end);
            }
            //end是哨兵
            else if (site.end == N) {
                site1 = new Site(site.start, N - 1, (N - 1) - site.start);
                site2 = new Site(N - 1, N, 1);
            }
            //都不是哨兵,坐中间
            else {
                site1 = new Site(site.start, site.start + site.length / 2, site.length / 2);
                site2 = new Site(site.start + site.length / 2, site.end, site.end - site.start - site.length / 2);
            }
    
            //将以该线段从两个hashMap中移出
            startMap.remove(site.start);
            endMap.remove(site.end);
    
            //在平衡二叉树中加入新生成的两个线段
            tree.add(site1);
            tree.add(site2);
    
            //在两个hashMap中加入新生成的两个线段
            startMap.put(site1.start, site1);
            endMap.put(site1.end, site1);
            startMap.put(site2.start, site2);
            endMap.put(site2.end, site2);
            //返回新挑选的座位号,site1.end=site2.start,返回哪一个都一样
            return site1.end;
        }
    
        //离开座位的时候应该将该座位左右两端的线段合并
        public void leave(int p) {
            //先从两个hashMap中移出以该座位号为开始的线段和以该座位为结束的线段,然后得到这两个线段,在tree中也删除这两个线段
            Site site0 = startMap.remove(p);
            Site site1 = endMap.remove(p);
            tree.remove(site0);
            tree.remove(site1);
            //将两个线段合并,生成新线段,然后加入tree和两个hashMap中
            Site site = new Site(site1.start, site0.end, site0.end - site1.start);
            tree.add(site);
            startMap.put(site.start,site);
            endMap.put(site.end,site);
        }
    
        static class Site {
            private int start;
            private int end;
            private int length;
    
            public Site(int start, int end, int length) {
                this.start = start;
                this.end = end;
                this.length = length;
            }
        }
    }
    
  • 注意事项:我们TreeSet中的比较器比较的是length/2,不能比较length

    • 因为如果有线段[0,4][5,10],那么比较长度的话[5,10]长度是5,[0,4]长度是4,我们会选择[5,10],但是5是奇数,5/2=2,我们和最近的人距离是2。
    • [0,4]的长度4是偶数,4/2=2,我们和最近的人的距离也是2。
    • 根据题意,距离一样我们应该选靠前的座位,因此此时应该选[0,4],选[0,5]就错了
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一酒。

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值