算法学习|背包问题

问题描述

有n件物品,有容量为c的背包
每件物品占的容量为\(\omega_{i}\) ;价值为\(v_i\)
问在容量限制下怎样选择才能带走最大价值的物品
根据物品的选择,还可以分为以下几种类型:
1.0-1背包问题
2.完全背包问题
3.多重背包问题
4.混合背包问题

一、.0-1背包问题

每件物品至多选一次

基础递归解决

时间复杂度为 \(O(2ⁿ)\)
每个物品选择或不选择,返回两个选择中最大价值

def ks_recursion(n , c , weights , values):
    if n == 0 or capacity == 0:
        return 0

    if weights[n-1] > capacity:
        return knapsack_recursive(weights, values, capacity, n-1)

    not_take = knapsack_recursive(weights, values, capacity, n-1)
    take = knapsack_recursive(weights, values, capacity - weights[n-1], n-1) + values[n-1]

    return max(not_take, take)

记忆递归

通过缓存中间结果,将时间复杂度优化为 \(O(n*c)\)
字典记录已经存在的计算结果,避免重复计算同一子问题

def ks_recursion_memory(n,c,w:list,v:list,memo:dict):
	if n <0 or c <=0:#
		return 0
	if (n,c) in memo:
		return memo[(n,c)]

	no_taken = ks_loop(n-1,c,w,v,memo)#
	take = 0#
	if c >= w[n]:
		take = ks_loop(n-1,c-w[n],w,v,memo) + v[n]#
	memo[(n,c)] = max(take,no_taken)
	return memo[(n,c)]

二维数组

复杂度:\(O(n * c)\)
对每个物品 i 和每个容量 w:
若当前物品重量 > 剩余容量:无法选择该物品,继承前 i-1 个物品的结果

dp[i][w] = dp[i-1][w]

若当前物品重量 ≤ 剩余容量:决策选或不选该物品,取最大价值。

dp[i][w] = max(
    dp[i-1][w],  
    dp[i-1][w - weights[i-1]] + values[i-1]  
)

最终结果在dp[n][c]

def ks_double_dim(n, c, w, v):
	dp = [[0] * (c + 1) for _ in range(n + 1)]
	for i in range(1, n + 1):  # i:每个物品
		for ca in range(1, c + 1):  # c:每个容量
			if w[i - 1] > ca:
				dp[i][ca] = dp[i - 1][ca]
			else:
				dp[i][ca] = max(dp[i - 1][ca], dp[i - 1][ca - w[i - 1]] + v[i - 1])##
	return dp[n][c]

一维数组

def ks_one(n,c,w,v):
	dp = [0]*(c+1)
	for i in range(n):
		for ca in range(c,w[i]-1,-1):
			dp[ca] = max(dp[ca],dp[ca-w[i]]+v[i])
	return dp[c]

若想求恰好填满容量C时最大价值,dp[0]初始化为0,其它初始化为负无穷即可

二、完全背包问题

每件物品可以重复选取

def unbounded_knapsack(weights, values, capacity):
    dp = [0] * (capacity + 1)
    for w in range(1, capacity + 1):           # 正序遍历容量
        for i in range(len(weights)):          # 遍历每个物品
            if weights[i] <= w:
                dp[w] = max(dp[w], dp[w - weights[i]] + values[i])
    return dp[capacity]

复杂度O(n*c)

三、多重背包问题

直接暴力循环

\(i\)个物品最大可选次数 \(k[i]\)
直接扩展完全背包复杂度\(O(n*k*W)\)

def multi_knapsack_naive(weights, values, counts, capacity):
    dp = [0] * (capacity + 1)
    for i in range(len(weights)):
        for _ in range(counts[i]):  # 将物品i拆分成k个独立物品,暴力循环
            for j in range(capacity, weights[i]-1, -1):  # 0-1背包
                dp[j] = max(dp[j], dp[j - weights[i]] + values[i])
    return dp[capacity]
n, m = map(int, input().split())
dp = [0] * (m + 1)
for i in range(1, n + 1):
    v, w, c = map(int, input().split())
    k = 1
    while k <= c:
        for j in range(m, k * v - 1, -1):
            dp[j] = max(dp[j], dp[j - k * v] + k * w)
        c -= k
        k += k
    if c > 0:
        for j in range(m, c * v - 1, -1):
            dp[j] = max(dp[j], dp[j - c * v] + c * w)
print(dp[m])

二进制拆分法

  1. 核心思想
    将数量 \(k\) 分解为 \(2^0, 2^1, ..., 2^m, remaining\) 的组合
    \(k\) 次选择的物品转换为 \(log₂k\) 个独立物品,每个新物品代表选择 \(2^m\) 个原物品
  2. 拆分步骤对每个物品$ i$,其数量为 \(s\)
    初始化 \(base = 1\)(从 \(2^0\) 开始拆分)
    循环拆分直到$ s >= base$
    \(base\) 个物品 \(i\) 打包成一个新物品,重量为 \(base * weight[i]\),价值为 \(base * value[i]\)
    处理剩余数量 \(s\)
    复杂度:\(O(n·logk·W)\)
def multi_knapsack_optimized(weights, values, counts, capacity):
    # 1. 二进制拆分生成新物品列表
    new_weights = []
    new_values = []
    for i in range(len(weights)):
        s = counts[i]
        base = 1
        while s >= base:
            new_weights.append(weights[i] * base)
            new_values.append(values[i] * base)
            s -= base
            base *= 2
        if s > 0:
            new_weights.append(weights[i] * s)
            new_values.append(values[i] * s)
    
    # 2. 转化为0-1背包问题
    dp = [0] * (capacity + 1)
    for i in range(len(new_weights)):
        for j in range(capacity, new_weights[i]-1, -1):  # 逆序更新
            dp[j] = max(dp[j], dp[j - new_weights[i]] + new_values[i])
    return dp[capacity]

四、混合背包问题

混合背包是上面三个背包问题的集大成者
可以考虑引入标记参数分类处理

def hybrid_knapsack(items, capacity):
    """
    items格式: [(type, weight, value, count)]
    type=0: 0-1背包 count = None
    type=1: 完全背包
    type=2: 多重背包 count
    """
    dp = [0] * (capacity + 1)
    
    for item in items:
        typ, weight, value = item[0], item[1], item[2]
        if typ == 0:
            # 0-1背包
            for j in range(capacity, weight - 1, -1):
                dp[j] = max(dp[j], dp[j - weight] + value)
        elif typ == 1:
            # 完全背包
            for j in range(weight, capacity + 1):
                dp[j] = max(dp[j], dp[j - weight] + value)
        elif typ == 2:
            # 多重背包:二进制拆分后按0-1处理
            count = item[3]
            # 二进制拆分
            base = 1
            s = count
            while s >= base:
                w = weight * base
                v = value * base
                for j in range(capacity, w - 1, -1):
                    dp[j] = max(dp[j], dp[j - w] + v)
                s -= base
                base *= 2
            if s > 0:
                w = weight * s
                v = value * s
                for j in range(capacity, w - 1, -1):
                    dp[j] = max(dp[j], dp[j - w] + v)
    return dp[capacity]
posted @ 2025-03-16 16:17  lumiere_cloud  阅读(35)  评论(0)    收藏  举报