经理有一堆项目需要程序员们做,请问每一个项目的完成时间是多少?

经理有一堆项目需要程序员们做,请问每一个项目的完成时间是多少?

提示:网易笔试原题
当年考这个题目,总共有1200人参加,只有6个人过了
而我在20220413华为的笔试题中,又遇到了类似的题目,由于这类题目我还是没怎么学透,所以没能做出来,今天,好好总结一下,把这个题突破!!!
改写系统堆结构,是非常非常重要的能力


题目

输入为一个数组arr,N*4维,表示N个项目,每个项目包括,经理编号pm,该项目润色出来的时间start,该项目对于经理来说的优先级rand,完成该项目要消耗的时间cost;
现在有pms>0个经理数量,每个人负责自己的一些项目,且,一次一个经理只能提供一个任务给程序员干,干完了这个项目,才能继续提供下一个项目。对于经理来说,它最喜欢优先级高的项目,如果优先级相同,它最喜欢耗时少的任务,如果耗时相同,它最喜欢润色时间早的任务。
现在有sde>0个程序员,自己选一个项目做,它们喜欢耗时少的任务,如果耗时相同,则更喜欢项目经理编号小的项目。
请返回一个数组,N长,表示N个项目的结束时间。
注意,优先级:1高于2


一、审题

示例:pms=2,sde=2,2个经理,2个程序员
programs =
1 1 1 2
1 2 1 1
1 3 2 2
2 1 1 2
2 3 5 5
输出:3 4 5 3 9
每个任务完成的时间有重复,因为有2个程序员同时在干


二、解题

系统的输入,是N*4的一堆项目,项目本身就是一个数据结构,需要造出来:

//前提是项目的对象
    public static class Program{
        public int index;//我刚刚进来的时候,我的位置是在哪里,方便填写结果的
        public int pm;//经理编号
        public int start;//润色时间
        public int rank;//优先级
        public int cost;//耗费时间

        public Program(int i, int p, int s, int r, int c){
            index = i;
            pm = p;
            start = s;
            rank = r;
            cost = c;
        }
    }

这种题目就是公司实际工作中遇到的真实案例,因此,需要学习一个非常有用又极其容易理解的方案,那就是改写系统堆结构:

构建一个联合经理–员工任务池的堆,用来模拟经理的任务池,按照经理的喜好投送给程序员任务池,而程序员又可以按照自己的喜好选择任务来做。
显然,这里要理解,虽然经理可能按照自己的喜好润色出项目来给了程序员,但是程序员做完上一个任务之后,下一个选谁的任务,是非常不确定的,那得看经理的项目情况如何,比如:

下图中,bigQueue是我们要构建的经理–员工堆结构,这是系统压根没有的【这是我们要手改堆的地方】,系统的就一个priorityQueue,它只能简单地做一个排序任务。bigQueue中,有2个数据结构:
(1)一个是List:PriorityQueue,注意到了吗?这个列表中包含了好多个系统堆结构【pms个,每个经理一个呗】,这些系统对用来干嘛?用来存放经理自己的任务呗。我们给这个list娶个名字:pmQueue。【下面不熟悉回来看】
(2)一个系统堆结构:sdeHeap,这是一个size长度的数组,即堆,系统堆说白了就是一个数组,然后通过下标关系模拟构建的而一个二叉树堆结构。这个堆用来干嘛呢,看箭头,每个经理自己挑选一个非常喜欢的项目,扔给员工任务池,希望程序员们尽快把这些任务搞定了。
图1
经理想得倒是很美,但是程序员可也有自己的喜好呢,他们喜欢干耗时任务少的,如果耗时任务一样,则喜欢干级别搞得经理,id好堪比优先级,1234,1高于2,这是公司员工的人性,面对级别更高的领导,自然是先完成级别更高的经理的任务,但是人也是贪钱的,如果耗时少的任务,多做得的钱多,何乐而不为?
这意味着,经理们投了size个任务来,程序员干谁的不一定呢,比如图中经理1和经理2分别把ac扔进sedHeap中,程序员可能首先不是干a,而可能先干c。
那假如c已经被干完了,那就要把sdeHeap中的c抹掉;
此时pm2自然要补充一个任务给sdeHeap,那d来了,请问程序员们是干原来的a呢?还是d?
这就不得而知了,就得看你d来了sdeHeap后,能排序排到哪里了?
如果排到堆顶,则d得先替换掉pm2之前堆顶按个位置,然后程序员们先干d,
假如轮不到干d,d就得往下排,则程序员们先干a。

好,这个道理明白了,那么现在告诉你为什么要改写系统堆结构?
刚刚上面例子说了,经理2一旦抛出一个项目d,我们要把d拿去替换之前经理2在sdeHeap中原来那个任务a的位置,既然要知道d是去替换哪个位置,那你堆中就要记录,原来所有pm经理们投过来的任务现在在哪个位置?
故,要造一个indexes数组来记录

请注意:系统堆,是无法索引你原来那些经理们投送的任务在哪里的。

因此,我们需要手动改写系统堆,造出这个联合堆结构来,关键要造一个indexes数组来记录原来所有pm经理们投过来的任务现在在哪个位置。

联合经理–员工堆bigQueue中
经理任务列表pmQueue内部怎么排序呢?题目说的很清楚,按照优先级,再按耗时少,再按润色时间早的,排序:

//然后是经理比较器,员工比较器,润色排序比较器
    public static class PmLoveRule implements Comparator<Program> {
        @Override
        public int compare(Program o1, Program o2){
            //经理三种情况
            if (o1.rank != o2.rank) return o1.rank - o2.rank;//小根堆,优先级小的排前面
            else if (o1.cost != o2.cost) return o1.cost - o2.cost;//否则看耗费时长少的
            else return o1.start - o2.start;
        }
    }

sdeHeap的排序规则呢?先按耗时少,再经理pm小的排序:

 public static int SdeLoveRule(Program o1, Program o2){
        //这个是一个比较函数,比较任务优先级
        if (o1.cost != o2.cost) return o1.cost - o2.cost;//当o1耗费时长少,则返回-,先做o1
        else return o1.pm - o2.pm;//当经理编号小返回-,先做小编号的
    }

联合经理–员工堆bigQueue数据结构初始化工作:
注意,初始化必须告知经理个数pm是多少。
造列表pmQueue,造sdeHeap【最大长度pm个,经理最多同时投这么多】,用heapSize表示实际投入的任务个数,最开始一个都没有投,那就0个;再造indexes,用来表示pmQueue他们堆顶那个任务,在sdeHeap中的位置。

有了初始化,再造:联合经理–员工堆bigQueue的内置函数
1)isEmpty()判断sdeHeap是否实际为空?
2)void add(Program p)函数,向sdeHeap中添加一个新来的项目
3)Program get()函数,程序员干掉一个任务,拿出去统计完成时间。
4)改写堆,必需要有内置的heapInsert函数,从index位置,往上浮动,检查新来的元素,是否需要往上浮动?
5)改写堆,必须要用内置的heapify函数,从i位置开始往下沉,检查i位置的元素是否需要往下浮动?
6)交换函数swap(i,j)交换sdeHeap中i和j位置的元素,同步交换indexes中经理们任务的位置【不熟悉回去看上面的解释】,项目交换了,经理投过来的任务位置自然也跟着变动。

当然,我们用数组表示堆sdeHeap的,在下标上对应是一个二叉树,寻找任意位置i的左子left右子right,其坐标换算是这样的:
图2

仔细研读下面的代码,就知道每一个函数,非常重要,也是改写堆必须要完成的事情:

//动态调整任务池-联合经理--员工堆bigQueue
    public static class BigQueue{
        public List<PriorityQueue<Program>> pmQueue;//经理列表,每个堆都有自己的多个任务,0号堆不用
        public Program[] sdeHeap;//任务堆,谁在顶端谁先被做
        public int heapSize;//任务堆已经来了多少个任务的实际大小
        public int[] indexes;//经理pm号堆顶的那个任务,在任务堆中的哪个位置,方便找,它和任务堆一定是联合使用的

        public BigQueue(int pms){
            //初始化时,知道经理的个数,就知道任务堆应该是多大,也知道任务堆位置表的大小,最开始任务堆来了0个任务
            heapSize = 0;
            sdeHeap = new Program[pms];//任务堆的最终大小
            indexes = new int[pms + 1];//0号位置我不用,因为经理的号从1开始的----这里一定配合使用的
            pmQueue = new ArrayList<>();//经理堆也该填了
            for (int i = 0; i <= pms; i++) {
                indexes[i] = -1;//一个任务都没有,意味着位置为-1
                pmQueue.add(new PriorityQueue<Program>(new PmLoveRule()));//0号经理不用,然后每个经理的任务有自己的喜好,顶端那个先做
            }
        }

        //判断空的函数
        public boolean isEmpty(){
            return heapSize == 0;
        }

        //很重要的交换位置的函数
        private void swap(int index1, int index2){
            Program p1 = sdeHeap[index1];
            Program p2 = sdeHeap[index2];
            sdeHeap[index1] = p2;
            sdeHeap[index2] = p1;
            indexes[p1.pm] = index2;//经理堆顶的任务位置变为index2了
            indexes[p2.pm] = index1;
        }
        
        //很重要的heapInsert函数,上浮,看父子大小
        private void heapInsert(int i){
            //从i位置开始上浮
            while (i != 0){
                int parent = (i - 1) >> 1;//父的位置,数组下标的换算,堆就是一个表示二叉树的数组
                if (SdeLoveRule(sdeHeap[i], sdeHeap[parent]) < 0) {
                    //i比父亲小,需要上浮
                    swap(i, parent);
                    i = parent;//继续上窜
                }else break;//一定不能上浮挂
            }
        }
        
        //很重要的heapify函数,下沉,需要比较我i与俩儿子的关系,左右子
        private void heapify(int i){
            int left = (i << 1) + 1;
            int right = (i << 1) + 2;//2i+1,2就是左右子的坐标
            int lovest = i;//目前程序员最想做的任务是lovest位置,类似于smallest位置
            
            while (left < heapSize){
                //左子还没有越界继续玩
                if (SdeLoveRule(sdeHeap[i], sdeHeap[left]) > 0) lovest = left;//i竟然比左子大,需要下沉
                if (right < heapSize && SdeLoveRule(sdeHeap[lovest], sdeHeap[right]) > 0) lovest = right;//i大于右子,下沉
                if (lovest == i) break;//左右子都比我i大,那算了吧,不干
                
                //需要下沉就交换
                swap(i, lovest);
                i = lovest;//继续下窜
                left = (i << 1) + 1;
                right = (i << 1) + 2;//更新左右子的坐标
            }
        }
        
        //核心函数来了
        //添加一个项目进来,任务池中经理们的档案需要变化,同时程序员任务等待区也要更新
        public void add(Program x){
            //x是谁的呢
            PriorityQueue<Program> pmHeap = pmQueue.get(x.pm);//根据经理号,找到自己的档案
            pmHeap.add(x);//加入

            //然后看看自己的头部有没有变更
            Program head = pmHeap.peek();
            int headIndex = indexes[head.pm];//看看它之前在任务排队区哪个位置,需要换吗
            if (headIndex == -1){
                //压根就没有在过
                //直接屁股加,调整就行
                sdeHeap[heapSize] = head;
                indexes[head.pm] = heapSize;
                heapInsert(heapSize++);//从末尾开始玩上浮,然后size++
            }else {
                //之前有就替换,然后上下沉浮检查
                sdeHeap[headIndex] = head;//替换
                heapInsert(headIndex);
                heapify(headIndex);
            }
        }

        //核心拿任务的函数
        public Program get(){
            //拿走一个任务,我就要清理经理的档案,如果经历没有任务,那需要减任务区大小
            Program head = sdeHeap[0];//返回结果

            //清理检查
            PriorityQueue<Program> pmHeap = pmQueue.get(head.pm);//根据返回结果找到经理
            //让经理清除项目啊
            pmHeap.poll();
            if (pmHeap.isEmpty()){
                //没了
                swap(0, heapSize - 1);//把任务区那个调换到尾部,跟堆排序一个道理
                sdeHeap[--heapSize] = null;//清理
                indexes[head.pm] = -1;//没进来,不管了
            }else sdeHeap[0] = pmHeap.peek();//否则经理的堆顶,补充到现在等待区的堆顶
            //上面不管哪个情况,都需要从0开始下沉检查
            heapify(0);

            return head;
        }
    }

解释一下:
——SdeLoveRule(sdeHeap[i], sdeHeap[parent]) < 0)代表sdeHeap[i]更优,需要排在堆上面,故要交换位置,这是系统堆,改写堆中heapInsert的基本骚操作,同理:SdeLoveRule(sdeHeap[i], sdeHeap[left]) > 0代表sdeHeap[left]更优,需要排在堆的上面,也就是sdeHeap[i]需要下层,这是堆中heapify的骚操作
——add项目时,你要看看该经理pm之前是否投过任务,没有,就直接新加,有投过任务,找到之前被干完了的任务的位置,然后对其进行替换,这个找位置的事情,系统堆做不了,这就是为什么我们要改写堆结构的原因!!!【又说了一遍为啥要改写系统对结构】
——程序员get一个项目之后,检查堆顶位置对应经理,他还有任务吗,有,拿过来替换,再排序,如果没有了,那就让经理对应位置要变-1,表示从此不再有这个经理


okay,我们来看算法的宏观调度

现在,来了N个任务,咱们要想办法让所有的任务依次进bigQueue这个联合结构,方便经理投送任务到sdeHeap。
既然是依次进去,那就得有一个顺序,怎么来?
虽然经理有自己的喜好,但是程序员做的任务,一定是经理润色好了的任务,经理润色一个任务如果要很晚才出来,随你优先级多高,没卵用;
因此我们要造一个比较器,让系统来的任务,按照润色时间排序成一列,放入系统堆中startQueue=designQueue。排序规则就是start早的在前面:

//按照润色时间排序
    public static class StartRule implements Comparator<Program>{
        @Override
        public int compare(Program o1, Program o2){
            return o1.start - o2.start;//根据润色时间排名
        }
    }

即:startQueue=designQueue
图3
现在,我们来模拟程序员sde,不妨设,他们在1时候都有闲空,都能腾出手来干活,那就可以接单了
一旦一个程序员干完活,下一个时刻,比如7点干完上一个任务,那它就只能7点才来领下一个任务,有空的人都会提前去接任务干。
那么我们将所有程序员定义成一个腾出空来的唤醒时间堆结构:wakeHeap
啥意思,每个程序员,他腾出手来干活的时间,就是其唤醒时间,所有人排着队根据自己的唤醒时间来干活,谁醒的早【谁手里有空】,谁先接单,这跟美团骑手非常类似的。
wakeHeap如下:
图4
一个骑手,一个程序员,他单独干完一个单c,下次可能就只能在7点唤醒了,另一个程序员干任务a,下次可能8点才能干,也可能3点就能有空了呢,看他手头任务有多耗时。

好,整体来讲,算法大流程就是:
图5

(1)来了一堆任务N个,造一个任务堆startQueue=designQueue:先按照润色时间排好序,
(2)准备把这些项目依次扔进联合经理–员工堆:bigQueue;
(3)模拟程序员的唤醒时间,造一个全1的堆:wakeHeap;
(4)然后开始模拟员工做任务:
——一上来,找闲空员工第一个,去把任务堆中润色时间能拿来做的,放入bigQueue,润色好的全部放进去;
——注意,如果bigQueue没有任何项目,那说明还没润色好,咱们把wakeHeap中第一个员工他的唤醒时间1直接换位startQueue中第一个润色好的任务润色时间,这样的话,员工直接从第一任务开始干,而不是要继续从1时刻开始等等等等等,等半天;
——员工S直接从bigQueue中拿一个任务来做,员工唤醒就开始做,加上该项目的耗时,就是该项目的完成时间,这也就是我们要的结果。
——员工S下一次能唤醒的时间,当然也就是他刚刚完成上一个任务的时间,因此,把这时间放入wakeHeap中,让他继续排队。

算法代码:仔细研读,模拟程序员干活的数据结构,非常真实的公司干活制度:

//有了任务等待区,就可以将任务模拟完成记录结果返回
    public static int[] workFinish(int[][] programs, int pms, int sdes){
        PriorityQueue<Program> startQueue = new PriorityQueue<>(new StartRule());//按照润色时间升序
        int N = programs.length;
        for (int i = 0; i < N; i++) {
            Program p = new Program(i, programs[i][0], programs[i][1], programs[i][2], programs[i][3]);
            startQueue.add(p);//加入,一旦唤醒员工可以做了,就投进去
        }
        PriorityQueue<Integer> wakeHeap = new PriorityQueue<>();//员工唤醒的时间,全是1最开始
        for (int i = 0; i < sdes; i++) {
            wakeHeap.add(1);//最开始都是1时刻开始干活
        }

        BigQueue bigQueue = new BigQueue(pms);//任务池
        int finish = 0;
        int[] ans = new int[N];//返回结果

        while (finish != N){
            //完成一个任务呀finish++
            int sdeWakeTime = wakeHeap.poll();//整一个员工出来,能干???
            while (! startQueue.isEmpty()){
                if (startQueue.peek().start > sdeWakeTime) break;//现在一个员工都没空,那你没法投润色好的任务
                bigQueue.add(startQueue.poll());//否则你可以投一个到任务池去
            }

            //看看员工可否干
            if (bigQueue.isEmpty()) wakeHeap.add(startQueue.peek().start);//没有项目可做,你只能等那个润色好的来了才做
            else {
                Program pro = bigQueue.get();//整一个项目来干
                ans[pro.index] = sdeWakeTime + pro.cost;//耗时之后,下一次我空的时间,结果别放错了
                wakeHeap.add(ans[pro.index]);
                finish++;//表示干完一个了
            }
        }

        return ans;
    }

熟不熟悉?
这种一堆人排队做任务的事情,并不是什么困难事!
下面这些样例都是类似的堆结构可以完成的,而且大部分都直接用系统堆 搞定了。
——比如:一堆咖啡机在那造咖啡,求一堆人能拿到咖啡的时间?可不就是程序员完成任务的时间?无非就是换程序员为咖啡员
——比如:美团骑手完成送货的时间……太多了这种例子

测试案例:

public static void test(){
        int[][] arr ={
                {1,1,1,2},
                {1,2,1,1},
                {1,3,2,2},
                {2,1,1,2},
                {2,3,5,5}
        };
        int pms = 2;
        int sdes = 2;

        int[] ans = workFinish(arr, pms, sdes);
        for(Integer x : ans) System.out.print(x +" ");
    }

    public static void main(String[] args) {
        test();
    }

本题中,其实:模拟员工干活的算法倒是不难,但模拟那个经理投任务,程序员接任务的数据结构bigQueue,是很难写的,当然熟悉了手动改写堆结构,其实也就不难了。

在大厂笔试时,测试是需要自己写ACM格式的输入的,就一堆数组,很简单,如果重复测试,就用while循环,每次只处理一个数组即可。


总结

提示:重要经验:

1)难题,不要怕,想清楚算法的核心思想,宏观调度,然后就能破解!快速提升算法能力,coding能力!
2)改写堆结构,是算法大佬的必备技能,这是必须要练会的。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

冰露可乐

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

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

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

打赏作者

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

抵扣说明:

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

余额充值