算法笔记21:雇佣 K 名工人的最低成本

413 阅读3分钟

857.雇佣 K 名工人的最低成本

看到这道题目之后试着从数学角度去思考了一下如果要计算这个成本该怎么搞。我们先不管最低成本如何计算,先思考随便挑出 K 个工人之后,对于这个工资组来说成本是多少。

  1. 对工资组中的每名工人,应当按其工作质量与同组其他工人的工作质量的比例来支付工资。
  2. 工资组中的每名工人至少应当得到他们的最低期望工资。

假设我们有 [worker1, ..., workerK] 总共 K 名工人。他们的薪资为 [wage1, ..., wageK] ,工作质量为 [quality1, ..., qualityK]

那么如何来计算由这 K 名工人组成的工资组成本呢?首先我们需要找到基准工资,也就是说这个人的工资是它的期望薪资,只要按照他的期望支付其他人的薪资就都满足了他们的要求。

假设已经找到这名员工 X ,那就可以计算这个工资组的成本了:

Cost = wageX * (quality1 + ... + qualityK) / qualityX

所以第一个问题就是如何找到这个 X 。如果拿 wage 去除 quality ,可以得到一个效用比。假如有三名工人 wage: [20, 40, 50]; quality: [10, 5, 10],那么效用比分别为 [2, 8, 5]

  • 如果我们挑 0 号工人的期望工资作为基准工资,那么其他两个工人都 拿不到 自己的基准工资。
  • 如果我们挑 1 号工人的期望工资作为基准工资,那么其他两个工人都 能拿到 自己的基准工资。
  • 如果我们挑 2 号工人的期望工资作为基准工资,那么 0 号工人 能拿到 自己的基准工资,而 1 号工人 拿不到

可以发现那些比 X 的效用比更低的工人,在给 X 期望薪资的情况下,都能拿到不低于自己期望薪资的收入。反之则不能。

这个通过数学也能得到印证。我们可以假设员工 K 的收入是 incomeK ,并且根据分配规则可以得出 incomeK = (qualityK / qualityX) * wageX ,再代入题目要求的那个必须不低于自己最低期望工资的不等式:

incomeK >= wageK

=> (qualityK / qualityX) * wageX >= wageK

=> wageX / qualityX >= wageK / qualityK

这个 wageK/ qualityK 不就是我们的效用比么。所以可以知道如果我们挑中工人 X 作为基准之后,为了满足题意,那么剩下的 K-1 个工人的效用比就都必须不大于 X 的效用比。其实到这里整个题目已经被重新定义了一下,回到之前写的工资组成本公式并稍微变换一下写法:

Cost = (wageX / qualityX) * (quality1 + ... + qualityK)

可以发现对于某工人来说,用他的效用比去乘以其他 K-1 个工人的 quality 之和就可以了。但题目是求最低成本,而作为成本这个乘积的两项相互还有关联。于是到这里我就卡壳了……

看了题解之后发现前面效用比(题解里叫价值)是一个个遍历去试的,而后面那个工作质量之和,是通过一个优先队列不断给出最大值,这样我们可以通过不断削减最大值,确保在用每一个员工作为基准薪资的同时,quality 之和总是最小的。

代码如下:

/**
 * @param {number[]} quality
 * @param {number[]} wage
 * @param {number} k
 * @return {number}
 */

class PriorityQueue {
    queue = [];
    
    parentIndex(i) {
        return Math.floor((i - 1) / 2)
    }
    
    leftChildIndex(i) {
        return i * 2 + 1;
    }
    
    rightChildIndex(i) {
        return i * 2 + 2;
    }
  
    offer(item) {
        this.queue.push(item);
        this.shiftUp(this.queue.length - 1);
    }
    
    poll() {
        const item = this.queue.shift();

        if (this.queue.length !== 0) {
            const lastLeaf = this.queue.pop();
            this.queue.unshift(lastLeaf);
            this.shiftDown(0);
        }    
        return item;
    }

    size() {
        return this.queue.length;
    }
    
    shiftUp(i) {
        const parentIndex = this.parentIndex(i);
        if (i > 0 && this.queue[parentIndex] < this.queue[i]) {
            this.swap(i, parentIndex);
            this.shiftUp(parentIndex);
        }
    }
    
    shiftDown(i) {
        let largest = i;
        const l = this.leftChildIndex(i);
        const r = this.rightChildIndex(i);
        
        if (l < this.queue.length && this.queue[largest] < this.queue[l]) {
            largest = l;
        }
        if (r < this.queue.length && this.queue[largest] < this.queue[r]) {
            largest = r;
        }
        
        if (largest != i) {
            this.swap(i, largest);
            this.shiftDown(largest);
        }
    }
    
    swap(l, r) {
        const t = this.queue[l];
        this.queue[l] = this.queue[r];
        this.queue[r] = t;
    }
}

class Worker {
    constructor(quality, wage) {
        this.quality = quality;
        this.wage = wage;
        this.ratio = wage / quality;
    }
}

const mincostToHireWorkers = (quality, wage, k) => {
    const workers = [];
    // create an instance for each worker
    quality.forEach((q, index) => {
        workers.push(new Worker(q, wage[index]))
    });
    // sort, start with lower ratios
    workers.sort((a, b) => a.ratio - b.ratio);

    const queue = new PriorityQueue();
    let ans = Number.MAX_SAFE_INTEGER;
    let sumQ = 0;
    for (const worker of workers) {
        // add current quality to the sum
        sumQ += worker.quality;
        // and also add it to the queue
        // let it figure out which one is the largest now
        queue.offer(worker.quality);
        // if we already added more than we need
        if (queue.size() > k) {
            sumQ -= queue.poll();
        }
        // if current size is good, we compare and save
        if (queue.size() === k) {
            ans = Math.min(ans, worker.ratio * sumQ);
        }
    }

    return ans;
};