We have jobs: difficulty[i] is the difficulty of the ith job, and profit[i] is the profit of the ith job.
Now we have some workers. worker[i] is the ability of the ith worker, which means that this worker can only complete a job with difficulty at most worker[i].
Every worker can be assigned at most one job, but one job can be completed multiple times.
For example, if 3 people attempt the same job that pays $1, then the total profit will be $3. If a worker cannot complete any job, his profit is $0.
What is the most profit we can make?
Example 1:
Input: difficulty = [2,4,6,8,10], profit = [10,20,30,40,50], worker = [4,5,6,7] Output: 100 Explanation: Workers are assigned jobs of difficulty [4,4,6,6] and they get profit of [20,20,30,30] seperately.
Notes:
1 <= difficulty.length = profit.length <= 100001 <= worker.length <= 10000difficulty[i], profit[i], worker[i]are in range[1, 10^5]
這道題給了我們一堆工作,每個工作有不同的難度,且對應着不同的利潤。現在還有一些工人,每個人能勝任工作的難度不同,題目說了沒人最多只能干一項工作,但是每個工作可以被多個人做。現在問我們這些工人能產生的最大利潤。題目中給了一個例子,但是這個例子會給我們一些錯覺,實際上difficulty數組不一定是有序的,而且可能有相同難度的工作對應不同的利潤。還有就是,難度大的工作不一定利潤就大,忽略了這些隱藏條件,很容易做錯。為了快速的知道每個工作的難度和其對應的利潤,我們可以建立難度和利潤的之間映射,由於前面說了,相同的難度可能有不同的利潤,所以我們希望難度映射到最高的利潤,所以每次都跟自身的映射值比較一下,保留較大值即可。同時,我們還希望工作的難度能排個序,這樣就可以根據工人的能力值來快速搜索能做的工作了,所以可以使用TreeMap。但是,還有個問題,前面說了難度大的工作不一定利潤就大,所以我們希望難度映射的利潤,是不大於其難度的工作的利潤中的最大值,所以我們還要遍歷一遍所有的映射,維護一個當前最大值cur,然后不斷的更新每個工作的利潤,同時也更新當前最大值。這些都建立好了后,之后就簡單了,我們遍歷每個工人,根據其能力值,來二分查第一個難度值大於該能力值的工作,可以用內置的 upper_bound 函數,如果結果第一個數字,那么我們將其前面一個難度的工作的利潤加入結果res即可,參見代碼如下:
解法一:
class Solution { public: int maxProfitAssignment(vector<int>& difficulty, vector<int>& profit, vector<int>& worker) { int res = 0, cur = 0; map<int, int> m; for (int i = 0; i < difficulty.size(); ++i) { m[difficulty[i]] = max(m[difficulty[i]], profit[i]); } for (auto &a : m) { a.second = max(a.second, cur); cur = a.second; } for (int i = 0; i < worker.size(); ++i) { auto it = m.upper_bound(worker[i]); if (it != m.begin()) { res += (--it)->second; } } return res; } };
我們也可以不用TreeMap,而是將難度和利潤組成pair,加入到一個數組中,那么就算相同的難度對應不同的利潤,也不會丟失數據。我們還是根據難度給所有的工作排個序,同時,我們按能力也給工人排個序,從能力低的工人開始分配工作,這樣我們只需要遍歷一次所有的工作,因為當能力低的工人分配了某個工作時,后面能力高的工人不需要再考慮之前的工作,因為工作已經按難度排好序了,只需要從當前位置繼續往后面匹配工作,我們可以使用一個全局變量i,當工人的能力值大於等於某個工作的難度時,我們用其來更新curMax,這里的curMax記錄當前工人能做的工作的最大利潤,這樣當工作遍歷完了,或者當前工作難度已經大於工人的能力值了時就停止,這樣curMax就是該工人能獲得的利潤,加入結果res,參見代碼如下:
解法二:
class Solution { public: int maxProfitAssignment(vector<int>& difficulty, vector<int>& profit, vector<int>& worker) { int res = 0, n = profit.size(), curMax = 0, i = 0; vector<pair<int, int>> jobs; for (int j = 0; j < n; ++j) { jobs.push_back({difficulty[j], profit[j]}); } sort(jobs.begin(), jobs.end()); sort(worker.begin(), worker.end()); for (int ability : worker) { while (i < n && ability >= jobs[i].first) { curMax = max(curMax, jobs[i++].second); } res += curMax; } return res; } };
我們還可以使用動態規划Dynamic Programming來做,建立一個一維的dp數組,這里的dp[i]表示能力為i的工人所能獲得的最大的利潤。dp數組大小初始化為100001,這是題目中給定的范圍,然后我們用給定的工作的難度和利潤來更新dp數組,跟解法一中的更新TreeMap有些像,要考慮相同的難度可能有不同的利潤,所以每次更新的時候要跟本身比較。之后就是從1開始更新dp本身,dp[i]跟前面一個dp值比較,取較大值更新dp[i],這個操作跟解法一中的那個更新TreeMap的映射值的操作是一樣的,都是為了避免難度高的工作利潤低。這里的dp數組cover了所有的能力值的范圍,這樣對於任何一個工人,我們可以根據其能力值馬上找出其可以獲得的最大利潤,而不用像解法一中要使用二分查找,我們犧牲了空間,換來了常數級的查找速度,參見代碼如下:
解法三:
class Solution { public: int maxProfitAssignment(vector<int>& difficulty, vector<int>& profit, vector<int>& worker) { int res = 0, n = profit.size(); vector<int> dp(100001); for (int i = 0; i < n; ++i) { dp[difficulty[i]] = max(dp[difficulty[i]], profit[i]); } for (int i = 1; i < dp.size(); ++i) { dp[i] = max(dp[i], dp[i - 1]); } for (int ability : worker) { res += dp[ability]; } return res; } };
參考資料:
https://leetcode.com/problems/most-profit-assigning-work/
https://leetcode.com/problems/most-profit-assigning-work/discuss/127133/Java-Solution-with-TreeMap
