dp基础之背包问题

问题一:有N个物品,重量为A[0]...A[N-1],有一个容量为M(M是一个正整数)。
问:最多能带走多重的物品。
例:A = [2,3,5,7]
    M = 10
输出:10(2,3,5)

问题分析:
要求N个物品能否拼出重量W(W = 0,1...M),需要知道前N-1个物品能否拼出重量W(W = 0,1...M)

考虑最后一个物品(A[N-1])放不放入进入背包
    情况1:如果前N-1个物品能拼出重量W,则前N个物品必然也能拼出重量W
    情况2:如果前N-1个物品能拼出重量W-A[N-1],则前N个物品就能拼出重量W,加上物品A[N-1]即可

子问题:
设:f[i][w]表示物品前i个能拼出重量w(True/False)
        f[i][w] = f[i-1][w] or f[i-1][w-A[i-1]]
                  不放入A[i-1]    or 放入A[i-1]
初始条件:
         0个物品可以拼出重量0
         f[0][0] = True
         0个物品不能拼出大于0的任何重量
         f[0][1...M] = False

边界情况:
         f[i][w-A[i-1]] w>=A[i-1]时使用

计算顺序:
         初始化
         f[0][0..M]
         前1个物品能拼出:f[1][0]...f[1][M]
                             .
                             .
         前N个物品能拼出:f[N][0]...f[N][M]

时间复杂度:O(MN),空间复杂度O(MN),优化后可以达到O(M)

代码及注释如下:

def backpack(A,M):
    N = len(A)
    if N == 0:
        return 0
    f = [[False for i in range(M+1)] for j in range(N+1)]
    #初始化,f[0][0] = True ;f[0][1...M] = False
    f[0][0] = True
    for i in range(1,N+1):
        
        for j in range(0,M+1):
            #f[i][w]表示物品前i个能拼出重量w(True/False)
            #f[i][w] = f[i-1][w] or f[i-1][w-A[i-1]](w>=A[i-1])
            f[i][j] = f[i-1][j]
            if j >= A[i-1] :
                f[i][j] = f[i-1][j] or f[i-1][j-A[i-1]]
    #返回前N个物品能拼出最大的重量,肯定不会超过M,因为最大就是M
    for j in range(M+1)[::-1]:
        if f[N][j]:
            return j
            
A = [2,3,7,5]
M = 10
print(backpack(A,M))
#结果:10

问题二:假设每个物品只有一个(每个物品只能用一次),问一共有多少种方式正好凑成重量Target?
例:
A = [1,2,3,3,7],Target = 7
输出:2(1,3,3;7)

问题分析:
如果知道这N个物品有多少种方式拼出0...Target,也就得到了答案
确定状态:需要N个物品有多少种方式拼出重量W(W = 0...Target)
最后一步:考虑第N个物品A[N-1](最后一个物品)是否进入背包

    case1: 不进入,用前N-1个物品拼出W
    case2: 进入,前N-1个物品能拼出W-A[N-1],加上最后一个A[N-1],正好拼出W
    现在要求的是方式数,
    case1的方式数+case2的方式数 = 用前N个物品拼出W的方式数
转移方程:设f[i][w]表示用前i个物品能拼出w的方式数
    f[i][w] = f[i-1][w]+f[i-1][w-A[i-1]]
初始条件:
    #0个物品有一种方式拼出0

    #0个物品不能拼出大于0的重量
    f[0][0] = 1
    f[0][1]...f[0][Target] = 0
 
边界情况:
    f[i-1][w-A[i-1]]中,w>=A[i-1]
计算顺序:
         初始化
         f[0][0..Target]
         前1个物品能拼出:f[1][0]...f[1][Target]
                             .
                             .
         前N个物品能拼出:f[N][0]...f[N][Target]
答案是f[N][Target]

时间复杂度:O(N*Target),空间复杂度O(N*Target),优化后可以达到O(Target)

代码及注释如下:

def backpackII(A,Target):
    #优化空间
    N = len(A)
    if N == 0:
        return 0
    f = [0 for i in range(Target+1)]
    #初始化f[0] = 1,f[1...Target] = 0
    for i in range(Target+1):
        f[i] = 1 if i == 0 else 0
    
    for i in range(N+1):
        #优化空间门卫了不覆盖掉有用的值,我们从后往前算j从Target到0
        #把原来的f[j]覆盖掉,
         for j in range(Target+1)[::-1]:
                #f'[j] = [j]+f[j-A[i-1]]
                #其实除了j>=A[i-1]为了减少计算,还有A[i-1] <= j <= sum(A[0]+...A[i-1])
                if j >= A[i-1] and j <= sum(A[0:i]):
                    f[j] += f[j-A[i-1]]
    return f[Target]
A = [1,2,3,3,7]
Target = 7
print(backpackII(A,Target))
#结果:2

问题三:假设每个物品只有一个(每个物品可以用人任意次),问一共有多少种方式正好凑成重量Target?
例:
A = [1,2,4],Target = 4
输出:6(1,1,1,1; 2,2,2; 4; 1,1,2;1,2,1;2,1,1)(顺序不同也算不同)

问题分析:
所有正确组合中,总重量都是Target
在总重量是Target的时,最后一个物品重是k,则前面物品重为Target-k
k的取值无非是A[0]..A[N-1]中的一个
如果最后一个物品是A[i],则要求有多种组合拼成Target-A[i]

转移方程:设f[i]表示有多少种方式拼出重量i
    f[i] = f[i-A[0]] + f[i-A[1] + ... + f[i-A[N-1]]
初始条件:
    有1种方式拼出重量0
    f[0] = 1
    若i<A[j],则对应的f[i-A[j]]不加入f[i]
 
计算顺序:
         f[0],...,f[Target]
                             .
答案是f[Target]

时间复杂度:O(N*Target),空间复杂度O(Target)

代码及注释如下:

def backpackIII(A,Target):
    f = [0 for i in range(Target+1)]
    f[0] = 1
    for i in range(1,Target+1):
        f[i] = 0
        for j in range(len(A)):
            if i >= A[j]:
                f[i] += f[i-A[j]]
    return f[Target]
A = [1,2,4]
Target = 4 
print(backpackIII(A,Target))
#答案:6

问题IV:如果要打印出问题III里拼出arget重量的一种方式,如何解?

代码及注释如下:

def printbackpackIV(A,Target):
    #打印出拼成Target的方式\
    f = [0 for i in range(Target+1)]
    f[0] = 1
    #pai[i]表示至少有一种方式拼成重量i,且最后一个物品是pai[i]
    pai = [-1 for i in range(Target+1)]
    for i in range(1,Target+1):
        f[i] = 0
        for j in range(len(A)):
            if i >= A[j]:
                f[i] += f[i-A[j]]
                #至少有一种方式拼出重量i
                if f[i-A[j]] >=1 :
                    #pai[i]表示pai[i]表示至少有一种方式拼成重量i,且最后一个物品是pai[i]
                    #如果至少有一种方式能够拼出重量i且最后一个物品是A[j],就是pai[i] = A[j]
                    pai[i] = A[j]
    if f[Target] >= 1:
        #要拼出重量Target
        i = Target
        print(i)
        while i != 0 and pai[i] != -1:
            #表示至少有一种方式拼成重量i,且最后一个物品是pai[i]
            print(pai[i])
            #现在的重量是i,且最后一个物品是物品pai[i],
            #去掉最后一个物品pai[i],之前的重量是i-pai[i],即减去最后一个物品的重量
            i = i-pai[i]
    return f[Target]
B = [5,7,13,17]
Target = 32
printbackpackIV(B,Target)
#
32
17
5
5
5
Out[30]:
22

问题五:N个物品重量为A[0]...A[N-1],价值分别V[0]...V[N-1],有一个容量为M的背包,问最多能带走多大价值的物品?
例:
A = [2,3,5,7],V = [1,5,2,4],M = 11
输出:9(物品1和物品3,5+4 = 9,重量 为2+7=10 < 11)

问题分析:和前面的题目类似,需要知道N个物品:
    能否拼出重量W(W = 0...M)
    对于每个重量W,最大总价值是多少
考虑最后一步:最后一个物品(物品A[N-1],价值V[N-1])能否进入背包:
    情况1:不进入,前N-1个物品能拼出重量W,最大总价值V,则前N个物品必然也能拼出重量W且最大总价值为V
    情况2:进入,前N-1个物品能拼出重量W-A[N-1],最大总价值V,则前N个物品(即加入物品A[N-1])拼出重量W且最大总价值为V+V[N-1]
子问题:要求前N个物品能不能拼出重量W(W = 0...M),以及拼出重W时的最大总价值
        则需要知道前N-1个物品能不能拼出重量W(W = 0...M),以及重量W此时的最大总价值
        
设:f[i][w]表示前i个物品拼出重量w时的最大总价值(我们用-1表示不能拼出)(跟问题一有点类似)

    f[i][w] = max{f[i-1][w] ,  f[i-1][w-A[i-1]] + V[i-1] ( w >= A[i-1] && f[i-1][w-A[i-1]] !=- 1 ) }
              max{不进入,前N-1个物品能拼出重量W,最大总价值V ;进入,前N-1个物品能拼出重量W-A[N-1],最大总价值V,则前N个物品(即加入物品A[N-1])拼出重量W且最大总价值为V+V[N-1] }(w> =A[i-1](能够拼出)且 f[i-1][w-A[i-1]] !=- 1(所拼出的重量w>=A[i-1],))

初始条件:
    f[0][0]...f[N][0] = 0,前0...N个物品能拼出重量0,且最大价值是0。
    f[0][1]...f[0][M] = -1,前1个物品不能拼出重量大于0,我们用-1表示。

计算顺序:
    f[0][0]...f[0][M]
    .
    .
    .
    f[N][0]...f[N][M]

答案:max(f[N][w] and f[N][w] != -1(w = 0...M))

时间复杂度O(N*W),空间复杂度O(N*W),优化后达到O(M)

详细解释一下空间优化的过程,转移方程如下:
     f[i][w] = max{f[i-1][w] ,  f[i-1][w-A[i-1]] + V[i-1]}
     
本来计算
    f[0][0]...f[0][M]
    f[1][0]...f[0][M]
    .
    .
    .
    f[i-1][0]...f[i-1][M]
    f[i][0]...f[i][M]
    .
    .
    .
    f[N][0]...f[N][M]
计算
    f[i][w] = max{f[i-1][w] ,  f[i-1][w-A[i-1]] + V[i-1]}时,我们让w按照M...0的顺序,
    即先算f[i][M],再算f[i][M-1]...f[i][0]发现算完f[i][M]时,上一行的f[i-1][M]之后就不用了,
    所以,我们直接把f[i][M]存放在f[i-1][M],即覆盖上一行(i-1行)的同一列(M列)的位置f[i-1][M],
    程序中可以这样:新的f[w]覆盖旧(上一行)的f[w]
        f[w] = max(f[w],f[w-A[i-1]] + V[i-1])
    因为计算f[i][M-1]时不会用到f[i-1][M],只会用到f[i-1][M]前面的数。
    所以只要用一个一维数组即可,空间复杂度优化到O(M),关键点在于我们计算f[w] = max(f[w],f[w-A[i-1]] + V[i-1])时,让w从后往前(从M...0)计算即可。

代码及注释如下:

def backpackV(A,V,M):
    #优化空间后的代码,用一维数组f[w]代替二维数组f[i][w]
    N = len(A)
    if N == 0:
        return 0
    #初始化,f[0] = 0,f[1]...f[M] = -1
    f = [-1 for i in range(M+1)]
    f[0] = 0
    
    for i in range(1,N+1):
        for  w in range(M+1)[::-1]:
            #f[i][w] = max{f[i-1][w] , f[i-1][w-A[i-1]] + V[i-1] ( w >= A[i-1] && f[i-1][w-A[i-1]] != -1 ) }
            if w >= A[i-1] and f[w-A[i-1]] != -1:
                f[w] = max(f[w],f[w-A[i-1]] + V[i-1])
    #return max(f if max(f) != -1
    #其实没必要,因为至少会存在f[0] = 0。故直接返回max(f)
    return max(f)
A = [2,3,5,7]
V = [1,5,2,4]
M = 11
print(backpackV(A,V,M))
#答案:9

问题六:N种物品,每种物品的重量是A[0]...A[N-1],价值分别V[0]...V[N-1],有一个容量为M的背包,
这里每个物品可以有任意个,问最多能带走多大价值的物品?
例:
A = [2,3,5,7],V = [1,5,2,4],M = 11
输出:15(3个物品1,价值3*5 = 15,重量3*3=9 < 11)


问题分析:和问题四不同点在于,A[i]可以有任意个:
因此设:f[i][w]表示前i种物品拼出重量w时的最大总价值(我们用-1表示不能拼出重量w)

    f[i][w] = max{f[i-1][w] , f[i-1][w-kA[i-1]] + kV[i-1]}(k>=0)
    #如果上面 k = 1 就和问题四一样
f[i][w] = max{f[i-1][w] , f[i-1][w-1*A[i-1]]+1*V[i-1] , f[i-1][w-2*A[i-1]]+2*V[i-1], ...}

但仔细观察,发现max{f[i-1][w-1*A[i-1]]+1*V[i-1] , f[i-1][w-2*A[i-1]]+2*V[i-1], ...} = f[i][w-A[i-1]]
因此:    
    f[i][w] = max{f[i-1][w],f[i][w-A[i-1]]+V[i-1]}
    
初始条件:

    f[0][0]...f[N][0] = 0,前0...N种物品能拼出重量0,且最大价值是0。
    f[0][1]...f[0][M] = -1,前1种物品不能拼出重量大于0,我们用-1表示。

计算顺序:
    f[0][0]...f[0][M]
    .
    .
    .
    f[N][0]...f[N][M]

答案:max(f[N][w] and f[N][w] != -1(w = 0...M))

时间复杂度O(N*W),空间复杂度O(N*W),优化后达到O(M)


这里也详细解释一下空间优化:我们的转移方程是
    f[i][w] = max{f[i-1][w],f[i][w-A[i-1]]+V[i-1]}
    同样,跟问题四里的空间优化一样,考虑:
本来计算
    f[0][0]...f[0][M]
    f[1][0]...f[0][M]
    .
    .
    .
    f[i-1][0]...f[i-1][M]
    f[i][0]...f[i][M]
    .
    .
    .
    f[N][0]...f[N][M]

   f[i][w] = max{f[i-1][w],f[i][w-A[i-1]]+V[i-1]}
 对于第i行和第i-1行,当计算f[i][M]时,只跟前一行(i-1行)同一列(M列)f[i-1][M]和同一行(i行)前面的w-A[i-1]列f[i][w-A[i-1]]有关。
如果我们还是跟问题四一样从后往前计算,则需要两行来记录i-1行和i行,不能压缩到1行。因为计算f[i][M]跟i行和i-1行都有关系,当从后往前计算时,不能直接覆盖。那如果我们从前往后计算呢?
例如:我们计算f[i][0],f[i][1]...f[i][M],随便几个例子,我们计算f[i][6]
    f[i][6] = max(f[i-1][6],f[i][6-A[i-1]]+V[i-1])
    也就是说,计算完f[i][6]时,i-1行的6列位置f[i-1][6]这个数后面不会再用到了,
    故我们可以用f[i][6]直接覆盖f[i-1][6],因此也可以只要开一个以为数组,只不过计算顺序从0...到M,即可,代码只要在问题四的代码上把w的循环顺序(M...0)改成(0...M)即可。

代码及注释如下:

def backpackV(A,V,M):
    #优化空间后的代码,用一维数组f[w]代替二维数组f[i][w]
    N = len(A)
    if N == 0:
        return 0
    #初始化,f[0] = 0,f[1]...f[M] = -1
    f = [-1 for i in range(M+1)]
    f[0] = 0
    
    for i in range(1,N+1):
        ############修改w循环顺序即可###########
        for  w in range(M+1):
            #f[i][w] = max{f[i-1][w] , f[i-1][w-A[i-1]] + V[i-1] ( w >= A[i-1] && f[i-1][w-A[i-1]] != -1 ) }
            if w >= A[i-1] and f[w-A[i-1]] != -1:
                f[w] = max(f[w],f[w-A[i-1]] + V[i-1])
    #return max(f if max(f) != -1
    #其实没必要,因为至少会存在f[0] = 0。故直接返回max(f)
    return max(f)
A = [2,3,5,7]
V = [1,5,2,4]
M = 10
print(backpackV(A,V,M))
#答案:15

猜你喜欢

转载自blog.csdn.net/lerry13579/article/details/84260603