857. Minimum Cost to Hire K Workers (O(N·logN)算法解析C++版)

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

  • 题目难度Hard

有 N 名工人。 第 i 名工人的工作质量为 quality[i] ,其最低期望工资为 wage[i] 。

现在我们想雇佣 K 名工人组成一个工资组。在雇佣 一组 K 名工人时,我们必须按照下述规则向他们支付工资:

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

返回组成一个满足上述条件的工资组至少需要多少钱。

示例 1:

输入: quality = [10,20,5], wage = [70,50,30], K = 2
输出: 105.00000
解释: 我们向 0 号工人支付 70,向 2 号工人支付 35。

示例 2:

输入: quality = [3,1,10,10,1], wage = [4,8,2,2,7], K = 3
输出: 30.66667
解释: 我们向 0 号工人支付 4,向 2 号和 3 号分别支付 13.33333。

提示:

  1. 1 <= K <= N <= 10000,其中 N = quality.length = wage.length
  2. 1 <= quality[i] <= 10000
  3. 1 <= wage[i] <= 10000
  4. 与正确答案误差在 10^-5 之内的答案将被视为正确的。

 

原题地址:

https://leetcode-cn.com/contest/weekly-contest-90/problems/minimum-cost-to-hire-k-workers/

如果还没有做,或者思考时间不到一个小时,最好不要看哦!以免剧透。

算法分析:

一、目标与条件的关系分析


 ①对工资组中的每名工人,应当按其工作质量quality与同组其他工人的工作质量的比例来支付工资wage。

因为支付给工人的工资必须和工人的工作效率——quality[]成正比,
这意味着选取了K名工人(记其编号为集合S 意为select)后,

支付的总工资 = wage(S)= rate(S)·quality_sum(S),
其中: quality\_sum(S) =\sum \begin{Bmatrix} quality[i] & |\forall i\in S \end{Bmatrix}

,而rate(S)取决于选取的工人集合S,看条件②。


    ②工资组中的每名工人至少应当得到他们的最低期望工资。
支付给i号的工资必须不少于wage[i],

\forall i\in S , rate(S)\cdot quality[i]> wage[i]
定义每个工人的单位劳动力价格 rate[i]=\frac{wage[i]}{quality[i]}(即性价比的倒数),
则应取 rate(S)=min\begin{Bmatrix} rate[i] & |\forall i\in S \end{Bmatrix}

 

二、暴力算法思路

    由上述分析,求解的目标就是找出某个集合S,使得wage(S)最小并返回。

只需用一个递归:(只做简单分析)
void fun(待选取的工人数量:num,已选取的最大下标:i,quality_sum,rate_max)
FOR( j  = i+1 ~ N-1)
递归调用:fun(--num,j,quality_sum +quality[j],max(rate_max,wage[j] /quality[j]));

递归出口:
    ①当num减为0时,比较表示最终解的全局变量 best和 rate_max*quality_sum 
    ②new_i达到N时也跳出递归。

  由于1 <= K <= N <= 10000,所以暴力算法:O(组合数(N,K))的时间复杂度是无法接受的!果断放弃!

 

三、创造并利用单调性

分析求解目标:总花费 wage(S)= rate(S)·quality_sum(S),

一、如果quality[]整体下降,quality_sum(S)会下降,但rate(S)上升,所以单纯按quality[]贪心是无用的。

二、不难发现wage[]整体下降,则总花费wage(S)的确能下降。那么可以按wage[]先取小再取大的贪心策略吗?

注意条件②,实际上降低S中大部分工人的wage[]是不会影响wage(S)的,只有降低rate[]值最高者(性价比最低者)的wage[]才能降低总花费wage(S)。所以按wage[]的贪心策略实际上受到rate[]值制约。

三、由分析二,注意到rate(S)受S中rate[]值最高者控制,而quality_sum(S)则受全体S控制。
所以尽管集合S有组合数(N,K)种取法,但是★rate(S)至多只有N种取法!取{rate[0],rate[1],...,rate[N-1]}中的一个。

四、所以要对rate[i]=\frac{wage[i]}{quality[i]} 进行升序排序,利用其单调性大大减少枚举量。
当i≥K时,只要选择S在编号0~i中取(其中i必须取),则rate[i]=rate(S)。

那么quality_sum(S)只需取编号0~i中quality[]值最小的K个工人的下标(其中i必须取),

做法很简单!只要在将quality[0~i]值升序排序,取前K-1个求和+quality[i]即可!

实际上最优解法求quality_sum(S)时间复杂度可以达到O(logK),想想堆排序!

 

四、O(N²)版本

注意此版本实际上是从rate高往低方向遍历,为了将求quality_sum(S)的时间复杂度从 O(NlogN)减少到O(N)。

其实我应该写个O(N²logN)的版本,就是“三、四”分析中的简单版本,更方便理解!(如果需求可以留言,我有空再改)


typedef int                 BOOL;
typedef struct SORT{
	double rate;	//价格:wage/ quality 即性价比的倒数
	int quality;	//工作效率
	BOOL visit;		//1是/0否 已经用过(用过==已经遍历完选择此工人能达到的最优解)
	//不用bool的原因是:结构体要凑 4 Byte整数倍字长,用BOOL比较可节省硬件开支
	SORT(const int & quality, const int & wage)
		:rate((double)wage/ quality), quality(quality), visit(0){}
};
//按价格(性价比的倒数)升序(次:工作效率升序)排序
bool rate_cmp(const SORT &a, const SORT &b) {
	if (a.rate == b.rate)return a.quality < b.quality;
	else return a.rate < b.rate;
}
//按工作效率升序排序(索引排序)
bool q_cmp(const vector<SORT>::iterator &a, const vector<SORT>::iterator &b) {
	return a->quality < b->quality;
}

#define INT_MAX       2147483647    // maximum (signed) int value
class Solution {
public:
	double mincostToHireWorkers(vector<int>& quality, vector<int>& wage, int K) {
		vector<SORT> r_rank;	//按价格(性价比的倒数)升序(次:工作效率升序)排序
		vector<vector<SORT>::iterator> q_rank;	//按工作效率升序排序(索引排序)
		size_t N = quality.size();
		r_rank.reserve(N);	//预留空间
		q_rank.reserve(N);
		//初始化 r_rank
		for (size_t i = 0; i < N; i++){	
			r_rank.push_back(SORT(quality[i], wage[i]));
		}//按价格(性价比的倒数)升序(次:工作效率升序)排序
		sort(r_rank.begin(), r_rank.end(), rate_cmp);
		//初始化q_rank
		for (auto r = r_rank.begin(); r != r_rank.end();++r) {
			q_rank.push_back(r);
		}//按工作效率升序排序(索引排序)
		sort(q_rank.begin(), q_rank.end(), q_cmp);

		double best = 1e100;	//要找最小,则初值大
		int K_1 = K - 1;		//要选K名,则所选工人的最大下标至少要 K-1
		for (size_t i = N; i-- > K_1;) {	//从价格最高(性价比最低)开始遍历
			int q_sum = r_rank[i].quality; //求K名未被使用过的工人(包含i号工人)的工作效率总和,要尽可能低。
			r_rank[i].visit = 1;	//表示r_rank[i]已经用过了
			for (size_t j = 0, c = K_1; c > 0; j++) {
				if (j >= N) {	//(理论上该if不会生效)
					return best;	//说明未使用过的工人已经凑不够K名了,无需遍历了
				}
				if (0 == q_rank[j]->visit) {	//未用过的
					q_sum += q_rank[j]->quality;
					--c;
				}
			}
			double wage_sum = q_sum*r_rank[i].rate;	//所有人要以最低性价比付工资
			if (wage_sum < best)best = wage_sum;	//比较最优
		}
		return best;
	}
};

该版本是我一开始写的,当然注释是后来加的(代码有调整)。其实还没写完此版本我就突然想到O(NlogN)版本了。

 

五、LeetCode竞赛中执行时间最优的大神的解法——O(NlogN)版本

该大神的代码可以在LeetCode中提交了C++版本通过后查看(如下图点击最左边的红条子即可出现)。此处的代码被替换了部分变量名以便与本文的分析采用的命名一致,当然注释也是本人加的。

LeetCode中查看大神的代码截图

代码如下:

class Solution {
public:
	double mincostToHireWorkers(vector<int>& quality, vector<int>& wage, int K) {
		int N = quality.size();
		//按照 rate[i] = wage[i]/quality[i](即性价比的倒数)进行索引升序排序的索引向量
		vector<int> rate_i(N);	
		for (int i = 0; i < N; i++) {
			rate_i[i] = i;
		}
		sort(rate_i.begin(), rate_i.end(), [&](const int &a, const int &b) {
			return wage[a] * quality[b] < wage[b] * quality[a];	//比值换乘法节省硬件开销
		});
		int quality_sum = 0;	//对应S中元素总和
		priority_queue<int> S;
		double best = 1e18;
		for (auto i : rate_i) {
			double rate = (double)wage[i] / quality[i];
			quality_sum += quality[i];	//quality_sum对应S中元素总和
			S.push(quality[i]);
			while (S.size() > K) {
				int w = S.top(); S.pop();	//堆顶是最大者,抛弃余下较小者(当前最优)
				quality_sum -= w;
			}
			if (S.size() == K) {	//当i∈S时的最优解 与历史最优比较
				best = min(best, rate * quality_sum);
			}
		}
		return best;
	}
};

 

六、从“版本四”的O(N²)改进到O(NlogN)版本

此版本是我自己改进的,其实和四号版本只有在求quality_sum(S)时有差别,不可避免的受到定式思维的影响。虽然时间复杂度和大神的一致,但代码比较繁杂,故放在最后,仅供参考。

typedef struct SORT {
	double rate;	//价格:wage/ quality 即性价比的倒数
	int quality;	//工作效率
	int q_rank;		//该工人在quality[]中的排名(quality[]最小的为第0名)
	//当q_rank为 INF(无穷大)时,表示已经使用过了(已经遍历完选择此工人能达到的最优解)
	SORT(const int & quality, const int & wage) :rate((double)wage / quality), quality(quality) {}
};
//按价格(性价比的倒数)升序(次:工作效率升序)排序
bool rate_cmp(const SORT &a, const SORT &b) {
	if (a.rate == b.rate)return a.quality < b.quality;
	else return a.rate < b.rate;
}
//按工作效率升序排序(索引排序)
bool quality_cmp(const vector<SORT>::iterator &a, const vector<SORT>::iterator &b) {
	return a->quality < b->quality;
}

#define INF  2147483647    // maximum (signed) int value
class Solution {
public:
	double mincostToHireWorkers(vector<int>& quality, vector<int>& wage, int K) {
		vector<SORT> r_rank;	//按价格(性价比的倒数)升序(次:工作效率升序)排序
		vector<vector<SORT>::iterator> q_rank;	//按工作效率升序排序(索引排序)
		size_t N = quality.size();
		r_rank.reserve(N);	//预留空间
		q_rank.reserve(N);
		for (size_t i = 0; i < N; i++) {
			r_rank.push_back(SORT(quality[i], wage[i]));
		} // 按价格(性价比的倒数)升序(次:工作效率升序)排序
		sort(r_rank.begin(), r_rank.end(), rate_cmp);

		int q_sum = 0, j = K - 1;	//求quality_sum使用,时间复杂度可达O(1)
		for (auto r = r_rank.begin(); r != r_rank.end(); ++r) {
			q_rank.push_back(r);
		}//按工作效率升序排序(索引排序)
		sort(q_rank.begin(), q_rank.end(), quality_cmp);
		for (size_t i = 0; i < N; i++) {
			q_rank[i]->q_rank = i;
			if (i < K)q_sum += q_rank[i]->quality;
		}
		//初始的 q_sum 就是quality[0~j]之和,遍历中每用掉q_sum中的一个,就将未使用且未选中的选一个最小的来替补。

		double best = 1e100;
		int K_1 = K - 1;		//要选K名,则所选工人的最大下标至少要 K-1
		for (size_t i = N; i-- > K_1;) {	//从价格最高(性价比最低)开始遍历
			if (r_rank[i].q_rank <= j) {	//q_sum 已经包含 r_rank[i].quality
											//先比较最优
				double wage_sum = q_sum*r_rank[i].rate;
				if (wage_sum < best)best = wage_sum;
				//∵用掉了r_rank[i],用未使用过的最小的quality替换r_rank[i].quality 更新q_sum 
				while (++j < N) {
					if (q_rank[j]->q_rank <N) {	//未被用过
						q_sum += q_rank[j]->quality - r_rank[i].quality;	//替换
						break;
					}
				}
				if (j >= N) {	//(理论上该if不会生效)
					return best;	//说明未使用过的工人已经凑不够K名了,无需遍历了
				}
			}
			else {	//q_sum 不含 r_rank[i].quality, 则用 r_rank[i].quality 临时换掉 q_sum中最大的quality
				double wage_sum = (q_sum - q_rank[j]->quality + r_rank[i].quality)
					*r_rank[i].rate;
				if (wage_sum < best)best = wage_sum;	//比较最优
			}
			r_rank[i].q_rank = INF;	//表示r_rank[i]已经用过了,不能再选入 q_sum
		}
		return best;
	}
};

如果有什么问题或意见请提出,欢迎大神指正。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值