动态规划(学习笔记)

由最少硬币问题引入

硬币面值1、2、5。支付13元,要求硬币数量最少。

贪心:(1)5元硬币,2个             (2)2元硬币,1个             (3)1元硬币,1个。正确!

硬币面值1、2、4、5、6。支付9元。

贪心:(1)6元硬币,1个             (2)2元硬币,1个             (3)1元硬币,1个。 错误!

答案是:5元硬币+4元硬币=2个

因为贪心在第一步的时候选择了最优解,不考虑后面几步的解是否是最优,所以整体情况不是最优

这里就要调转思路,我们重新举个例子

现在有面值1元,5元,10元,25元和50元的纸币,问支付s元需要最少多少张纸币?

type = [1, 5, 10, 25, 50]   #5种面值 定义数组

Min[ ] 记录最少硬币数量:对输入的某个金额i,Min[i]是最少的硬币数量。

i=1元时,等价于:i = i-1 = 0元需要的硬币数量,加上1个1元硬币。

继续,所有金额仍然都只用1元硬币

i=2元时,等价于:i = i-1 = 1元需要的硬币数量,加上1个1元硬币。

i=3元时... i=4元时… 

在1元硬币的计算结果基础上,再考虑加上5元硬币的情况。从i=5开始就行了

i=5元时,等价于: (1)i = i-5 = 0元需要的硬币数量,加上1个5元硬币。Min[5]=1。

(2)原来的Min[5]=5。 取(1)(2)的最小值,所以Min[5]=1。 

i=6元时,等价于: (1)i = i - 5 = 1元需要的硬币数量,加上1个5元硬币。 Min[6] = 2

(2)原来的Min[6] = 6

取(1)(2)的最小值,所以Min[6] = 2

i=7元时,… i=8元时,… 

用1元和5元硬币,结果:

 递推关系:           Min[i] = min(Min[i], Min[i - 5] + 1)

def solve(s):
    Min = [int(1e12)]*(s+1)
    Min[0] = 0
    for j in range(cnt):
        for i in range(type[j], s+1):
            Min[i] = min(Min[i], Min[i-type[j]]+1)
cnt = 5
typr = [1, 5, 10, 25, 50]
s = int(input())
print(solve(s))

DP的两个特征

(1)重叠子问题。子问题是原大问题的小版本,计算步骤完全一样;计算大问题的时候,需要多次重复计算小问题。     一个子问题的多次计算,耗费了大量时间。用DP处理重叠子问题,每个子问题只需要计算一次,从而避免了重复计算,这就是DP效率高的原因。    

(2)最优子结构。首先,大问题的最优解包含小问题的最优解;其次,可以通过小问题的最优解推导出大问题的最优解。

动态规划算法通常包含以下三个步骤:

1.定义状态:动态规划算法中的状态通常是指问题的解。例如,在求解最长公共子序列问题时,状态可以定义为“字符串 A 和字符串 B 的前缀所构成的子问题的最长公共子序列长度”。

2.定义状态转移方程:状态转移方程是动态规划算法的核心。它描述了一个状态和它的相邻状态之间的关系。例如,在求解最长公共子序列问题时,状态转移方程可以定义为:

dp[i][j] = dp[i-1][j-1] + 1, if A[i] == B[j]                                                                                   

dp[i][j] = max(dp[i-1][j], dp[i][j-1]), if A[i] != B[j]                                                                       

其中 dp[i][j] 表示字符串 A 的前 i 个字符和字符串 B 的前 j 个字符所构成的子问题的最长公共子序列长度。如果 A[i] 等于 B[j],那么 dp[i][j] 就等于 dp[i-1][j-1] 加上 1。如果 A[i] 不等于 B[j],那么 dp[i][j] 就等于 dp[i-1][j] 和 dp[i][j-1] 中的最大值。

3.计算最优解:通过计算状态转移方程来计算最优解。在动态规划算法中,通常需要一个二维数组来存储子问题的解,以便在计算时进行查找和更新。

下面我用最经典的背包问题来举例说明动态规划算法的应用。

假设有一个背包,容量为 C,现在有 n 个物品,每个物品的重量为 w[i],价值为 v[i]。现在需要从这 n 个物品中选择一些物品放入背包中,使得放入的物品重量不超过 C,且价值最大。问最多能放入多少价值的物品?

首先我们需要定义状态。在这个问题中,状态可以定义为“前 i 个物品,容量为 j 的背包所能容纳的最大价值”。设这个状态为 f[i][j],其中 0 <= i <= n,0 <= j <= C。

接下来,我们需要找到状态转移方程。当考虑第 i 个物品时,有两种选择:放入背包和不放入背包。如果选择放入背包,那么最大价值就是 f[i-1][j-w[i]] + v[i]。如果选择不放入背包,那么最大价值就是 f[i-1][j]。因此,状态转移方程可以定义为:

f[i][j] = max(f[i-1][j-w[i]] + v[i], f[i-1][j])

最后,我们需要计算最优解。由于这个问题是一个最大化问题,因此最优解就是 f[n][C],即前 n 个物品,容量为 C 的背包所能容纳的最大价值。

题目:小明的背包1

小明有一个容量为C的背包。这天他去商场购物,商场一共有N件物品,第i件物品的体积为ci,价值为wi。小明想知道在购买的物品总体积不超过C的情况下所能获得的最大价值为多少,请你帮他算算。

【输入描述】输入第1行包含两个正整数 N, C,表示商场物品的数量和小明的背包容量。第 2∼N+1 行包含 2个正整数 c,w,表示物品的体积和价值。1 ≤ N ≤102,1≤C≤103,1≤wi,ci ≤103。 【输出描述】输出一行整数表示小明所能获得的最大价值

DP状态设计:定义二维数组dp[][],大小为N×C。

dp[i][j]:把前i个物品(从第1个到第i个)装入容量为j的背包中获得的最大价值。 把每个dp[i][j]看成一个背包:背包容量为j,装1~i这些物品。最后得到的dp[N][C]就是问题的答案:把N个物品装进容量C的背包的最大价值。

dp的状态转移方程:

递推计算到dp[i][j],分2种情况

(1)第i个物品的体积比容量j还大,不能装进容量j的背包。那么直接继承前i-1个物品装进容量j的背包的情况即可:dp[i][j] = dp[i-1][j]。

(2)第i个物品的体积比容量j小,能装进背包。又可以分为2种情况装或者不装第i个。

1)装第i个。从前i-1个物品的情况下推广而来,前i-1个物品是dp[i-1][j]。第i个物品装进背包后,背包容量减少c[i],价值增加w[i]。有:     dp[i][j] = dp[i-1][j-c[i]] + w[i]。

2)不装第i个。那么:dp[i][j] = dp[i-1][j]。 取1)和2)的最大值,状态转移方程:     dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - c[i]] + w[i])

def solve(n,C):
    for i in range(1,n+1):
        for j in range (0,C+1):
            if c[i]>j: dp[i][j] = dp[i-1][j]
            else:      dp[i][j] = max(dp[i-1][j], dp[i-1][j-c[i]]+w[i])
    return dp[n][C]   

N=3011
dp = [[0 for i in range(N)] for j in range(N)] 
#或者这样写:dp = [[0]*N for j in range(N)]
w = [0]*N
c = [0]*N
n, C = map(int, input().split())
for i in range(1, n+1):
    c[i], w[i] = map(int, input().split())
print(solve(n, C))

空间优化:滚动数组

把dp[][]优化成一维的dp[],以节省空间。

dp[i][]是从上面一行dp[i-1]算出来的,第i行只跟第i-1行有关系,跟更前面的行没有关系:       

dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - c[i]] + w[i])

优化:只需要两行dp[0][]、dp[1][],用新的一行覆盖原来的一行,交替滚动。 经过优化,空间复杂度从O(N×C)减少为O(C)。

定义dp[2][j]:用dp[0][]和dp[1][]交替滚动。

def solve(n,C):
    now = 0
    old = 1
    for i in range(1,n+1):
        old,now = now,old            #处理完一次循环后进行交换
        for j in range (0,C+1):
            if c[i] > j:  dp[now][j] = dp[old][j]
            else: 
              dp[now][j] = max(dp[old][j], dp[old][j-c[i]]+w[i])
    return dp[now][C]   
N = 3011
dp = [[0 for i in range(N)] for j in range(2)]    #注意先后
w = [0]*N
c = [0]*N
n, C = map(int, input().split())
for i in range(1, n+1):   c[i], w[i] = map(int, input().split())
print(solve(n, C))

只能优化空间复杂度,无法优化时间复杂度

但是滚动数组

再优化一下:自我滚动

因为状态转移每次只与上一层有关,所以用一个一维数组就可以。

继续精简:用一个一维的dp[]就够了,自己滚动自己。 dp[j]=dp[j-c[i]]+w[i] 为什么从大到小遍历, 看 dp[j]=dp[j-c[i]]+w[i]这一状态转移,是根据小的改大的,如果先把小的改了,那小的还会被用到,数据就不对了,所以从小到大。

#装满
for i in range(C + 50):
    dp.append(-0x3f3f3f3f)  #这个是16进制的-INF
#当两个数特别大的时候,相加会变成负数,所以需要初始化-INF
dp[0] = 0
#不装满
for i in range(C+50):
    dp.append(0)
#装满 dp[0]=0,其余赋值-INF;
不装满全初始化为 0;

若一定要求装满: 则必有n=sum(c[i]) i∈(已选集合 ,所以dp[n-sum(c[i])]= dp[0]

所以只有从dp[0]出发才合法,那就把其他的设成无穷小。

线性DP

题目:0/1背包简化版

【题目描述】有一个箱子容量为V(正整数,0≤V≤20000),同时有n个物品(0< n ≤ 30),每个物品有一个体积(正整数)。要求 n 个物品中,任取若干个装入箱内,使箱子的剩余空间为最小。 【输入描述】输入第一行,一个整数,表示箱子容量。第二行,一个整数n,表示有n个物品。接下来n行,分别表示这n个物品的各自体积。

【输出描述】输出一行,表示箱子剩余空间。

0/1背包的简化版,不管物品的价格。把体积(不是价格)看成最优化目标:最大化体积

V = int(input())
n = int(input())
w = [0]
for i in range(n):
  w.append(int(input()))
dp = [0] * (V+1)
for i in range(1, n+1):
    for j in range(V, w[i]-1, -1):
        dp[j] = max(dp[j], dp[j-w[i]]+w[i])
print(V-dp[V])

题目:0/1背包的方案数

【题目描述】将2022拆分成10个互不相同的正整数之和,总共有多少种拆分方法?注意交换顺序视为同一种方法。 注意交换顺序视为同一种方法, 例如:      

2022 = 1000+1022     2022 = 1022+1000  视为同一种方法。

题目求10个数的组合情况,这十个数相加等于2022。因为是填空题可以不管运行时间,看起来可以用暴力for循环10次,加上剪枝。 然而暴力的时间极长,因为答案是:379187662194355221。

这一题其实是0/1背包:背包容量为2022,物品体积为1~2022,往背包中装10个物品,要求总体积为2022,问一共有多少种方案。 与标准背包的区别:是求方案总数。

定义dp[][][]:dp[i][j][k]表示数字1~i取j个,和为k的方案数。

下面的分析沿用标准0/1背包的分析方法。

从i-1扩展到i,分两种情况:

(1)k≥i。数i可以要,也可以不要。     要i。从1~i-1中取j-1个数,再取i,等价于dp[i-1][j-1][k-i]。     不要i。从1~i-1中取j个数,等价于dp[i-1][j][k]       合起来:dp[i][j][k] = dp[i-1][j][k] + dp[i-1][j-1][k-i] (2)k<i。由于数i比总和k还大,显然i不能用。有:dp[i][j][k] = dp[i-1][j][k]

#不用滚动数组
dp = [[[0 for _ in range(2222)] for _ in range(11)] for _ in range(2222)]
for i in range(2023):
    dp[i][0][0] = 1   #特别注意这个初始化
for i in range(1, 2023):
    for j in range(1, 11):  #注意:j从小到大,或从大到小都行
        for k in range(1, 2023):
            if k < i:
                dp[i][j][k] = dp[i-1][j][k]  #无法装进背包
            else:
                dp[i][j][k] = dp[i-1][j][k] + dp[i-1][j-1][k-i]
print(dp[2022][10][2022])
#用滚动数组
dp = [[0 for _ in range(2222)] for _ in range(11)]
dp[0][0]=1
for i in range(1, 2023):
    for j in range(10, 0, -1):  #注意:j一定要从大到小
        for k in range(i, 2023):
            dp[j][k]+=dp[j-1][k-i]
print(dp[10][2022])

题目:小明的背包2

【题目描述】小明有一个容量为 C的背包。这天他去商场购物,商场一共有 N 种物品,第i种物品的体积为 ci,价值为wi,每种物品都有无限多个。小明想知道在购买的物品总体积不超过C的情况下所能获得的最大价值为多少,请你帮他算算。

【输入描述】输入第1行包含两个正整数N,C,表示商场物品的数量和小明的背包容量。第2∼N+1行包含2个正整数c,w,表示物品的体积和价值。1≤N≤10^3,1≤C≤10^3,1≤ci, wi≤10^3。

【输出描述】输出一行整数表示小明所能获得的最大价值。

思路和0/1背包类似。0/1背包的每种物品只有1件,完全背包的每种物品有无穷多件,第i种可以装0件、1件、2件、C/ci件。 定义dp[i][j]:把前i种物品(从第1种到第i种)装入容量为j的背包中获得的最大价值。 把每个dp[i][j]都看成一个背包:背包容量为j,装1~i这些物品。最后得到的dp[N][C]就是问题的答案:把N种物品装进容量C的背包的最大价值。

在0/1背包问题中,每个物品只有拿与不拿两种;而完全背包问题,需要考虑拿几个。

 

#优化前
c = []
w = []
if __name__ == '__main__':
    n, C = map(int, input().split())
    c.append(0)
    w.append(0)

    for i in range(n):
        tempC, tempW = map(int, input().split())
        c.append(tempC)
        w.append(tempW)

    dp = [[0]*(C+5) for _ in range(n+5)]
    # _是一个变量(因为Python中的变量命名能够以下划线开始,单独的下划线也是一个变量),
    # 跟i一样,不同点在于,i会在后续的循环体中运用到,而_只是用来实现循环的次数。
    for i in range(1, n+1):
        for j in range(1, C+1):
            dp[i][j] = dp[i-1][j]
            if j >= c[i]:
                dp[i][j] = max(dp[i][j], dp[i][j-c[i]]+w[i])
    print(dp[n][C])

这里会超时,我们进行优化,因为每次状态转移只与上一层有关,所以我们用一个一维数组就行了

#优化后
N, V = map(int, input().split())
dp = [0]*(V+1)
for _ in range(N):
  w, v = map(int, input().split())
  for j in range(1, V+1):
    if j<w:
      dp[j] = dp[j]
    else:
      dp[j] = max(dp[j], dp[j-w]+v)
print(dp[V])

题目:最长公共子序列

【题目描述】给定一个长度为n数组A和一个长度为m的数组B。请你求出它们的最长公共子序列长度为多少。

【输入描述】输入第一行包含两个整数n、m。第二行包含n个整数ai,第三行包含m个整数bi,1≤n, m≤103, 1≤ai, bi≤109。

【输出描述】输出一行整数表示答案。

输入输出样例

示例 1

输入

5 6
1 2 3 4 5
2 3 2 1 4 5

输出

4

一个给定序列的子序列,是在该序列中删去若干元素后得到的序列。 例如:X = {A, B, C, B, D, A, B},它的子序列有{A, B, C, B, A}、{A, B, D}、{B, C, D, B}等。 给定两个序列X和Y,当另一序列Z既是X的子序列又是Y的子序列时,称Z是序列X和Y的公共子序列。 最长公共子序列是长度最长的子序列。

暴力法:先找出A的所有子序列,然后一一验证是否为Y的子序列。 如果A有m个元素,那么A有2m个子序列;B有n个元素;总复杂度大于O(n2^m)

dp[i][j]:序列Ai(a1~ai)和Bj (b1~bj)的最长公共子序列的长度。 答案:dp[n][m]

分解为2种情况:

(1)当ai = bj时,已求得Ai-1和Bj-1的最长公共子序列,在其尾部加上ai 或bj即可得到Ai和Bj的最长公共子序列。 状态转移方程:     dp[i][j] = dp[i-1][j-1] + 1

(2)当ai ≠ bj时,求解两个子问题: Ai-1和Bj的最长公共子序列;Ai和Bj-1的最长公共子序列。取最大值,状态转移方程:     dp[i][j] = max(dp[i][j-1], dp[i-1][j])

Maxn = 1005

dp = [[0 for _ in range(Maxn)] for _ in range(Maxn)]

if __name__ == '__main__':

    n,m=map(int,input().split())

    a= list(map(int,input().split()))

    b= list(map(int,input().split()))

    for i in range(len(a)):

        for j in range(len(b)):

            if a[i] == b[j]:

                dp[i + 1][j + 1] = dp[i][j] + 1

            else:

                if dp[i + 1][j] > dp[i][j + 1]:

                    dp[i + 1][j + 1] = dp[i + 1][j]


                else:

                    dp[i + 1][j + 1] = max(dp[i + 1][j], dp[i][j + 1])

    print(dp[n][m])

题目:蓝桥骑士

【题目描述】小明是蓝桥王国的骑士,他喜欢不断突破自我。这天蓝桥国王给他安排了N个对手,他们的战力值分别为 a1, a2, ..., an,且按顺序阻挡在小明的前方。对于这些对手小明可以选择挑战,也可以选择避战。身为高傲的骑士,小明从不走回头路,且只愿意挑战战力值越来越高的对手。请你算算小明最多会挑战多少名对手。

【输入描述】第一行是整数N,表示对手的个数,第二行是N个整数a1, a2, ..., an ,表示对手战力值。1 ≤ N ≤ 3×10^5

【输出描述】输出一行整数表示答案。

from bisect import bisect_left

n = int(input())
a = list(map(int, input().split()))

tail = [a[0]]#初始化真正想挑战的对手列表
for i in range(1, n):
    if a[i] > tail[-1]:
        tail.append(a[i])
    else:
        j = bisect_left(tail, a[i])
        tail[j] = a[i]
#如果a[i]比tail里最后一个元素还大,就把a[i]加到tail里面
#否则,在递增子序列中查找小于等于 a[i] 的最大元素,并将其替换为 a[i]。

print(len(tail))

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值