Acwing 基础算法之:第五章 动态规划

背包问题
01背包问题
  • 状态f[j]的定义:表示取前i件物品,在背包容量不超过j的前提下,能得到的最大价值
  • 由于这里做了优化:由二维优化到一维,所以在进行第i次遍历的时候要保证用到的状态是由i-1转移过来
  • 所以背包体积的遍历就需要从大到小遍历,遍历的最小值必须要满足j - v[i] ≥0
for i in range(1,N+1):
    for j in range(V,self.v[i]-1,-1):
        """
                1. j - self.v[i] 恒小于j,所以j的遍历必须从大到小进行遍历,否
                   则j - self.v[i]就是由第i层进行更新了
                2. 由于j需要大于等于vi,所以j的遍历直接从vi开始即可
                """
        self.f[j] = max(self.f[j],self.f[j - self.v[i]] + self.w[i])

AcWing 2. 01背包问题

有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。

第 i 件物品的体积是 v i v_i vi,价值是 w i w_i wi

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

输入格式

第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。

接下来有 N 行,每行两个整数 v i , w i v_i,w_i vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。

输出格式

输出一个整数,表示最大价值。

数据范围

0<N,V≤1000
0 < v i , w i ≤ 1000 0<v_i,w_i≤1000 0<vi,wi1000

输入样例

4 5
1 2
2 4
3 4
4 5

输出样例:

8
  • 朴素版:
    • 集合f[i][j] :表示从前i个物品中选取,物品总体积不超过j的集合
    • 属性f[i][j]: 表示最大价值
    • 状态转移 :
      • 不选第i个物品: 最大价值就等于f[i][j] = f[i-1][j]
      • 选择第i个物品: 最大价值就等于f[i][j] = max(f[i-1][j-v[i]]+w[i],f[i][j])
# 朴素版
class Solution:
    def __init__(self):
        N = 1010
        self.f = [[0] * N for _ in range(N)]
        self.v = [0] * N
        self.w = [0] * N
    def main(self):
        n,m = map(int,input().split())
        for i in range(1,n+1):
            v,w = map(int,input().split())
            self.w[i] =  w
            self.v[i] = v

        for i in range(1,n+1):
            for j in range(m+1):
                self.f[i][j] = self.f[i-1][j]
                if j >= self.v[i]:
                    self.f[i][j] = max(self.f[i][j],self.f[i-1][j-self.v[i]] + self.w[i])

        print(self.f[n][m])

if __name__ == '__main__':
    solution = Solution()
    solution.main()
  • 一维数组优化:
    • 由于第i层计算的最大价值只与第i-1层有关,所以考虑滚动数组优化
    • 对朴素版状态转移进行优化self.f[i][j] = max(self.f[i][j],self.f[i-1][j-self.v[i]] + self.w[i])
    • 去掉第一维,就得到self.f[j] = max(self.f[j],self.f[j-self.v[i]] + self.w[i])
    • 但是要由于self.f[j-self.v[i]]中的j-self.v[i]恒小于j,所以如果背包体积如果从小到大进行遍历,第i层计算所用到的数组就是被第i层已经更新过;所以这里背包容量的遍历就需要从大到小进行遍历
# 优化版
class knapsack:
    def __init__(self):
        N = 1010
        self.f = [0] * N
        self.w = [0] * N
        self.v = [0] * N
    def main(self):
        N,V = map(int,input().split())
        for k in range(1,N+1):
            v,w = map(int, input().split())
            self.w[k] = w
            self.v[k] = v
        # 由于i只跟i-1的状态有关,所以可以对二维数组进行优化,优化成滚动数组
        for i in range(1,N+1):
            for j in range(V,self.v[i]-1,-1):
                """
                1. j - self.v[i] 恒小于j,所以j的遍历必须从大到小进行遍历,否
                   则j - self.v[i]就是由第i层进行更新了
                2. 由于j需要大于等于vi,所以j的遍历直接从vi开始即可
                """
                self.f[j] = max(self.f[j],self.f[j - self.v[i]] + self.w[i])
        print(self.f[V])
if __name__ == '__main__':
    knapsack = knapsack()
    knapsack.main()

完全背包问题
  • 朴素版:

    • 集合f[i][j] :表示从前i个物品中选取,物品总体积不超过j的集合
    • 属性f[i][j]: 表示最大价值
    • 状态转移 : self.f[i][j] = max(self.f[i][j],self.f[i-1][j-k*self.v[i]] + self.w[i] * k)
      • 不选第i个物品: 最大价值就等于f[i][j] = f[i-1][j]
      • i个物品选1个: 最大价值就等于f[i][j] = max(f[i-1][j-v[i]]+w[i],f[i][j])
      • i个物品选2个: 最大价值就等于f[i][j] = max(f[i-1][j-v[i]]+w[i],f[i-1][j-2*v[i]]+2*w[i],f[i][j])
      • ……
  • 滚动数组优化:

    • 集合f[j] :表示从前i个物品中选取,物品总体积不超过j的集合

    • 属性f[j]: 表示最大价值

    • 状态转移 :

      • f[i , j] = max( f[i-1,j] , f[i-1,j-v]+w , f[i-1,j-2*v]+2*w , f[i-1,j-3*v]+3*w , .....)

      • f[i , j-v]= max( f[i-1,j-v] , f[i-1,j-2*v] + w , f[i-1,j-3*v]+2*w , .....)

      • 由上两式,可得出如下递推关系: f[i][j]=max(f[i][j-v]+w , f[i-1][j])

      • 优化一维后:f[j]=max(f[j-v]+w , f[j])

      • 注意:优化前f[i][j-v]+w为第i层计算后的值,所以这里遍历顺序需要从小到大遍历

AcWing 3. 完全背包问题

有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。

第 i 种物品的体积是 v i v_i vi,价值是 w i w_i wi

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

输入格式

第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。

接下来有 N 行,每行两个整数 v i , w i v_i,w_i vi,wi,用空格隔开,分别表示第 i 种物品的体积和价值。

输出格式

输出一个整数,表示最大价值。

数据范围

0<N,V≤1000
0 < v i , w i ≤ 1000 0<v_i,w_i≤1000 0<vi,wi1000

输入样例

4 5
1 2
2 4
3 4
4 5

输出样例:

10
# 朴素版
class Backsnap:
    def __init__(self):
        N = 1010
        self.w = [0] * N
        self.v = [0] * N
        self.f = [[0] * N for _ in range(N)]

    def main(self):
        N,V = map(int,input().split())
        for i in range(1,N+1):
            v,w = map(int,input().split())
            self.w[i] = w
            self.v[i] = v

        for i in range(1,N+1): # 逐个遍历背包容量
            for j in range(V+1): # 逐个遍历每个物品的体积
                for k in range(j//self.v[i] + 1): # 遍历每个物品可以选取的数量,选取物品不能超过背包的总体积j
                    """
                    状态计算:
                    1.self.f[i-1][j-k*self.v[i]] + self.w[i] * k 当k=0的时候,这个式子就是self.f[i-1][j]
                    2.对转移过后的式子与当前位置的值进行比较,将当前位置的值更新为较大者
                    """
                    self.f[i][j] = max(self.f[i][j],self.f[i-1][j-k*self.v[i]] + self.w[i] * k)

        print(self.f[N][V])

if __name__ == '__main__':
    backsnap = Backsnap()
    backsnap.main()
# 滚动数组优化
class Solution:
    def __init__(self):
        N = 1010
        self.f = [0] * N
        self.w = [0] * N
        self.v = [0] * N

    def main(self):
        n,m = map(int,input().split())
        for i in range(1,n+1):
            v,w = map(int,input().split())
            self.w[i] = w
            self.v[i] = v

        for i in range(1,n+1):
            for j in range(self.v[i],m+1):
                self.f[j] = max(self.f[j],self.f[j-self.v[i]] + self.w[i])
        print(self.f[m])

if __name__ == '__main__':
    solution = Solution()
    solution.main()

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qVrFdQdy-1683472543425)(C:\Users\Administrator.Bili-2021JLBPTZ\AppData\Roaming\Typora\typora-user-images\image-20230507192927903.png)]


多重背包问题
  • 集合f[j] :表示从前i个物品中选取,物品总体积不超过j的集合

  • 属性f[j]: 表示最大价值

  • 状态转移 :

    • f[i , j] = max( f[i-1,j] , f[i-1,j-v]+w , f[i-1,j-2*v]+2*w , f[i-1,j-3*v]+3*w , .....)

    • 优化一维后:f[j]=max(f[j-k*v]+k*w , f[j])

    • 注意:优化前f[i-1][j-k*v]+k*w为第i-1层计算的值,j-k*v ≤ j:所以背包体积循环还是需要跟01背包一样,从大到小循环

AcWing 4. 多重背包问题

有 N 种物品和一个容量是 V 的背包。

第 i 种物品最多有 s i s_i si 件,每件体积是 v i v_i vi,价值是 w i w_i wi

求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。

输入格式

第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。

接下来有 N 行,每行三个整数 v i , w i , s i v_i,w_i,s_i vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。

输出格式

输出一个整数,表示最大价值。

数据范围

0<N,V≤100
0 < v i , w i , s i ≤ 100 0<v_i,w_i,s_i≤100 0<vi,wi,si100

输入样例

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

输出样例:

10
class Solution:
    def __init__(self):
        N = 110
        self.f = [0] * N
        self.w = [0] * N
        self.v = [0] * N
        self.s = [0] * N

    def main(self):
        n,m = map(int,input().split())
        for i in range(1,n+1):
            v,w,s = map(int,input().split())
            self.v[i] = v
            self.w[i] = w
            self.s[i] = s

        for i in range(1,n+1):
            for j in range(m,-1,-1):
                k = 0
                while k * self.v[i] <= j and k <= self.s[i]:
                    self.f[j] = max(self.f[j],self.f[j-k * self.v[i]] + k * self.w[i])
                    k += 1
        print(self.f[m])

if __name__ == '__main__':
    solution = Solution()
    solution.main()

多重背包问题 II - 二进制拆分
  • 每个物品最多有s件,任意实数都可转换成二进制表示形式,所以将s分成logs个堆
  • 为保证从logs个堆中凑出s,并且不会超过s,所以最后一个堆的大小 s − k l o g s s - k^{logs} sklogs
  • 例如:15可以拆分成[1,2,4,8,7] ,拆分出来的数可以表示1-15内的所有数
  • 将多重背包问题就转换成了01背包问题
# 伪代码
k = 1
while k <= s:
    v[cnt] = v * k
    w[cnt] = w * k
    s -= k
    k *= 2
    cnt += 1
if s > 0: # 如果s还有剩余,就将剩余的分成一组
    v[cnt] = v * s
    w[cnt] = w * s
    cnt += 1

AcWing 5. 多重背包问题 II

有 N 种物品和一个容量是 V 的背包。

第 i 种物品最多有 s i s_i si 件,每件体积是 v i v_i vi,价值是 w i w_i wi

求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。

输入格式

第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。

接下来有 N 行,每行三个整数 v i , w i , s i v_i,w_i,s_i vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。

输出格式

输出一个整数,表示最大价值。

数据范围

0<N≤1000
0<V≤2000
0 < v i , w i , s i ≤ 2000 0<v_i,w_i,s_i≤2000 0<vi,wi,si2000

提示:

本题考查多重背包的二进制优化方法。

输入样例

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

输出样例:

10
class Solution:
    def __init__(self):
        N = 1000 * 12 # log2000 ≈ 12,二进制拆分后最后为12*1000个物品
        self.f = [0] * N
        self.v = [0] * N
        self.w = [0] * N

    def main(self):
        n,m = map(int,input().split())
        cnt = 1
        for i in range(1,n+1):
            v,w,s = map(int,input().split())
            k = 1 
            while k <= s:
                self.v[cnt] = v * k
                self.w[cnt] = w * k
                s -= k
                k *= 2
                cnt += 1
            if s > 0: # 如果s还有剩余,就将剩余的分成一组
                self.v[cnt] = v * s
                self.w[cnt] = w * s
                cnt += 1
        for j in range(1,cnt+1):
            for k in range(m,self.v[j] - 1,-1):
                self.f[k] = max(self.f[k],self.f[k-self.v[j]] + self.w[j])

        print(self.f[m])

if __name__ == '__main__':
    solution = Solution()
    solution.main()

分组背包
  • 集合f[i][j] :表示从前i个物品中选取,物品总体积不超过j的集合

  • 属性f[i][j]: 表示最大价值

  • 状态转移 :

    • f[i][j]=max(f[i][j],f[i-1][j-v[i][k]]+w[i][k]) 第i个组中选择组内的第k个物品
  • 一维数组优化:

    • 状态转移方程依旧只跟第i-1层有关,可以转换成01背包问题求解
    • 最内层遍历每个组的物品即可
    # 伪代码
    for k in range(m,-1,-1):
        for l in range(1,s+1):
            if self.v[l] <= k:
                self.f[k] = max(self.f[k],self.f[k-self.v[l]] + self.w[l])
    

AcWing 9. 分组背包问题

有 N 组物品和一个容量是 V 的背包。

每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 v i j v_{ij} vij,价值是 w i j w_{ij} wij,其中 i 是组号,j 是组内编号。

求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。

输出最大价值。

输入格式

第一行有两个整数 N,V,用空格隔开,分别表示物品组数和背包容量。

接下来有 N 组数据:

  • 每组数据第一行有一个整数 S i S_i Si,表示第 i 个物品组的物品数量;
  • 每组数据接下来有 S i S_i Si 行,每行有两个整数 v i j , w i j v_{ij},w_{ij} vij,wij,用空格隔开,分别表示第 i 个物品组的第 j 个物品的体积和价值;

输出格式

输出一个整数,表示最大价值。

数据范围

0<N,V≤100
0 < S i ≤ 100 0<Si≤100 0<Si100
0 < v i j , w i j ≤ 100 0<v_{ij},w_{ij}≤100 0<vij,wij100

输入样例

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

输出样例:

8
class Solution:
    def __init__(self):
        N = 110
        self.f = [0] * N
        self.w = [0] * N
        self.v = [0] * N
        self.s = [0] * N

    def main(self):
        n,m = map(int,input().split())
        for i in range(1,n+1):
            s = int(input())
            for j in range(1,s+1):
                v,w = map(int,input().split())
                self.v[j] = v
                self.w[j] = w
            # 遍历每组内的物品,转换成01背包问题求解
            for k in range(m,-1,-1):
                for l in range(1,s+1):
                    if self.v[l] <= k:
                        self.f[k] = max(self.f[k],self.f[k-self.v[l]] + self.w[l])
        print(self.f[m])

if __name__ == '__main__':
    solution = Solution()
    solution.main()

线性DP

AcWing 898. 数字三角形

  • 集合f[i][j] :表示到达当前位置的所有方案集合

  • 属性f[i][j]: 最大值

  • 状态转移 : (这里为了使代码简洁,避免边界条件判断,采用从下往上遍历)

    • f[i][j]:只能从两个方向转移过来:1. 是从左下角转移过来f[i+1][j];2. 从右下角转移过来f[i+1][j+1]

给定一个如下图所示的数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大。

        7
      3   8
    8   1   0
  2   7   4   4
4   5   2   6   5
输入格式

第一行包含整数 n,表示数字三角形的层数。

接下来 n 行,每行包含若干整数,其中第 i 行表示数字三角形第 i 层包含的整数。

输出格式

输出一个整数,表示最大的路径数字和。

数据范围

1≤n≤500,
−10000≤三角形中的整数≤10000

输入样例:

5
7
3 8
8 1 0 
2 7 4 4
4 5 2 6 5

输出样例:

30
class Solution:
    def __init__(self):
        N = 510
        self.f = [[float('-inf')] * N for _ in range(N)] # 由于存在负数,这里需要初始化为一个极小的数
        self.arr = [[0] * N for _ in range(N)]

    def main(self):
        n = int(input())
        for i in range(1,n+1):
            line= list(map(int,input().split()))
            for j in range(1,i+1):
                self.arr[i][j] = line[j-1]
        # 从下往上遍历,省去边界条件判断
        # 初始化最下层的初始值为第n层的整数的值
        for i in range(1,n+1):
            self.f[n][i] = self.arr[n][i]
        # 从下往上遍历
        # 状态f[i][j]:只能从两个方向转移过来
        # 1. 从左下转移过来:f[i+1][j]
        # 2. 从右下转移过来:f[i+1][j+1]
        # 取二者最大值即可
        for i in range(n,0,-1):
            for j in range(i+1):
                self.f[i][j] = max(self.f[i][j],self.f[i+1][j] + self.arr[i][j],self.f[i+1][j+1] + self.arr[i][j])
        print(self.f[1][1])

if __name__ == '__main__':
    solution = Solution()
    solution.main()

AcWing 895. 最长上升子序列

  • 集合f[i]: 表示以第i个字符结尾的上升子序列
  • 属性:最长上升子序列长度
  • 状态转移:
    • 选择第j个字符:那么第i个字符就必须大于第j个字符,此时f[i] = f[j] + 1
    • 不选择第j个字符:f[i] = f[j]
    • 取二者最大值即可
  • 根据定义f[i]表示的是第I个字符结尾的最长上升子序列,所以每个字符都有可能是最长上升子序列,取f[i]的最大值即可

给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。

输入格式

第一行包含整数 N。

第二行包含 N 个整数,表示完整序列。

输出格式

输出一个整数,表示最大长度。

数据范围

1≤N≤1000
− 1 0 9 ≤ 数列中的数 ≤ 1 0 9 −10^9≤数列中的数≤10^9 109数列中的数109

输入样例:

7
3 1 2 1 8 5 6

输出样例:

4
class Solution:
    def __init__(self):
        N = 1010
        self.f = [1] * N

    def main(self):
        n = int(input())
        line = list(map(int,input().split()))
        nums = [0] + line
        self.f[0] = 0
        res = 0 # 记录最大长度的值
        for i in range(1,n+1):
            for j in range(i):
                if nums[j] < nums[i]:
                    self.f[i] = max(self.f[i],self.f[j] + 1)
                    res = max(res,self.f[i]) #更新一次最大长度
        print(res)

solution = Solution()
solution.main()

AcWing 896. 最长上升子序列 II

  • 思路:用一个单独的数组q存储上升子序列,如果遍历到的数字大于q的末尾的数字,就直接加入到q的末尾,反之就查找第一个小于这个数的位置,将这个位置后一个数字替换成当前数字
for i in range(n):
    if self.q[self.idx] < line[i]: # 如果数字大于上升队列末尾的数字,就直接加入到队列中
        self.idx += 1
        self.q[self.idx] = line[i]
    else:
        r = self.find(line[i]) #否则就找到第一个小于这个数的位置,将这个位置后面的数换成当前数字
        self.q[r + 1] = line[i]

给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。

输入格式

第一行包含整数 N。

第二行包含 N 个整数,表示完整序列。

输出格式

输出一个整数,表示最大长度。

数据范围

1≤N≤100000,
− 1 0 9 ≤ 数列中的数 ≤ 1 0 9 −10^9≤数列中的数≤10^9 109数列中的数109

输入样例:

7
3 1 2 1 8 5 6

输出样例:

4
class Solution:
    def __init__(self):
        N = 100010
        self.f = [0] * N
        self.q = [0] * N
        self.idx = 0

    def find(self,x):
        """
        找到第一个大于等于x的数的位置
        :param x:
        :return:
        """
        l = 0
        r = self.idx
        while l < r:
            mid = l + r + 1>> 1
            if self.q[mid] < x: l = mid
            else:r = mid - 1

        return r

    def main(self):
        n = int(input())
        line = list(map(int,input().split()))
        self.q[0] = -2e9
        for i in range(n):
            if self.q[self.idx] < line[i]: # 如果数字大于上升队列末尾的数字,就直接加入到队列中
                self.idx += 1
                self.q[self.idx] = line[i]
            else:
                r = self.find(line[i]) #否则就找到第一个小于这个数的位置,将这个位置后面的数换成当前数字
                self.q[r + 1] = line[i]
        print(self.idx)

if __name__ == '__main__':
    solution = Solution()
    solution.main()

AcWing 902. 最短编辑距离

  • 状态表示:f[i][j]表示字符串a中的前i个字符经过变换得到字符串b的前j个字符的操作数

  • 状态属性:最小操作数

  • 状态计算:
    1.删除操作:当a[i]的前[i-1]个字符与b[j]匹配,则需要将字符串a的第i个字符删掉;f[i][j] = f[i-1][j] + 1
    2.增加操作:当a[i]的前i个字符与b[j-1]匹配,则需要将将字符串a增加一个字符;f[i][j] = f[i][j-1] + 1
    3.修改操作:当a[i]的前i-1个字符与b[j-1]匹配时,
    如果第i个字符与第j个字符匹配,则不需要进行操作;f[i][j] = f[i-1][j-1]
    不匹配则需要进行一次更改操作; f[i][j] = f[i-1][j-1] + 1

  • dp数组初始化:
    1.当i = 0时,需要增加j个字符才能与b[j]匹配;所以f[i][0] = i
    2.当j = 0时,需要删除i个字符才能与b[0]匹配,所以f[0][j] = j

给定两个字符串 A 和 B,现在要将 A 经过若干操作变为 B,可进行的操作有:

  1. 删除–将字符串 A 中的某个字符删除。
  2. 插入–在字符串 A 的某个位置插入某个字符。
  3. 替换–将字符串 A 中的某个字符替换为另一个字符。

现在请你求出,将 A 变为 B 至少需要进行多少次操作。

输入格式

第一行包含整数 n,表示字符串 A 的长度。

第二行包含一个长度为 n 的字符串 A。

第三行包含整数 m,表示字符串 B 的长度。

第四行包含一个长度为 m 的字符串 B。

字符串中均只包含大小写字母。

输出格式

输出一个整数,表示最少操作次数。

数据范围

1≤n,m≤1000

输入样例:

10 
AGTCTGACGC
11 
AGTAAGTAGGC

输出样例:

4
class Etdis:
    def __init__(self):
        N = 1010
        self.f = [[0] * N for _ in range(N)]

    def main(self):
        n = int(input())
        A = '0'
        A += input()
        m = int(input())
        B = '0'
        B +=input()
        # 初始化dp数组
        for i in range(n+1):self.f[i][0] = i
        for j in range(m+1):self.f[0][j] = j

        for i in range(1,n+1):
            for j in range(1,m+1):
                self.f[i][j] = min(self.f[i-1][j]+1,self.f[i][j-1]+1)
                if A[i] == B[j]:self.f[i][j] = min(self.f[i][j],self.f[i-1][j-1])
                else:self.f[i][j] = min(self.f[i][j] , self.f[i-1][j-1] + 1)
        print(self.f[n][m])
if __name__ == '__main__':
    etdis = Etdis()
    etdis.main()

AcWing 902. 最短编辑距离

  • 状态表示:f[i][j]表示字符串a中的前i个字符经过变换得到字符串b的前j个字符的操作数

  • 状态属性:最小操作数

  • 状态计算:
    1.删除操作:当a[i]的前[i-1]个字符与b[j]匹配,则需要将字符串a的第i个字符删掉;f[i][j] = f[i-1][j] + 1
    2.增加操作:当a[i]的前i个字符与b[j-1]匹配,则需要将将字符串a增加一个字符;f[i][j] = f[i][j-1] + 1
    3.修改操作:当a[i]的前i-1个字符与b[j-1]匹配时,
    如果第i个字符与第j个字符匹配,则不需要进行操作;f[i][j] = f[i-1][j-1]
    不匹配则需要进行一次更改操作; f[i][j] = f[i-1][j-1] + 1

  • dp数组初始化:
    1.当i = 0时,需要增加j个字符才能与b[j]匹配;所以f[i][0] = i
    2.当j = 0时,需要删除i个字符才能与b[0]匹配,所以f[0][j] = j

给定两个字符串 A 和 B,现在要将 A 经过若干操作变为 B,可进行的操作有:

  1. 删除–将字符串 A 中的某个字符删除。
  2. 插入–在字符串 A 的某个位置插入某个字符。
  3. 替换–将字符串 A 中的某个字符替换为另一个字符。

现在请你求出,将 A 变为 B 至少需要进行多少次操作。

输入格式

第一行包含整数 n,表示字符串 A 的长度。

第二行包含一个长度为 n 的字符串 A。

第三行包含整数 m,表示字符串 B 的长度。

第四行包含一个长度为 m 的字符串 B。

字符串中均只包含大小写字母。

输出格式

输出一个整数,表示最少操作次数。

数据范围

1≤n,m≤1000

输入样例:

10 
AGTCTGACGC
11 
AGTAAGTAGGC

输出样例:

4
class Etdis:
    def __init__(self):
        N = 1010
        self.f = [[0] * N for _ in range(N)]

    def main(self):
        n = int(input())
        A = '0'
        A += input()
        m = int(input())
        B = '0'
        B +=input()
        # 初始化dp数组
        for i in range(n+1):self.f[i][0] = i
        for j in range(m+1):self.f[0][j] = j

        for i in range(1,n+1):
            for j in range(1,m+1):
                self.f[i][j] = min(self.f[i-1][j]+1,self.f[i][j-1]+1)
                if A[i] == B[j]:self.f[i][j] = min(self.f[i][j],self.f[i-1][j-1])
                else:self.f[i][j] = min(self.f[i][j] , self.f[i-1][j-1] + 1)
        print(self.f[n][m])
if __name__ == '__main__':
    etdis = Etdis()
    etdis.main()

AcWing 899. 编辑距离

  • 将每次询问给出的字符串和n个字符串逐一计算编辑距离
  • 如果计算的编辑距离小于等于操作次数上线,就可以完成操作变成目标字符串

给定 n 个长度不超过 10 的字符串以及 m 次询问,每次询问给出一个字符串和一个操作次数上限。

对于每次询问,请你求出给定的 n 个字符串中有多少个字符串可以在上限操作次数内经过操作变成询问给出的字符串。

每个对字符串进行的单个字符的插入、删除或替换算作一次操作。

输入格式

第一行包含两个整数 n 和 m。

接下来 n 行,每行包含一个字符串,表示给定的字符串。

再接下来 m 行,每行包含一个字符串和一个整数,表示一次询问。

字符串中只包含小写字母,且长度均不超过 10。

输出格式

输出共 m 行,每行输出一个整数作为结果,表示一次询问中满足条件的字符串个数。

数据范围

1≤n,m≤1000,

输入样例:

3 2
abc
acd
bcd
ab 1
acbd 2

输出样例:

1
3
class Solution:
    def __init__(self):
        self.strs = list()

    def etdis(self,strs1,strs2):
        n = len(strs1)
        m = len(strs2)
        strs1 = '0' + strs1
        strs2 = '0' + strs2
        f = [[0] * (m+10) for _ in range(n+10)]
        for i in range(n+1):f[i][0] = i
        for i in range(m+1):f[0][i] = i
        for i in range(1,n+1):
            for j in range(1,m+1):
                f[i][j] = min(f[i-1][j],f[i][j-1]) + 1
                f[i][j] = min(f[i][j],f[i-1][j-1] + (strs1[i] != strs2[j]))
        return f[n][m]

    def main(self):
        n,m = map(int,input().split())
        for i in range(n):
            self.strs.append(input())

        for j in range(m):
            res = 0
            line = input().split()
            strs2 = line[0]
            tar = int(line[1])
            for strs in self.strs:
                dis = self.etdis(strs2,strs)
                if dis <= tar:
                    res += 1
            print(res)
if __name__ == '__main__':
    solution = Solution()
    solution.main()

区间DP

区间dp:

  1. 状态表示,f[i][j]:表示区间[i,j]范围内,石子的合并方法数
  2. 属性:最小合并代价
  3. 状态计算:
    1. 遍历区间长度,对每个区间长度计算合并该区间的最小代价
    2. 区间[i,j]中用k来进行划分,将区间[i.j]转换为计算区间[i][k]+[k+1][j] + s[i][j]

AcWing 282. 石子合并

设有 N 堆石子排成一排,其编号为 1,2,3,…,N。

每堆石子有一定的质量,可以用一个整数来描述,现在要将这 N 堆石子合并成为一堆。

每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。

例如有 4 堆石子分别为 1 3 5 2, 我们可以先合并 1、2 堆,代价为 4,得到 4 5 2, 又合并 1、2堆,代价为 9,得到 9 2 ,再合并得到 11,总代价为 4+9+11=24;

如果第二步是先合并 2、3堆,则代价为 7,得到 4 7,最后一次合并代价为 11,总代价为 4+7+11=22。

问题是:找出一种合理的方法,使总的代价最小,输出最小代价。

输入格式

第一行一个数 N 表示石子的堆数 N。

第二行 N 个数,表示每堆石子的质量(均不超过 1000)。

输出格式

输出一个整数,表示最小代价。

数据范围

1≤N≤300

输入样例:

4
1 3 5 2

输出样例:

22
class Merge_stone:
    def __init__(self):
        N = 310
        self.f = [[0] * N for _ in range(N)]

    def main(self):
        n = int(input())
        prefix_sum = [0] + list(map(int,input().split()))
        # 计算前缀和,便于快速求出区间和
        for i in range(1,n+1):prefix_sum[i] += prefix_sum[i-1]
        # 遍历区间长度,长度范围[1,n]
        for lens in range(2,n+1):
            for i in range(1,n - lens + 2): # 便利区间的起点,起点不能超过n - lens + 1
                l = i # 区间左端点
                r = lens + i - 1 # 区间右端点
                self.f[l][r] = float('inf') # 初始化当前区间的最小代价为一个极大值,方便后面进行min操作
                for k in range(l,r): # 遍历区间的划分点k,划分点的范围为[l,r)
                    # 划分后的最小代价计算就转换为:
                    # f[i][j] = min(f[i][j],f[i][k] + f[k+1][j] + prefixsum[r] - prefixsum[l-1])
                    self.f[l][r] = min(self.f[l][r], self.f[l][k] + self.f[k+1][r] + prefix_sum[r] - prefix_sum[l-1])

        print(self.f[1][n]) # 合并区间[1,n]的最小代价

if __name__ == '__main__':
    meragestone = Merge_stone()
    meragestone.main()

计数类DP

AcWing 900. 整数划分

状态表示:f[i][j]表示从前1,2…,i中选取,刚好构成和为j的方法数
状态属性:刚好构成和为j的方法数
状态计算:将此类问题转化成特殊的完全背包问题来看
求方案数:把集合选0个i,1个i,2个i,…全部加起来

   1. 不选取第i个数:`f[i][j] = f[i-1`][j]
      2. 选取k个第i个数:`f[i][j] = f[i-1][j - ki]`
         第i个数总的选取方法数: `f[i][j] = f[i-1][j] + f[i-1][j-i] + f[i-1][j-i*2] + ... + f[i-1][j-i*k] // j >= i*k`
              计算`f[i][j-i] =           f[i-1][j-i] + f[i-1][j-i*2] + ... + f[i-1][j-i*k] // j >= i*k`
         所以就可以得到:` f[i][j] = f[i-1][j] + f[i][j-i]`

一个正整数 n 可以表示成若干个正整数之和,形如: n = n 1 + n 2 + … + n k n=n_1+n_2+…+n_k n=n1+n2++nk,其中 n 1 ≥ n 2 ≥ … ≥ n k , k ≥ 1 n_1≥n_2≥…≥n_k,k≥1 n1n2nk,k1

我们将这样的一种表示称为正整数 n 的一种划分。

现在给定一个正整数 n,请你求出 n 共有多少种不同的划分方法。

输入格式

共一行,包含一个整数 n。

输出格式

共一行,包含一个整数,表示总划分数量。

由于答案可能很大,输出结果请对 1 0 9 + 7 10^9+7 109+7取模。

数据范围

1≤n≤1000

输入样例:

5

输出样例:

7
class Solution:
    def __init__(self):
        N = 1010
        self.f = [0] * N

    def main(self):
        n = int(input())
        self.f[0] = 1 # 要凑成总和为0,所有数都不选择,方案数为1
        for i in range(1,n+1):
            for j in range(i,n+1):
                self.f[j] = (self.f[j] + self.f[j-i]) % int(1e9+7)
        print(self.f[n])

solution = Solution()
solution.main()

数位统计DP

AcWing 338. 计数问题

  • 计数问题:

  • 分情况讨论:
    以abcdefg来计算,当前位置d,数字1出现的总次数
    1. d的左边三位取值:000 - abc-1,d=1,右侧可取值为 000 - 999 (1000种)
    2. d的左边三位取值:abc
        2.1 d > 1: 右边可取值为:000 - 999 (1000种)
        2.2 d = 1: 右边可取值为:000 - efg (efg+1种)
        2.3 d < 1: 右边可取值为:0种 
    # 边界情况:当x = 0时,情况1就不能从000开始取值,必须从001开始取值,否则就存在前导0,
        当 x = 0,右侧取值为:000 - 999 (1000种),左侧可取值为001 - abc - 1 (abc - 1种)
    

给定两个整数 a 和 b,求 a 和 b 之间的所有数字中 0∼9的出现次数。

例如,a=1024,b=1032,则 a 和 b 之间共有 9 个数如下:

1024 1025 1026 1027 1028 1029 1030 1031 1032

其中 0 出现 10 次,1 出现 10 次,2 出现 7 次,3 出现 3 次等等…

输入格式

输入包含多组测试数据。

每组测试数据占一行,包含两个整数 a 和 b。

当读入一行为 0 0 时,表示输入终止,且该行不作处理。

输出格式

每组数据输出一个结果,每个结果占一行。

每个结果包含十个用空格隔开的数字,第一个数字表示 0 出现的次数,第二个数字表示 1 出现的次数,以此类推。

数据范围

0<a,b<100000000

输入样例:

1 10
44 497
346 542
1199 1748
1496 1403
1004 503
1714 190
1317 854
1976 494
1001 1960
0 0

输出样例:

1 2 1 1 1 1 1 1 1 1
85 185 185 185 190 96 96 96 95 93
40 40 40 93 136 82 40 40 40 40
115 666 215 215 214 205 205 154 105 106
16 113 19 20 114 20 20 19 19 16
107 105 100 101 101 197 200 200 200 200
413 1133 503 503 503 502 502 417 402 412
196 512 186 104 87 93 97 97 142 196
398 1375 398 398 405 499 499 495 488 471
294 1256 296 296 296 296 287 286 286 247
class Count_nums:
    def count_x(self,n,x):
        """
        计算数字小于等于n中各位上x出现的总次数
        :param n:
        :param x:
        :return:
        """
        lens = len(str(n))
        # 从右到左遍历各位上的数字
        res = 0
        for i in range(1,lens+1):
            p = pow(10,lens - i)
            l = n // (p * 10)
            r = n % p
            pi = n // p % 10
            # 以abcdefg,当前位为d
            # 1. 计算左侧为000-abc-1的情况,当前位右侧可取值为p
            if x: res += l * p
            else: res += (l - 1) * p
            # 2. 当左侧为abc,
            # 2.1 当前位的值大于待查询的x,此时有p种取值方式
            if pi > x: res += p
            # 2.2 当前位的值等于x,此时右侧取值方式为:000-efg共计efg+ 1种
            if pi == x: res += (r + 1)
            # 2.3 当前位的值小于x,此时右侧取值方式为0种
            if pi < x: res += 0
        return res

    def main(self):
        while True:
            a,b = map(int,input().split())
            if not a and not b :break
            if a > b: a,b = b,a
            for i in range(10):
                print(self.count_x(b,i) - self.count_x(a-1,i),end=' ')
            print()

if __name__ == '__main__':
    countnums = Count_nums()
    countnums.main()

状态压缩DP

AcWing 291. 蒙德里安的梦想

  • 状态压缩DP:

    • 摆放方块的时候先摆放横着的方块,剩下的格子摆放竖着的方块,横着的方块摆放方法确定了过后,竖着摆放方块的方法也就唯一确定

    • 如何判断当前的摆放方式是否合法:

      1. 每一列上连续的空方块数必须为偶数,才能被竖着的方块摆满
    • 状态表示:每一列的状态用一个二进制数来表示,1表示这个位置摆放了方块,0表示这个位置没有白方格方块

      • f[i][j]:表示从第i-1列伸到第i列的状态为j的所有方案
    • 状态转移:第i-1列如何转移到第i列

      • f[i-1][k]:表示从第i-2列伸到第i-1的状态为k的方案数
      • 必须满足的条件:i-2列伸到i-1列的方块不能与i-1列伸到第i列的方块冲突:即j & k == 0
      • 剩余位置连续0的个数必须为偶数
    • 这里为了优化时间复杂度,预先处理出来哪些状态是合法状态

      • 预处理1:判断 [ 0 , 2 n ) [0,2^n) [0,2n)中的每个状态是否合法
      for i in range(1 << n):
          cnt = 0 # 记录连续0的个数
          isvalid = True # 记录当前状态是否有连续奇数个0,如果不存在连续奇数个0,则认为是合法状态
          for j in range(n): # 遍历当前列
              if i >> j & 1: # 判断当前状态的位置j是否为1
                  if cnt & 1:
                      isvalid = False # 如果cnt是奇数个,则不是合法状态
                      break
              else: cnt += 1 # 说明当前位置为0,需要将cnt计数增加1
          if cnt & 1: isvalid = False # 判断高位是否存在连续奇数个0
          # 例如 4: 0100,前面判断完成后,在进入到最后一次循环前,cnt = 2,判断是合法的,
          # 当运行到最后一次的时候,cnt = 3,但是已经退出了循环,所以最后需要单独判断一次
          self.st[i] = isvalid
          # n = 4, m = 2预处理后合法状态为(连续0的个数均为偶数个)
          # st[0] = 0000
          # st[3] = 0011
          # st[9] = 1001
          # st[12]= 1100
          # st[15]= 1111
          # 预处理2:判断第i-2列伸出来的方块和第i-1列伸出来的方块是否存在冲突
          for j in range(1 << n): # 对于第i列的所有状态
              self.state[j] = []  # 初始化当前状态
              for k in range(1 << n): # 对于第i-1列的所有状态
                  if j & k == 0 and self.st[j | k]:
                  # 1. j & k 表示j和k不能存在相同行摆放了方块
                  # 2. st[j | k]
                  # 已经知道st[] 数组表示的是这一列没有连续奇数个0的情况,
                  # 我们要考虑的是第i - 1 列(第i - 1列是这里的主体)中从第i - 2
                  # 列横插过来的,还要考虑自己这一列(i - 1列)横插到第i列的
                  # 比如:第i - 2列插过来的是 k = 10101,
                  # 第i - 1列插出去到第i列的是j = 01000,
                  # 那么合并后在第i - 1列,到底有多少个1呢?
                  # 自然想到的就是这两个操作共同的结果:两个状态或。 j | k = 01000 | 10101 = 11101
                  # 这个j | k就是当前第i - 1列的到底有几个1,即哪几行是横着放格子的
                  	self.state[j].append(k)
                  	# state[j]表示第j行;j表示第i列真正可行的状态
      

求把 N×M 的棋盘分割成若干个 1×2 的长方形,有多少种方案。

例如当 N=2,M=4 时,共有 5 种方案。当 N=2,M=3 时,共有 3 种方案。

如下图所示:

2411_1.jpg

输入格式

输入包含多组测试用例。

每组测试用例占一行,包含两个整数 N 和 M。

当输入用例 N=0,M=0 时,表示输入终止,且该用例无需处理。

输出格式

每个测试用例输出一个结果,每个结果占一行。

数据范围

1≤N,M≤11

输入样例:

1 2
1 3
1 4
2 2
2 3
2 4
2 11
4 11
0 0

输出样例:

1
0
1
2
3
5
144
51205
class Mondrian_dream:
    def __init__(self):
        self.N = 12
        self.M = 1 << self.N
        self.st = [False] * self.M # 记录空白区域是否存在连续偶数个0,如果是奇数个则置为false
        self.state = [[]] * self.M # 记录合法的状态

    def main(self):
        while True:
            n,m = map(int,input().split())
            # 如果两个数不为0,则是合法输入
            if not n and not m: break
            # 预处理1:判断 [0,2^n)中的每个状态是否合法
            for i in range(1 << n):
                cnt = 0 # 记录连续0的个数
                isvalid = True # 记录当前状态是否有连续奇数个0,如果不存在连续奇数个0,则认为是合法状态
                for j in range(n): # 遍历当前列
                    if i >> j & 1: # 判断当前状态的位置j是否为1
                        if cnt & 1:
                            isvalid = False # 如果cnt是奇数个,则不是合法状态
                            break
                        cnt = 0 # 如果当前位置为1,并且前面不存在奇数个0,则重置cnt状态
                    else: cnt += 1 # 说明当前位置为0,需要将cnt计数增加1
                if cnt & 1: isvalid = False # 判断高位是否存在连续奇数个0
                # 例如 4: 0100,前面判断完成后,在进入到最后一次循环前,cnt = 2,判断是合法的,
                # 当运行到最后一次的时候,cnt = 3,但是已经退出了循环,所以最后需要单独判断一次
                self.st[i] = isvalid
                # n = 4, m = 2预处理后合法状态为(连续0的个数均为偶数个)
                # st[0] = 0000
                # st[3] = 0011
                # st[9] = 1001
                # st[12]= 1100
                # st[15]= 1111
            # 预处理2:判断第i-2列伸出来的方块和第i-1列伸出来的方块是否存在冲突
            for j in range(1 << n): # 对于第i列的所有状态
                self.state[j] = []  # 初始化当前状态
                for k in range(1 << n): # 对于第i-1列的所有状态
                    if j & k == 0 and self.st[j | k]:
                    # 1. j & k 表示j和k不能存在相同行摆放了方块
                    # 2. st[j | k]
                    # 已经知道st[] 数组表示的是这一列没有连续奇数个0的情况,
                    # 我们要考虑的是第i - 1 列(第i - 1列是这里的主体)中从第i - 2
                    # 列横插过来的,还要考虑自己这一列(i - 1列)横插到第i列的
                    # 比如:第i - 2列插过来的是 k = 10101,
                    # 第i - 1列插出去到第i列的是j = 01000,
                    # 那么合并后在第i - 1列,到底有多少个1呢?
                    # 自然想到的就是这两个操作共同的结果:两个状态或。 j | k = 01000 | 10101 = 11101
                    # 这个j | k就是当前第i - 1列的到底有几个1,即哪几行是横着放格子的
                        self.state[j].append(k)
                        # state[j]表示第j行;j表示第i列真正可行的状态
            # 进行dp
            self.f = [[0] * self.M for _ in range(self.N)]  # 第一维表示列,第二维表示伸到当前列的所有可能状态

            self.f[0][0] = 1 # 第0列不横放是一种方案

            for i in range(1,m+1):
                for j in range(1 << n):
                    for k in self.state[j]:
                        self.f[i][j] += self.f[i-1][k]
            print(self.f[m][0])

if __name__ == '__main__':
    mondrian = Mondrian_dream()
    mondrian.main()

AcWing 91. 最短Hamilton路径

1. 状态表示:
f[i][j]:表示到达点j的所有经过点的集合(二进制表示:例如110111,表示经过点0、1、2、4、5,右边是低位)
2. 属性:集合中的最小值
3. 状态计算:
找到j的前一个点k,计算经过k点到达j点的距离能否更新j点的距离
f[i][j] = min(f[i][j], f[i-j][k] + w[k][j])
转移条件:
    1. 状态i的右起第j位必须为1,(1表示经过当前点)
    2. 状态i的右起第k位必须为1,(1表示经过当前点),这样才能从k走到j

给定一张 n 个点的带权无向图,点从 0∼n−1 标号,求起点 0 到终点 n−1 的最短 Hamilton 路径。

Hamilton 路径的定义是从 0 到 n−1 不重不漏地经过每个点恰好一次。

输入格式

第一行输入整数 n。

接下来 n 行每行 n 个整数,其中第 i 行第 j 个整数表示点 i 到 j 的距离(记为 a[i,j])。

对于任意的 x,y,z,数据保证 a[x,x]=0,a[x,y]=a[y,x] 并且 a[x,y]+a[y,z]≥a[x,z]。

输出格式

输出一个整数,表示最短 Hamilton 路径的长度。

数据范围

1≤n≤20
0 ≤ a [ i , j ] ≤ 1 0 7 0≤a[i,j]≤10^7 0a[i,j]107

输入样例:

5
0 2 4 5 1
2 0 6 5 3
4 6 0 8 3
5 5 8 0 5
1 3 3 5 0

输出样例:

18
class Hamilton_distance:
    def __init__(self):
        N = 21
        M = 1 << N
        self.f = [[float('inf')] * N for _ in range(M)]
        self.weight = []

    def main(self):
        n = int(input())
        for i in range(n):
            line = list(map(int,input().split()))
            self.weight.append(line)
        # 初始化0号点到自身的距离为0
        self.f[0][0] = 0
        # 枚举所有状态,一共2^n种状态
        for i in range(1 << n):
            # 枚举每个点
            for j in range(n):
                if i >> j & 1: # 如果i的右起第j位为1
                    for k in range(n):
                        if i >> k & 1: # k表示走到j点之前到达的点k,判断k点到j点的距离能否更新j点的距离
                            self.f[i][j] = min(self.f[i][j], self.f[i - (1 << j)][k] + self.weight[k][j])
        # (1<<n) - 1表示经过所有点
        print(self.f[(1 << n) - 1][n-1])

if __name__ == '__main__':
    hamilton = Hamilton_distance()
    hamilton.main()

树形DP

AcWing 285. 没有上司的舞会

Ural 大学有 N 名职员,编号为 1∼N。

他们的关系就像一棵以校长为根的树,父节点就是子节点的直接上司。

每个职员有一个快乐指数,用整数 Hi 给出,其中 1≤i≤N。

现在要召开一场周年庆宴会,不过,没有职员愿意和直接上司一起参会。

在满足这个条件的前提下,主办方希望邀请一部分职员参会,使得所有参会职员的快乐指数总和最大,求这个最大值。

输入格式

第一行一个整数 N。

接下来 N 行,第 i 行表示 i 号职员的快乐指数 H i H_i Hi

接下来 N−1 行,每行输入一对整数 L,K,表示 K 是 L 的直接上司。(注意一下,后一个数是前一个数的父节点,不要搞反)。

输出格式

输出最大的快乐指数。

数据范围

1≤N≤6000,
− 128 ≤ H i ≤ 127 −128≤H_i≤127 128Hi127

输入样例:

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

输出样例:

5
import sys
sys.setrecursionlimit(1000000)
class Ball:
    def __init__(self):
        N = 6010
        self.f = [[0] * 2 for _ in range(N)]

        self.e = [0] * N
        self.h = [-1] * N
        self.ne = [0] * N
        self.idx = 0

        self.ha = [0] * N

    def add(self,a,b):
        self.e[self.idx] = b
        self.ne[self.idx] = self.h[a]
        self.h[a] = self.idx
        self.idx += 1

    def dfs(self,u):
        self.f[u][1] = self.ha[u] # 如果当前节点参会,快乐值初始化为当前节点的快乐值
        idx = self.h[u]
        while ~idx:
            j = self.e[idx]
            self.dfs(j)
            self.f[u][0] += max(self.f[j][0],self.f[j][1])
            self.f[u][1] += self.f[j][0]
            idx = self.ne[idx]

    def main(self):
        n = int(input())
        hp = [0] * (n + 10)
        for i in range(n):
            s = int(input())
            self.ha[i+1] = s

        for k in range(n-1):
            a,b = map(int,input().split())
            self.add(b,a)
            hp[a] = 1
        root = 1
        while hp[root]:
            root += 1
        self.dfs(root)
        print(max(self.f[root][0],self.f[root][1]))

if __name__ == '__main__':
    ball = Ball()
    ball.main()

记忆化搜索

AcWing 901. 滑雪

给定一个 R 行 C 列的矩阵,表示一个矩形网格滑雪场。

矩阵中第 i 行第 j 列的点表示滑雪场的第 i 行第 j 列区域的高度。

一个人从滑雪场中的某个区域内出发,每次可以向上下左右任意一个方向滑动一个单位距离。

当然,一个人能够滑动到某相邻区域的前提是该区域的高度低于自己目前所在区域的高度。

下面给出一个矩阵作为例子:

 1  2  3  4 5

16 17 18 19 6

15 24 25 20 7

14 23 22 21 8

13 12 11 10 9

在给定矩阵中,一条可行的滑行轨迹为 24−17−2−1。

在给定矩阵中,最长的滑行轨迹为 25−24−23−…−3−2−1,沿途共经过 25 个区域。

现在给定你一个二维矩阵表示滑雪场各区域的高度,请你找出在该滑雪场中能够完成的最长滑雪轨迹,并输出其长度(可经过最大区域数)。

输入格式

第一行包含两个整数 R 和 C。

接下来 R 行,每行包含 C 个整数,表示完整的二维矩阵。

输出格式

输出一个整数,表示可完成的最长滑雪长度。

数据范围

1≤R,C≤300,
0≤矩阵中整数≤10000

输入样例:

5 5
1 2 3 4 5
16 17 18 19 6
15 24 25 20 7
14 23 22 21 8
13 12 11 10 9

输出样例:

25
class Distance:
    def __init__(self):
        N = 310
        self.f = [[0] * N for _ in range(N)]
        self.direction = [[-1,0],[1,0],[0,-1],[0,1]]
        # 存储每个点的高度信息
        self.high = []

    def dp(self,x,y):
        """
        表示从[x,y]为起点,能到达的终点滑行步数
        :param x:
        :param y:
        :return:
        """
        # 如果当前点已经被滑到过,就直接return,避免重复滑
        if self.f[x][y] != 0:return self.f[x][y]
        self.f[x][y] = 1 # 将当前点的初值赋值为1,表示当前点至少能滑行一格
        # 从当前点向四个方向滑行
        for i in range(4):
            new_x = x + self.direction[i][0]
            new_y = y + self.direction[i][1]
            # 如果将要滑行到的点没有超过边界;另外需要满足将要滑行到的点的高度需要低于当前点的高度
            if 0 <= new_x < self.R and 0 <= new_y < self.C and self.high[x][y] > self.high[new_x][new_y]:
                # 递归将要到达的点,每次递归后需要将滑行长度加一
                # 最后从当前点为起点的最大滑行长度就等于当前点和将要滑行到的点的最大值,取最大值
                self.f[x][y] = max(self.f[x][y],self.dp(new_x,new_y) + 1)
        return self.f[x][y]

    def main(self):
        R,C = map(int,input().split())
        self.R = R
        self.C = C
        for i in range(R):
            line = list(map(int,input().split()))
            self.high.append(line)

        res = 0
        for i in range(R):
            for j in range(C):
               res = max(res,self.dp(i,j))
        print(res)

if __name__ == '__main__':
    distance = Distance()
    distance.main()

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值