题目描述
给了一个整数数组jobs表示工作,元素ei表示第i个工作需要花费的时间
给k个人,每个工作都需要分配,且每个只能给一个人
问如果要完成所有工作,求最短的工作时间?
f1-状态压缩+动态规划 |
基本分析
- 能看出来需要二进制压缩状态,这里n是工作量,k是人数,怎么定义dp定义好?因为给第i个人的分配工作的时候只和之前i-1个人被分配的工作有关,因此定义dp状态为f[i][j],表示给前i个人分配工作状态为j时候的最小时间
- 考虑怎么转移?加入给第i个人分配的工作是mask的一个子集sub,那么前i-1个人做的工作就是mask^sub,本次分配对应的时间就是max(f[i-1][mask^sub],value[i]);因为这个不一定是最合理的,我们需要枚举mask的子集sub,来让总和最小
- 考虑以上转移,是不是可以对value做预处理?可以遍历mask的子集sub,把sub对应的时间存起来
- 怎么考虑dp的初始状态?考虑只由第一个人做的时候,mask对应的时间就是我们上一步求的sub对应的时间。
- 转移的时候怎么考虑?
- (1)先遍历人i,因为i=0给了初值,且要看i-1的状态,i从1开始就行
- (2)再遍历状态sub,对应每个sub给一大的初始时间inf,再初始化他的子集last=sub
- (3)枚举sub的子集last,看不同的prev和last的组合时间是多少,选出最小的就是f[i][sub]的结果
- 以上复杂度很高,可以考虑怎么优化?
- (1)python的min和max比较复杂,需要写成直接比较值的形式
- (2)在函数之外,把mask的子集存起来,写成subsets[mask]的形式
- (3)在枚举子集的时候借用二分等,后面再细看
代码
subsets = [[] for _ in range(1<<12)]
for i in range(1<<12):
s = i
while s > 0:
subsets[i].append(s)
s = (s-1) & i
class Solution:
def minimumTimeRequired(self, jobs: List[int], k: int) -> int:
n = len(jobs)
mask = 1<<n
ssum = [0] * mask
for sub in range(mask):
for j in range(n):
if sub & (1<<j):
ssum[sub] = ssum[sub ^ (1<<j)] + jobs[j]
break
f = [[0]*mask for _ in range(k)]
f[0] = ssum.copy()
for i in range(1, k):
for j in range(1, mask):
last = j
mint = inf
for last in subsets[j]:
prev = j - last
curt = ssum[last] if ssum[last] > f[i-1][prev] else f[i-1][prev]
mint = mint if mint <= curt else curt
f[i][j] = mint
return f[-1][-1]
复杂度
时间:\(需要O(2^n)来预处理数组,dp有O(n \cdot 2^n种状态), 将状态看做集合,大小为k的集合有n \ cdot C_{n}^{k}2^k=3^n个, 总时间复杂度是O(n \cdot 3^n)\)
空间:\(O(n \cdot 2^n)\)
总结
这个题在写状态的时候需要考虑怎么转移的问题,这里线索第i个人要分配的任务需要看前i-1个人的分配情况,这样就建立起来了联系,把人放在第1维度,把对应的任务状态放在第2维度,f[i][j]表示前i个人完成任务j对应的最小时间。
这里需要考虑预处理,是因为在转移的时候,需要知道某个任务组合如果都给一个人做的时候,需要总共多少时间,这个可以提前累加起来。同时在累加的时候,由于子集是从小到大的,对于某个大的sub,肯定是可以找到一个低位的j,ssum[sub] = ssum[sub^(1<<j)] + jobs[j]是肯定成立的,之后就能break,去找下一个sub,这样节省时间。
给dp赋初值的时候,考虑到我们枚举人是需要往前看的,所以i要从1取,那么i=0的情况要初始化好,恰好i等于0时候就代表只派0号人去做,这个结果在ssum中刚好已经统计了。
常规转移的时候,先枚举人,再枚举状态,再枚举状态的子集。这个题python不好过,需要一些小技巧优化,前两个只是技巧,后面考虑二分优化需要再彻底了解下。