动态规划(二)

整数01背包问题

用背包去装载物品,背包容量为W,一共n件物品,第i个物品价值为 v i v_{i} vi,重 w i w_{i} wi,目标:找到 x i x_{i} xi使得所有 x i = ( 0 , 1 ) x_{i}=(0,1) xi=(0,1),即:
m a x ∑ i = 1 n v i x i , ∑ i = 1 n w i x i ⩽ W max\sum_{i=1}^{n}v_{i}x_{i},\sum_{i=1}^{n}w_{i}x_{i}\leqslant W maxi=1nvixi,i=1nwixiW

分析

先暴力递归,假设 F ( i , s ) F(i,s) F(i,s)表示选择到第i件物品获得的最大价值,s代表当前背包已经达到的重量,类似上一篇的商店打劫问题,倒序来选,从最后一个物品开始,当然, F ( i , s ) F(i,s) F(i,s)中的s初始为0;
因此,其中一个边界条件应为: s ⩾ W s\geqslant W sW
现在面对第 i d x idx idx个物品时,可以选择它进入背包,也可以不选择它;
如果选择了它,则从下一个物品 i d x − 1 idx-1 idx1开始的最大价值为 F ( i d x − 1 , s + w i d x ) F(idx-1,s+w_{idx}) F(idx1,s+widx)
如果不选择它,则从下一个物品 i d x − 1 idx-1 idx1开始的最大价值为 F ( i d x − 1 , s ) F(idx-1,s) F(idx1,s)
因此,从 i d x idx idx开始选择的最大价值为:
F ( i d x , s ) = m a x { F ( i d x − 1 , s + w i d x ) + v i d x , F ( i d x − 1 , s ) + 0 } F(idx,s)=max\left \{ F(idx-1,s+w_{idx})+v_{idx},F(idx-1,s) +0\right \} F(idx,s)=max{F(idx1,s+widx)+vidx,F(idx1,s)+0}

暴力递归

暴力递归实现如下:

import numpy as np

n=15
w=np.array([4, 3, 6, 8, 5, 5, 7, 2, 3, 5, 3, 2, 7, 1, 7])
v=np.array([0, 0, 3, 1, 2, 2, 1, 5, 4, 3, 5, 1, 5, 2, 3])
W=30

def search(idx,s):
    #超重
    if s>W:
        return 0
    #选完第一个没有物品可以再选
    elif idx<0:
        return 0
    else:
        return max(search(idx-1,s),search(idx-1,s+w[idx])+v[idx])

#从最后一个物品开始选,从0计数,所以idx=n-1
search(n-1,0)

在不用动态规划时,暴力递归对每次max要进行比较(计算两次),每个比较的子分支会延伸出新的比较,直到深入计算完n个物品,所以时间复杂度为 O ( 2 n ) O(2^{n}) O(2n)

用动态规划改进

开辟空间保存每一步的最优结果,数组的大小取决于问题一共有多少状态,此时需要注意一下,问题为整数的01背包问题,整数指的是每个物品的重量为整数;
在暴力递归中,状态有两个维度,一个维度反映物品的序号 i d x idx idx,另一个维度反映背包当前的重量 s s s,可见,如果物品重量是连续值,将无法构造有限大小的数组去保存每个状态对应的最优结果;
根据以上分析,开辟数组如下:

result=-1*np.ones((n,W+1))

完整动态规划改进如下:

#用动态规划改进
import numpy as np

n=15
w=np.array([4, 3, 6, 8, 5, 5, 7, 2, 3, 5, 3, 2, 7, 1, 7])
v=np.array([0, 0, 3, 1, 2, 2, 1, 5, 4, 3, 5, 1, 5, 2, 3])
W=30

#可见,只有物品重量为整数才能构造有限的数组实现动态规划
result=-1*np.ones((n,W+1))

def dpsearch(idx,s):
    #超重
    if s>W:
        return 0
    #选完第一个后没有物品可以再选
    elif idx<0:
        return 0
    
    elif result[idx][s]>=0:
        return result[idx][s]
    else:
        result[idx][s]=max(dpsearch(idx-1,s),dpsearch(idx-1,s+w[idx])+v[idx])
        return result[idx][s]

#从最后一个物品开始选,从0计数,所以idx=n-1
dpsearch(n-1,0)

空间复杂度:随着规模增大,result会相应增大,所以空间复杂度为 O ( n W ) O(nW) O(nW)
时间复杂度:result内的每个状态只计算一次,所以时间复杂度为 O ( n W ) O(nW) O(nW)

类似问题coin change

问题分析
现在有coins=[1,2,5]三种面值的硬币,每种面值的硬币数量不限,目标值amount=11,要求用最少数量的硬币凑齐amount,比如(5,5,1),即最少3个硬币能凑够amount;
这个问题类似整数01背包问题,假设假设 F ( i d x , a m o u n t ) F(idx,amount) F(idx,amount)可以返回在coins三种面值中,从 c o i n s [ i d x ] coins[idx] coins[idx]面值开始找硬币,最后凑够amount的最少硬币数,比如:
从coins中最后一种面值开始搜索, i d x = 2 idx=2 idx=2,即从coins[2]的5元开始搜索;
如果选择了coins[idx],由于每种面值的硬币数量不限,所以下次选择还是从面值coins[idx]开始,因此,以下一次选择开始的最少硬币数为 F ( i d x , a m o u n t − c o i n s [ i d x ] ) F(idx,amount-coins[idx]) F(idx,amountcoins[idx])
如果不选coins[idx],下次选择必然只能来到coins[idx-1],所以,以下一次选择开始的最少硬币数为 F ( i d x − 1 , a m o u n t ) F(idx-1,amount) F(idx1,amount)
综上分析,在面对第 i d x idx idx种硬币时的最少硬币数为:
F ( i d x , a m o u n t ) = m i n { F ( i d x , a m o u n t − c o i n s [ i d x ] ) + 1 , F ( i d x − 1 , a m o u n t ) } F(idx,amount)=min\left \{ F(idx,amount-coins[idx])+1,F(idx-1,amount) \right \} F(idx,amount)=min{F(idx,amountcoins[idx])+1,F(idx1,amount)}
递归实现
在实际实现时,需要注意一个技巧:
需要记录不合法的状态,因为有时候coins不能刚好凑齐amount,此时问题是无解的;
如果要获得最小值,非法值应该设为无穷大,求最大值时,非法值设为无穷小,这样才可以确保非法值不影响递推公式的正确性;
递归实现如下:

coins=[1,2,5]
amount=11

def coinchange(idx,amount,coins):
    #记录不合法的状态,因为有时候coins不能刚好凑齐amount
    #问题是无解的
    
    #要求min,非法值设为无穷大,求max,非法值设为无穷小,这样可以确保非法值不影响递推公式的正确性
    maxvalue=1000000
    if amount<0 or idx<0:
        return maxvalue

    # amount=0,不需要硬币
    elif amount==0:
        return 0
    else:
        return min(coinchange(idx,amount-coins[idx],coins)+1,coinchange(idx-1,amount,coins))

coinchange(len(coins)-1,amount,coins)

使用动态规划改进
真正的状态有两个维度,一个是硬币的面值类别idx,另一个是还需要拼凑的值amount,所以需要开辟二维数组保存中间最优结果;
可以看出,与整数01背包问题类似,coin change的amount应当是整数才能构建空间保存状态结果;
动态规划改进如下:

#使用动态规划改进
import numpy as np

coins=[1,2,5]
amount=11

result=-1*np.ones((len(coins),amount+1))

def dpcoinchange(idx,amount,coins):
    #记录不合法的状态,因为有时候coins不能刚好凑齐amount
    #问题是无解的
    
    #要求min,非法值设为无穷大,求max,非法值设为无穷小,这样可以确保非法值不影响递推公式
    maxvalue=1000000
    if amount<0 or idx<0:
        return maxvalue

    # amount=0,不需要硬币
    elif amount==0:
        return 0
    
    elif result[idx][amount]>=0:
        return result[idx][amount]
    else:
        result[idx][amount]=min(dpcoinchange(idx,amount-coins[idx],coins)+1,
                                dpcoinchange(idx-1,amount,coins))
        return result[idx][amount]

dpcoinchange(len(coins)-1,amount,coins)

LCS最长公共子序列

其他算法-LCS中,已经提到过算法思想与递归实现;
假设x有m个字符,y有n个字符,记 x i x_{i} xi为x的i前缀,而定义 F ( x m , y n ) F(x_{m},y_{n}) F(xm,yn)可以返回两字符串m前缀和n前缀的LCS,则有以下递推公式:
如果 x [ m ] = y [ n ] x[m]=y[n] x[m]=y[n]
F ( x m , y n ) = F ( x m − 1 , y n − 1 ) + 1 F(x_{m},y_{n})=F(x_{m-1},y_{n-1})+1 F(xm,yn)=F(xm1,yn1)+1
否则: F ( x m , y n ) = m a x [ F ( x m − 1 , y n ) , F ( x m , y n − 1 ) ] F(x_{m},y_{n})=max[F(x_{m-1},y_{n}),F(x_{m},y_{n-1})] F(xm,yn)=max[F(xm1,yn),F(xm,yn1)]

#最长公共子序列LCS
"""
x有m个字符
y有n个字符
记xi为x的i前缀

如果x[m]=y[n]
则F(xm,yn)=F(x_{m-1},y_{n-1})+1

否则
F(xm,yn)=max{F(x_{m-1},y_{n}),F(x_{m},y_{n-1})}
"""

x=[1,3,4,5,5]
y=[2,4,5,5,7,6]

def lcs(x,y):
    m=len(x)-1
    n=len(y)-1
    
    #边界条件
    if m<0 or n<0:
        return 0
    
    #注意切片左闭右开
    elif x[m]==y[n]:
        return lcs(x[:m],y[:n])+1
    else:
        return max(lcs(x[:m],y[:n+1]),lcs(x[:m+1],y[:n]))
    
lcs(x,y)

同样,这个问题可以用动态规划改进,状态实际上一个是x字符串的m前缀,一个是y字符串的n前缀,可以用二维数组 r e s u l t m , n result_{m,n} resultm,n保存不同前缀字符串的LCS长度:

#用动态规划改进
import numpy as np

x=[1,3,4,5,5]
y=[2,4,5,5,7,6]

result=-1*np.ones((len(x),len(y)))

def dplcs(x,y):
    m=len(x)-1
    n=len(y)-1
    
    #边界条件
    if m<0 or n<0:
        return 0
    
    elif result[m][n]>=0:
        return result[m][n]
    #注意切片左闭右开
    elif x[m]==y[n]:
        result[m][n]=dplcs(x[:m],y[:n])+1
        return result[m][n]
    else:
        result[m][n]=max(dplcs(x[:m],y[:n+1]),dplcs(x[:m+1],y[:n]))
        return result[m][n]
    
dplcs(x,y)

TSP旅行商问题

旅行商问题即Traveling Salesman Problem,简称TSP,问题描述:一个商人要不重复访问N个城市,允许从任意城市出发,在任意城市结束,现在已知任意两城市之间的道路长度,求城市访问序列,使商人走过的路程最短;
举个例子,现在共有4个城市{1,2,3,4},如果从城市3出发并遍历城市,最短路径的顺序是:3->4->1->2;
TSP是NP问题(非确定性多项式问题)

关于P问题和NP问题

时间复杂度

首先,时间复杂度表示一个算法运行得到想要的解所需的计算工作量,反映了当输入量接近无穷时,算法所需工作量的变化快慢程度;
比如对于同一个问题,有两种算法,一个时间复杂度是 O ( n 2 ) O(n^{2}) O(n2),另一个的时间复杂度是 O ( 2 n ) O(2^{n}) O(2n),如果输入数据只是1000个,不能体现出两者的差距,但当数据增加到百万时,两者的开销将会十分明显, O ( 2 n ) O(2^{n}) O(2n)算法的时间消耗远大于 O ( n 2 ) O(n^{2}) O(n2)算法;

P问题

对于一个问题,可以找到一种时间复杂度为多项式比如 O ( n 2 ) O(n^{2}) O(n2)的算法来求解,就称问题是一个存在多项式时间算法的问题,即P问题,P指polynominal(多项式);
回到时间复杂度部分的例子,我需要研究一个问题是否存在多项式时间算法。而且我只在乎一个问题是否存在多项式时间算法,因为一个时间复杂度比多项式时间算法还要复杂的算法研究起来没有任何实际意义。

NP问题

NP问题指,能在多项式时间内验证得出一个正确解的问题;(NP即Nondeterministic polynominal,非确定性多项式);
因为存在多项式时间算法的问题,总能在多项式时间内验证出结果,所以P问题是NP问题的子集。
关于在多项式时间内验证正确解,比如TSP弱化一下,只要路径小于某个值就算完成任务;暴力枚举的时间复杂度为 O ( n ! ) O(n!) O(n!),但如果用猜测的方式,猜了几次后运气很好就得到了一条正确的路径,但并不是每次都能运气好猜到正确路径;
换句话说,我能在多项式的时间内验证并得到问题的正确解,但是我不知道该问题是否存在一个多项式时间算法;注意:“不知道"不等同于"不存在”;

七大千禧难题之一

“千僖难题”之一:P (确定性多项式算法)与NP (非确定性多项式算法);即P=NP?
如果 P = N P P=NP P=NP,则意味着,每一个NP问题都可以转化成P,也就是每一个难题最终可以变成一个简单命题,让计算机可以快速求解;
如果 P ≠ N P P\neq NP P=NP,则意味着,很多NP问题无法简化成P,也就是计算机只能暴力求解。

TSP分析

城市之间的距离保存类似于邻接矩阵,比如distance:
d i s t a n c e [ i ] [ j ] distance[i][j] distance[i][j]即为城市 i i i到城市 j j j的距离;
假设 F ( i d x , v i s i t ) F(idx,visit) F(idx,visit)可以返回从idx城市出发的最短路径, v i s i t visit visit是一个一维数组,元素个数等于城市数 N N N v i s i t visit visit记录了被访问过的城市,如果访问过,记为1,否则为0;
则从城市 i d x idx idx开始的最短路径等于:
1.先得到集合:
{从 i d x idx idx到其他城市的路径,加从其他城市开始的最短路径};
2.在集合中选出路径最短的元素;
所以得到递推公式:
F ( i d x , v i s i t ) = m i n { d i s t a n c e [ i d x ] [ i ] + F ( i , v i s i t ) } F(idx,visit)=min\left \{ distance[idx][i]+F(i,visit) \right \} F(idx,visit)=min{distance[idx][i]+F(i,visit)}
其中, i i i N N N个城市的遍历;

暴力递归

假设 N = 4 N=4 N=4,城市之间的距离为:

array([[0, 3, 1, 2],
       [3, 0, 2, 1],
       [1, 2, 0, 2],
       [2, 1, 2, 0]])

注意:由于 v i s i t visit visit是全局的,集合中各城市 i i i的最短路径之间比较应当为平等关系,所以在集合的下一个元素计算前,需要复原 v i s i t [ i ] = 0 visit[i]=0 visit[i]=0
暴力递归实现如下:

#旅行商问题 Traveling Salesman Problem

import numpy as np

N=4

# distance[i][j]->i到j的距离
"""
distance=np.ones((N,N)) 
# cityA到cityA不需要路程
for i in range(N):
    distance[i][i]=0
"""

#array([[0, 3, 1, 2],
#       [3, 0, 2, 1],
#       [1, 2, 0, 2],
#       [2, 1, 2, 0]])

distance=np.array([[0,3,1,2],[3,0,2,1],[1,2,0,2],[2,1,2,0]])
    
#判断是否已经访问过该城市
visit=np.zeros(N)

def tsp(idx,visit:"访问过的城市")->"返回从idx出发的最短路径":
    #已经访问满了(边界)
    if np.sum(visit)==N:
        return 0
        
    mindistance=100000
    #依次比较idx到哪个城市路径最短
    for i in range(N):
        # 城市i未被访问过
        if visit[i]==0:
            #现在i已经被访问
            visit[i]=1

            t=distance[idx][i]+tsp(i,visit)
            
            if t<mindistance:
                mindistance=t
            #由于visit是全局的,各城市与idx的比较为平等关系
            #所以需要复原到visit[i]=0
            visit[i]=0

    return mindistance

"""
时间复杂度为O(n!)
"""

distance=np.array([[0,3,1,2],[3,0,2,1],[1,2,0,2],[2,1,2,0]])

#从城市0开始访问,所以先记录visit[0]=1
visit[0]=1
tsp(0,visit)

在调用 t s p ( i , v i s i t ) tsp(i,visit) tsp(i,visit)前,需要先在 v i s i t visit visit中记录初始访问的城市 i i i,即: v i s i t [ i ] = 1 visit[i]=1 visit[i]=1
在该实现中,从最开始的4个城市进行遍历,只要选出一个城市, v i s i t visit visit中就少了一个下次可以访问到的城市,所以时间复杂度为 O ( n ! ) O(n!) O(n!)

用动态规划改进

暴力递归的时间复杂度太高,想到用动态规划优化求解,但发现visit对应的状态难以表达,这导致数组的设计有些困难;
简单地想,会把visit数组每个元素作为一个新维度,与idx构建多维数组,这会导致空间复杂度过高,而且visit每个元素也就两种值(0和1),这会导致过度空间浪费;所以借助状态压缩解决这个问题

状态压缩

所谓状态压缩就是将visit数组映射到一个数值,这可以降低空间复杂度:

visit[1,0,1,0]=>b(1010)=>int 10,把一个数组空间压缩到一个整数

由于visit的特殊构造,可以把每种状态对应到二进制码,从而对应到一个数值;将0,1构成的列表转为10进制数的实现如下(具体参考python记事本):

"""
用于状态压缩
"""
def arraytoint(binarray)->'int':
    import functools
    #偏函数
    bintoint=functools.partial(int,base=2)
    
    return bintoint("".join([str(i) for i in binarray]))

动态规划实现

城市 i d x idx idx可以从1到 N N N v i s i t visit visit的状态从[0,0,0,0]到[1,1,1,1],从而有 2 N 2^{N} 2N种状态;
现在就能轻松开辟二维数组,这个数组已经可以涵盖TSP的所有状态:

result=-1*np.ones((N,2**N))

所以,使用动态规划的改进如下:

#用动态规划改进(这里要用到状态压缩,主要是减少了空间复杂度)
"""
状态压缩:
visit[1,0,1,0]=>b(1010)=>int 10,把一个数组空间压缩到一个整数
"""
import numpy as np

N=4

#array([[0, 3, 1, 2],
#       [3, 0, 2, 1],
#       [1, 2, 0, 2],
#       [2, 1, 2, 0]])

distance=np.array([[0,3,1,2],[3,0,2,1],[1,2,0,2],[2,1,2,0]])

#判断是否已经访问过该城市
visit=np.zeros(N,dtype=np.int)

result=-1*np.ones((N,2**N))

"""
用于状态压缩
"""
def arraytoint(binarray)->'int':
    import functools
    #偏函数
    bintoint=functools.partial(int,base=2)
    
    return bintoint("".join([str(i) for i in binarray]))



def dptsp(idx,visit:"访问过的城市")->"返回从idx出发的最短路径":
    #已经访问满了(边界)
    if np.sum(visit)==N:
        return 0
    
    elif result[idx][arraytoint(visit)-1]>=0:
        return result[idx][arraytoint(visit)-1]
        
    mindistance=100000
    #依次比较idx到哪个城市路径最短
    for i in range(N):
        # 城市i未被访问过
        if visit[i]==0:
            #现在i已经被访问
            visit[i]=1

            t=distance[idx][i]+dptsp(i,visit)
            if t<mindistance:
                mindistance=t
            #由于visit是全局的,各城市与idx的比较为平等关系
            #所以需要复原到visit[i]=0
            visit[i]=0
            
    result[idx][arraytoint(visit)-1]=mindistance

    return result[idx][arraytoint(visit)-1]

"""
visit有2^n种
一共有n*2^n种状态,每个状态计算n次
时间复杂度为O(n*n*2^n)
"""

visit[0]=1
dptsp(0,visit)

状态一共有 N 2 N N2^{N} N2N种,可看出空间复杂度为 O ( n 2 n ) O(n2^{n}) O(n2n),每个状态的最优结果只需要计算 N N N次再比较就能求出,所以时间复杂度为 O ( n 2 2 n ) O(n^{2}2^{n}) O(n22n)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值