蓝桥杯DP专题

前言

        DP 是一种容易理解的计算思想。有一些问题有两个特征:重叠子问题、最优子结构。用DP 可以高效率地处理具有这两个特征的问题。DP中有很多经典的基础问题,例如斐波那契数列问题、 爬楼梯问题、01背包问题、完全背包问题、最长子序列问题等等。 下面用一系列的子问题来讲解DP的四大基本问题:状态设计状态转移方程 DP 代码实现滚动数组。理解dp数组的含义和初始化DP数组是最为关键的两个部分。

子序列问题

最长公共子序列问题

        a,b是两个不相同的数组,求这两个数组的最长公共子序列。这里的dp数组的含义是:dp[i][j]代表的是第i - 1个位置和j - 1个位置相同的个数。初始化为0代表的是a数组的第0个位置前和b数组的第0个位置前的元素没有相同的。

N, M = map(int, input().split())
a = [int(k) for k in input().split()]
b = [int(r) for r in input().split()]
# 定义dp[i][j]数组dp的第i - 1个位置和j - 1个位置相同的个数
dp = [[0] * (M + 10) for _ in range(N + 10)]
ans = 0
for i in range(1, N + 1):
  for j in range(1, M + 1):
    if a[i - 1] == b [j - 1]:
      dp[i][j] = dp[i - 1][j - 1] + 1
    else:
      dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
    if dp[i][j] > ans:
        ans = dp[i][j]

print(ans)

蓝桥杯真题

密码脱落
题目描述
X 星球的考古学家发现了一批古代留下来的密码。
这些密码是由 A、B、C、D 四种植物的种子串成的序列。仔细分析发现,这些密码串当初应该是前后对称的(也就是我们说的镜像串)由于年代久远,其中许多种子脱落了,因而可能会失去镜像的特征。
你的任务是:给定一个现在看到的密码串,计算一下从当初的状态,它要至少脱落多少个种子,才可能会变成现在的样子。
输入描述
输入一行,表示现在看到的密码串(长度不大于 1000)。
输出描述
要求输出一个正整数,表示至少脱落了多少个种子。

s = input()
s_ = s[::-1]
length = len(s)
dp = [[0]*(length + 1) for _ in range(length + 1)]
def function():
    if s == s_:
        return 0
    for i in range(1, length + 1):
        for j in range(1, length + 1):
            if s[i - 1] == s_[j - 1]:
                dp[i][j] = dp[i - 1][j - 1] + 1
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
    return length - dp[-1][-1]


print(function())

        本题就是在暗中考察两个子序列的最大公共子序列的求解方法。 

最长重复子数组

class Solution:
    def findLength(self, nums1: List[int], nums2: List[int]) -> int:
        # 创建一个二维数组 dp,用于存储最长公共子数组的长度
        dp = [[0] * (len(nums2) + 1) for _ in range(len(nums1) + 1)]
        # 记录最长公共子数组的长度
        result = 0
        # 遍历数组 nums1
        for i in range(1, len(nums1) + 1):
            # 遍历数组 nums2
            for j in range(1, len(nums2) + 1):
                # 如果 nums1[i-1] 和 nums2[j-1] 相等
                if nums1[i - 1] == nums2[j - 1]:
                    # 在当前位置上的最长公共子数组长度为前一个位置上的长度加一
                    dp[i][j] = dp[i - 1][j - 1] + 1
                # 更新最长公共子数组的长度
                if dp[i][j] > result:
                    result = dp[i][j]

        # 返回最长公共子数组的长度
        return result

最长递增子序列

最长连续递增子序列

        这里dp数组的含义是以i为下标的数字的最长递增子序列的长度。因为每一个单独的数字都是一个递增的子序列,所以dp数组初始化为1。

N = int(input())
arr = [int(k) for k in input().split()]
# dp数组的含义dp[i]为第1 ~ i个数的最长递增数
dp = [1]*(N + 10)
for i in range(1, N):
    if arr[i] > arr[i - 1]:
        dp[i] = dp[i - 1] + 1

print(max(dp))

最长非连续递增子序列

        dp数组的含义与连续型的递增子序列相同,不同的是在于更新dp数组的判断条件。

N = int(input())
arr = [int(k) for k in input().split()]
# dp数组的含义dp[i]为第1 ~ i个数的最长递增数
dp = [1]*(N + 10)
for i in range(1, N):
    for j in range(0, i):
        if arr[i] > arr[j]:
            dp[i] = max(dp[j] + 1, dp[i])

print(max(dp))

蓝桥杯真题

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

import bisect
n = int(input())
a = [0] + [int(i) for i in input().split()]
b = [a[1]]
for i in range(1, n + 1):
    ind = bisect.bisect_left(b, a[i])
    if ind == len(b):
        b.append(a[i])
    else:
        b[ind] = a[i]

print(len(b))

背包问题

        主要的两类问题:01背包问题完全背包问题。

01背包问题

# 二维dp
# 1.明确dp数组的含义---以二维为例dp[i][j],在0~i个物品中,任取放入容量为j的背包中
# 2.明确递推公式---不放物品dp[i - 1][j],放物品dp[i - 1][j - weight[i]] + value[i]
# 3.如何初始化---这个重要
# 4.循环---第一层遍历物品,第二行遍历背包

# 一维dp
# 1.明确dp数组的含义---以一维为例dp[j],任取放入容量为j的背包中
# 2.明确递推公式---不放物品i,dp[j],放物品dp[j - weight[i]] + value[i]
# 3.如何初始化---dp[0] = 0
# 4.遍历顺序---先遍历物品,后遍历背包
# for i in range(nums):
#     for j in range(max_weight,weight[i] - 1,-1):

完全背包问题

# 物品数量无限次使用
# 1.明确dp数组的含义---以一维为例dp[j],任取放入容量为j的背包中
# 2.明确递推公式---不放物品i,dp[j],放物品dp[j - weight[i]] + value[i]
# 3.如何初始化---dp[0] = 0
# 4.遍历顺序---先遍历物品,后遍历背包
# for i in range(nums):
#     for j in range(weight[i],max_weight + 1):

压缩DP

        状态压缩 DP 的应用背景是以集合为状态,且集合可以用二进制数来表示,用二进制数的位运算来处理。集合问题一般是指数级复杂度的,例如:子集问题,假设元素无先后关系,那么共有 2 n个子集;排列问题,对所有元素进行全排列,共有 n!个全排列。
        对状态压缩 DP 这种技巧概括如下:集合的状态(子集或排列)如果用二进制数来表示, 并用二进制数的位运算来遍历和操作,那么又简单又快。

糖果
题目描述
糖果店的老板一共有M种口味的糖果出售。为了方便描述,我们将M种口味编号1~M。
小明希望能品尝到所有口味的糖果。遗憾的是老板并不单独出售糖果,而是  颗一包整包出售。幸好糖果包装上注明了其中飞颗糖果的口味,所以小明可以在买之前就知道每包内的糖果口味。给定N包糖果,请你计算小明最少买几包,就可以品尝到所有口味的糖果。
输入描述
第一行包含三个整数N,M,K。
接下来N行每行K个整数,代表一包糖果的口味。
输出描述
输出一个整数表示答案。如果小明无法品尝所有口味,输出-1

N, M, K = map(int, input().split())
tot = (1 << M) - 1
arr = []
step = 0
# 全部口味的情况
dp = [-1 for _ in range(1 << 20)]
dp[0] = 0

for _ in range(N):
    arr.append(list(map(int, input().split())))

# 用k来遍历每包糖果
for k in arr:
    step = 0
    # 将口味转化为二进制
    for x in k:
        step |= (1 << (x - 1))
    for i in range(tot + 1):
        if dp[i] == -1:
            continue
        newcase = i | step
        if dp[newcase] == -1 or dp[newcase] > dp[i] + 1:
            dp[newcase] = dp[i] + 1

print(dp[tot])

树形DP

        树形 DP 是非线性 DP,是在树这种数据结构上进行的 DP:给出一棵树,要求以最少的代价(或取得最大收益)完成给定的操作。

生命之树
题目描述
在 X森林里,上帝创建了生命之树。
他给每棵树的每个节点(叶子也称为一个节点)上,都标了一个整数,代表这个点的和谐值。
上帝要在这棵树内选出一个非空节点集S,使得对于S中的任意两个点 a,b,都存在一个点列 a,u1,u2,··,b使得这个点列中的每个点都是S里面的元素目序列中相邻两个点间有一条边相连。
在这个前提下,上帝要使得S中的点所对应的整数的和尽量大。
这个最大的和就是上帝给生命之树的评分。
经过atm的努力,他已经知道了上帝给每棵树上每个节点上的整数。但是由于atm 不擅长计算,他不知道怎样有效的求评分。他需要你为他写一个程序来计算一棵树的分数。
输入描述
第一行一个整数  表示这棵树有n个节点。
第二行n个整数,依次表示每个节点的评分。
接下来n-1行,每行2个整数 u,v,表示存在一条u到v的边。由于这是一棵树,所以是不存在环的。
输出描述
输出一行一个数,表示上帝给这棵树的分数。

import sys
sys.setrecursionlimit(50020)
def dfs(u: int, fa: int):
    global res
    # 遍历所有的树节点
    for son in tree[u]:
        if son != fa:
            dfs(son, u)
            if dp[son] > 0:
                dp[u] += dp[son]
    res = max(res, dp[u])


N = int(input())
# 创建一个邻接表
tree = [[] for _ in range(N + 1)]
# dp[i]以i为根节点的最大权值
w = [0] + [int(i) for i in input().split()]
res = 0
# 创建每一个结点的权值
dp = [0 for _ in range(N + 1)]
for i in range(1, N + 1):
    dp[i] = w[i]
# 建立一个邻接表
for i in range(N - 1):
    u, v = map(int, input().split())
    tree[u].append(v)
    tree[v].append(u)


dfs(1, 0)
print(res)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值