DP动态规划

1.背包问题

1.1 0/1背包

 1.1.1经典做法

def solve(N,C):  # 从左到右,从上到下 (先种类,再体积)
    for i in range(1,N+1): # N种物品,先1种,再2种......
        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-c[i]]+w[i],dp[i-1][j])  # 装或者不装,找最大值
            
    return dp[N][C]
N,C= map(int,input().split())
n=3000
dp = [[0]*n for i in range(n)]  # 初始化dp数组,预留更大空间
c=[0]*n  # 记录体积
w=[0]*n # 记录价值
for i in range(1,N+1):   #读入N种物品的价值和体积
    c[i],w[i] = map(int,input().split())
print(solve(N,C))

 1.1.2 优化为两行队列存储

def solve(N,C):  # 从左到右,从上到下 (先种类,再体积)
    old = 1
    new = 0
    for i in range(1,N+1): # N种物品,先1种,再2种......
        new,old = old,new  # 交换两行
        for j in range(0,C+1):  # 当前背包体积
            if c[i]>j : dp[new][j] = dp[old][j]
            else: dp[new][j] = max(dp[old][j-c[i]]+w[i],dp[old][j])  # 装或者不装,找最大值
            
    return dp[N][C]
N,C= map(int,input().split())
n=3000
dp = [[0]*n for i in range(2)]  # 初始化dp数组,预留更大空间
c=[0]*n  # 记录体积
w=[0]*n # 记录价值
for i in range(1,N+1):   #读入N种物品的价值和体积
    c[i],w[i] = map(int,input().split())
print(solve(N,C))

1.1.3  优化为数组滚动

def solve(N,C):  # 从左到右,从上到下 (先种类,再体积)
    for i in range(1,N+1): # N种物品,先1种,再2种......
        for j in range(C,c[i]-1,-1):  # 当前背包体积,倒序进行,到c[i]-1,因为范围[c[i],C]
           dp[j] = max(dp[j],dp[j-c[i]]+w[i]) # 选或者不选           
    return dp[C]
 
 
N,C= map(int,input().split())
n=3000
dp = [0]*n  # 初始化dp数组,预留更大空间 一维
c=[0]*n  # 记录体积
w=[0]*n # 记录价值
for i in range(1,N+1):   #读入N种物品的价值和体积,从1开始计数
    c[i],w[i] = map(int,input().split())
print(solve(N,C))

1.2 0/1背包简化版

1.2.1 装箱问题

 把体积看成最优化目标,即最大化体积

def solve(N,C):  # 从左到右,从上到下 (先种类,再体积)
    new = 0
    old = 1
    for i in range(1,N+1): # N种物品,先1种,再2种......
        new,old = old,new
        for j in range(0,C+1):
            if c[i]>j: dp[new][j] = dp[old][j]
            else: dp[new][j] = max(dp[old][j-c[i]]+c[i],dp[old][j])      
    return dp[N][C]
 
 
V = int(input()) # 体积
N = int(input()) # 物品种类
n=21000
dp = [[0]*n for i in range(2)]  # 初始化dp数组,预留更大空间 一维
c=[0]*n  # 记录体积
 
for i in range(1,N+1):   #读入N种物品的价值和体积,从1开始计数
    c[i]=int(input())
print(V-solve(N,V))

数组滚动做法

def solve(N,C):  # 从左到右,从上到下 (先种类,再体积)
    for i in range(1,N+1): # N种物品,先1种,再2种......
        for j in range(C,c[i]-1,-1):  # 当前背包体积,倒序进行,到c[i]-1,因为范围[c[i],C]
           dp[j] = max(dp[j],dp[j-c[i]]+c[i]) # 选或者不选           
    return dp[C]
 
 
V = int(input()) # 体积
N = int(input()) # 物品种类
n=21000
dp = [0]*n  # 初始化dp数组,预留更大空间 一维
c=[0]*n  # 记录体积
 
for i in range(1,N+1):   #读入N种物品的价值和体积,从1开始计数
    c[i]=int(input())
print(V-solve(N,V))

 1.2.2 0/1背包的方案数

# dp[i][j][k] 从数字1~i中取j个,和为k的方案数
dp = [[[0]*2222 for i in range(11)] for j in range(2222)]
for i in range (0,2023):   # 初始化
    dp[i][0][0]=1
for i in range(1,2023):
    for j in range(1,11):
        for k in range (1,2023):
            if k<i:  # 取不到i
                dp[i][j][k] = dp[i-1][j][k]
            else:  # 没有取i + 取i的情况
                dp[i][j][k]=dp[i-1][j][k]+dp[i-1][j-1][k-1]
print(dp[2022][10][2022])

用滚动数组

# dp[i][j][k] 从数字1~i中取j个,和为k的方案数
dp = [[0]*2222 for i in range(11)]
dp[0][0]=1
for i in range(1,2023):
    for j in range(10,0,-1):  #10个数
       for k in range(i,2023):  # 只考虑k>i的情况,k<i的情况直接不变
                dp[j][k]+=dp[j-1][k-1]
print(dp[10][2022])

1.3   完全背包

 CSDN优质文章讲解背包问题

# 代码与0/1背包相似,多了一个k循环用来遍历每种物品拿几个
# dp[i][j] 把前i种物品装入容量为j的背包所获得的最大价值
def solve(n, C):  # 从左到右,从上到下 (先种类,再体积)
    for i in range(1, n + 1):  # N种物品,先1种,再2种......
        for j in range(0, C + 1):
            # if i==1:
            #     dp[i][j]=0
            # else:
            #     dp[i][j]=dp[i-1][j]
            # for k in range(0, j // c[i] + 1):     # k * c[i] <=j  # 在容量为j的背包中放k个
            #     dp[i][j] = max(dp[i][j], dp[i-1][j - k * c[i]] + k * w[i])
            dp[i][j] = dp[i - 1][j]
            if j > c[i]:  # 看装几个可以获得最大价值
                for k in range(0, j // c[i] + 1):  # k * c[i] <=j  # 在容量为j的背包中放k个
                    dp[i][j] = max(dp[i][j], dp[i - 1][j - k * c[i]] + k * w[i])
    return dp[N][C]


N=3011
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))

1.4   分组背包

思路为0/1背包,完全背包的结合,三重循环 

# 状态转移方程 dp[i][j] = max{dp[i-1][j] , dp[i-1][j-c[i][k]]+w[i][k] }
# 滚动数组    dp[j]=max{dp[j] , dp[j-c[i][k]]+w[i][k] }

dp=[0]*N
for i in range(1,n+1):  # 遍历每个组
    for j in range(C,-1,-1) :  # 遍历每个容量
        for k in range(1,C+1):  # 用k枚举第i组的所有物品
            if (j>=c[i][k]):  # 第k个物品能够转入容量为j的背包
                dp[j]=max( dp[j] , dp[j-c[i][k]] + w[i][k] )   # 不装 或者装 第i组第k个
print(dp[C])

1.5    多重背包

1.5.1 转为0/1背包问题

1.5.2 转为与完全背包相似的问题

 2. 线性DP

 2.1 最长公共子序列

 

# 当 ai=bj 时,状态转移方程 dp[i][j] = dp[i-1][j-1] +1
# 当 ai!=bj时,状态转移方程 dp[i][j] = max{ dp[i][j-1] , dp[i-1][j]}

n,m = map(int,input().split())  # B n个元素 A m个元素
a = [0] + list(map(int,input().split()))
b = [0] + list(map(int,input().split()))
dp = [[0]*(m+1) for _ in range(2)]   # 注意这里是m,不是n,进行初始化
now = 0 ;old = 1
for i in range(1,n+1):  # B的长度1-n遍历 
    now,old = old,now
    for j in range(1,m+1):  # A的长度 1-m遍历
        dp[now][j] = max(dp[now][j-1],dp[old][j])  #继承以前的最大值 
        if a[i]==b[j]:
            dp[now][j] = dp[now][j]+1
print(dp[now][m])

扩展

2.2 最长递增子序列 LIS

 

 

N =int(input()) # 对手个数
a = [0]+[int(i) for i in input().split()]  # 记录对手战力值
dp = [0]*(N+1) # 记录以第i个数为结尾的最长递增子序列
dp[1]=1
for i in range(2,N+1):  # 从2-N循环
    for j in range(1,i): # 查找前面的比a[i]小的
        if a[j]<a[i] and dp[j]>dp[i]:   #找到小的同时给他赋值max(dp[j])
            dp[i]=dp[j]
        dp[i]+=1  # 加1,即本身
max(dp)

 方法一:

方法二: 

2.3 编辑距离,字符串转换

"""
A转换为B
删除: A删除最后一个    dp[i][j]=dp[i-1][j]+1
插入: B末尾插入相同的  dp[i][j]=dp[i][j-1]+1
替换: A的末尾直接替换  dp[i][j]=dp[i-1][j-1]+1
"""
# dp[i][j] 表示A的前i个字符转换B的前j个字符所需要的操作步骤
# 将长度为m的A存储在a[1]~a[m]中,不用a[0]和b[0]
"""转移方程"""
# a[i]=b[j],则dp[i][j] = dp[i-1][j-1]
# 其他情况 dp[i][j] = min{dp[i-1][j-1], dp[i-1][j],dp[i][j-1]} +1
a=' '+input()
b=" "+input()
m=len(a)-1
n=len(b)-1
dp = [[0]*(n+1) for i in range(m+1)]
for i in range(1,m+1):dp[i][0]=i
for j in range(1,n+1):dp[0][j]=j
for i in range(1,m+1):   # 遍历a的每一个
    for j in range(1,n+1):  # 遍历b的每一个
        if a[i]==b[j]:
            dp[i][j]=dp[i-1][j-1]
        else:
            dp[i][j]=min([dp[i-1][j],dp[i][j-1],dp[i-1][j-1]])+1
print(dp[m][n])

 2.3.1网格上的DP(过河卒)

x1,y1,x2,y2 = map(int,input().split())  # B点坐标和马的坐标
dp = [[0]*25 for i in range(25)]
s = [[0]*25 for i in range(25)]
x1+=2;y1+=2;x2+=2;y2+=2  # 相当于从(2,2)开始的,坐标轴左移2,右移动2
dp[2][1] = 1 # 初始化,也可以 dp[1][2]=1
# 将马可到达的点全部置为1,即不可达
s[x2][y2]=1
s[x2-2][y2-1]=1;s[x2-2][y2+1]=1;s[x2+2][y2-1]=1;s[x2+2][y2+1]=1;
s[x2-1][y2-2]=1;s[x2-1][y2+2]=1;s[x2+1][y2-2]=1;s[x2+1][y2+2]=1;
for i in range(2,x2+1):
    for j in range(2,y2+1):
        if s[i][j]==1:
            dp[i][j]=0
        else: dp[i][j] = dp[i-1][j]+dp[i][j-1]  # 上面的路径加上左边的路径
print(dp[x1][y1])

 2.4 最大连续子段和

2.5 DP例题

2.5.1 排列数

暴力法,使用内置排列组合

from itertools import *   # 导入itertools 使用排列函数 permutations(list)
n,k = map(int, input().split())  
nums = [i for i in range (1, n+1)]  #1~n
cnt = 0
for num in permutations (nums):     #检查每个排列
    tmp = 0  #记录有多少个折点
    for i in range (n-2) :
        if num[i+1]>num[i+2] and num[i+1]>num[i]:
            tmp += 1 #凸折点
        elif num[i+1]<num[i+2] and num[i+1]<num[i]:
            tmp += 1 #凹折点
    if tmp == k-1 : cnt+=1
print(cnt % 123456)
 

 DP动态规划做法

2.5.2 数字三角形

n = int(input())
a = [list(map(int,input().split())) for i in range(n)]
# 数组a[][] 同时当成dp[][]用
# 从上到下遍历
for i in range(1,n): #循环n-1次
    for j in range(0,i+1): # 第i层有i个元素
        if j==0:
            a[i][j]+=a[i-1][j]
        elif j==i:
            a[i][j]+=a[i-1][j-1]
        else:
            #a[i][j]=max(a[i-1][j-1],a[i-1][j])
            a[i][j]+=max(a[i-1][j-1:j+1])
if n&1: #奇数,位运算
    print(a[-1][n//2])   # 1 2 3   # 3//2=1
else:
    print(max(a[-1][n//2-1],a[-1][n//2]))  # 1 2 3 4  #4//2=2

3 状态压缩DP(Python考的概率很小)

3.1 含义和表示

结合位运算,通过二进制来表示有或者没有。

"""
<< 右移运算
a=1<<(n-1)  即右移n-1位
1<<2        100

通过或运算实现相加去重
temp |=(1<<(n-1))

1<<2    100
temp=10
temp |=(1<<2)    110
"""

"""
1<<(i-1) &a  判断第i为是否为1
a|(1<<(i-1))   将第i为改为1
a&(a-1)   去掉最后一个1
快速幂需要借鉴位运算

"""

3.2 状态DP例题糖果

 例如有  2 3 5三种口味,可以用 10110 表示!

 

import sys
import collections
import itertools
import heapq
sys.setrecursionlimit(300000)


n,m,k = map(int,input().split())
tot=(1<<m)-1   # tot: 二进制是m个1,表示所有m种口味
dp=[-1 for _ in range(1<<20)]   # 全部初始化为-1,同时将大小设置大一点
dp[0]=0  #
kw = []
for _ in range(n):  # 存糖果
    kw.append([int(i) for i in input().split()])

for c in kw:  # 遍历每一包糖果
    temp=0
    for x in c:   #[ 2 ,3 ,4]
        temp|=(1<<(x-1))   # 状态压缩
    for i in range(tot+1):    # 口味是从0-tot
        if (dp[i]==-1): # 没有得到过该种口味,直接跳过
            continue
        newcase = temp| i   # 得到新的口味
        if (dp[newcase] ==-1 or (dp[newcase]>dp[i]+1)):  # 找到更少的口味组合
            dp[newcase]=dp[i]+1
print(dp[tot])

# 有点类似 Dijstra , SPFA , BEll_Floyd

 3.3 矩阵计数例题

# 0 用0表示  X 用1表示
# 横,竖不能出现连续的3个1

# dp[i][j][k] 第i行的合法状态为j,前一行为k
def check(x):
    num=0
    while(x):
        if x&1: num+=1   #发现一个1
        else:   num=0    #不是连续的重新计数
        if num==3:
            return False
        x>>=1   # 右移一次
   
N,M = list(map(int,input().split()))
s=2**M
row =[]
for i in range(s):  #可能的范围 0000-1111
    if check(i):
        row.append(i)
dp=[[[0 for k in range(s)] for j in ragne(s)] for k in range(N)]
for i in row:
    dp[0][i][0]=1
for i in range(1,N):   # 遍历每一行
    for j in row:   # 连续检测三列是否合法
        for k in row:
            for p in row:
                if j&k&p==0:
                    dp[i][j][k]+=dp[i-1][k][p]
ans=0
for j in row:
    ans+=sum(dp[N-1][j])
print(ans)

3.4 回路计数例题(没看懂)

 

from math import gcd
m=1<<22
dp=[[0 for j in range(22)] for i in range(m)]
dist = [[False for j in range(22)] for i in range(22)]    # 矩阵存图
for i in range(1,22):
    for j in range(1,22):
        if gcd(i,j)==1:
            dist[i][j]=True
dist[2][1]=1
for S in range(2,m-1):  # 10-111110
    for j in range(1,22):
        if S>>j&1:
            for k in range(1,22):
              if S-(1<<j)>>k &1 and dist[k][j]:
                  dp[S][j]+=dp[S-(1<<j)][k]

ans=0
for i in range(2,22):
    ans+=dp[m-2][i]
print(ans)

4 树形DP

DFS(不用vis数组)+ 树结构

 4.1 生命之树 

 

 即要我们找最大子树

import sys
import collections
import itertools
import heapq
sys.setrecursionlimit(300000)



def dfs(u,fa):
    global ans
    for son in t[u]:
        if son !=fa:
            dfs(son,u)  # 遍历搜索
            if dp[son]>0:  # 子节点大于0,父结点就添加
                dp[u]+=dp[son]
    ans=max(ans,dp[u])  

n=int(input())
dp=[0]*(n+1)
dp[1:n]=map(int,input().split())   # 不用dp[0]
t=[[] for i in range(n+1)]  # tree,临接表矩阵
for i in range(n-1):  # 存n-1条边
    u,v=map(int,input().split())
    t[u].append(v)  #记录邻居
    t[v].append(u)
ans=0
dfs(1,-1)
print(ans)

#tt=[[]*10]  这样创建里面只有1个
#tt=[[] for i in range(10)]

4.2 大臣的旅费例题

 两次DFS有贪心的思想,第一次DFS找到最远的,第二次一定找到最远的(不适合有负数的情况)

树形DP只要一次DFS

 给出两种实现方式,要标记数组和不要标记数组,都是只遍历一次,保证不回头遍历

import sys
import collections
import itertools
import heapq
sys.setrecursionlimit(300000)

##def dfs(u):
##    global ans
##    vis[u]=1   #标记为访问过
##    for v,c in e[u]:  # 遍历u的邻居v,费用c
##        if vis[v]==1:
##            continue
##        dfs(v)
##        ans = max(ans ,dp[u]+dp[v]+c)
##        dp[u]=max(dp[u],dp[v]+c)
##
##n=int(input())
##e=[[] for i in range(n+1)]  # 存图
##for i in range(n-1):
##    a,b,c=map(int,input().split())
##    e[a].append((b,c))  # 临接表存边
##    e[b].append((a,c))
##
##ans=0
##vis=[0 for i in range(n+1)]
##dp=[0 for i in range(n+1)]
##dfs(1)
##print(ans*10 + ans*(ans+1)//2)  # 等差数列求和


def dfs(u,fa):
    global ans
    for v,c in e[u]:  # 遍历u的邻居v,费用c
        if v!=fa:
            dfs(v,u)
            ans = max(ans ,dp[u]+dp[v]+c)
            dp[u]=max(dp[u],dp[v]+c)

n=int(input())
e=[[] for i in range(n+1)]  # 存图
for i in range(n-1):
    a,b,c=map(int,input().split())
    e[a].append((b,c))  # 临接表存边
    e[b].append((a,c))

ans=0
dp=[0 for i in range(n+1)]
dfs(1,-1)
print(ans*10 + ans*(ans+1)//2)  # 等差数列求和

4.3 蓝桥舞会

 

import os
import sys

n=int(input())  # 员工数
h=[0]
h.extend(list(map(int,input().split())))    # 第i个员工的快乐指数,从索引1开始
# extend()将可迭代的所有元素(列表、元组、字符串等)添加到列表的末尾。可实现从索引为1开始输入
son={}  # 字典
pre=[0 for _ in range(n+1)]     # 存放各自的上司
for _ in range(n-1):
  a,b=map(int,input().split())
  pre[a]=b  # 表示a的上司是b,a的父结点是b
  # get() 函数返回指定键的值。
  # 若字典中没有设置这个键,也没有设置默认的值,输出 None
  if son.get(b,0)==0:son[b]=[a]     # 如果父结点b还没有子节点,说明a是b的子节点
  else:son[b].append(a)     # 如果父结点b已经有子节点了,就让a接在子节点列表里面
root=0  # 根节点初始值
# 找根节点
for i in range(1,n+1):
  if pre[i]==0:     # 如果某个结点没有父结点
    root=i  # 说明这个结点就是根节点
    break
dp=[[0]*2 for _ in range(n+1)]
def DFS(u):     # 深度优先搜索
  dp[u][0]=0    # 底层搜索出口,没有其它结点时,当结点u不被选择,快乐指数为0
  dp[u][1]=h[u] # 底层搜索出口,没有其它结点时,当结点u被选择,快乐指数为他自己的
  for v in son.get(u,[]):   # 遍历结点u的子节点
    DFS(v)  # 对子节点搜索
    dp[u][0]+=max(dp[v][0],dp[v][1])    # 状态转移方程
    dp[u][1]+=dp[v][0]
DFS(root)   # 对根节点进行搜索
print(max(dp[root][0],dp[root][1]))     # 选根节点被选与不被选的最优值

5 数位DP

5.1 数位DP介绍

dp[ i ] :i位数的每种数码有多少个,将0也看成占位符,后期再特别处理。

例如一位数:0-9

        两位数:00-99

        三位数:000-999

dp[ i ] 的计算

递推计算 

 排列组合计算(总共10**i个数字,每个数字上面有i位,10个数均等出现)

 

5.2 统计所有数码的出现个数 

 

 

import sys
import collections
import itertools
import heapq
sys.setrecursionlimit(300000)


def solve(x):   #计算[1,x]中的每个数码的个数
    n=tuple(map(int,str(x)))  #把一个数按位分解 123 —> (1,2,3)
    n=n[::-1]  # (1,2,3)—> (3,2,1)
    for i in range(len(n)-1,-1,-1):  # 从高到低处理x的每一位 (2,1,0)
        for j in range(10):
            cnt[j]+=dp[i]*n[i]
        for j in range(n[i]):   # (1)特判最高位比n[i]小的数字
            cnt[j]+=ten[i]
        u=0
        for j in range(i-1,-1,-1):
            u=u*10+n[j]
        cnt[n[i]]+=u+1   #(2)特判最高位的数字n[i],
        cnt[0]-=ten[i]   # 特判前导0  ,对前导0去除  
            

ten=[0]*15
ten[0]=1    # ten[i] : 10的i次方
dp=[0]*15
for i in range(1,15):   #预计算dp[]
    # dp[i]=i*10**(i-1)  这个和下面一样,但是运算速度比下面慢
    dp[i] = i*ten[i-1]  # 或者dp[i]=dp[i-1]*10+ten[i-1]
    ten[i]=ten[i-1]*10
b=int(input())
cnt=[0]*15      # 答案cnt[i] ,数字“i”出现了多少次
solve(b)
for i in range(10):
    print(cnt[i],end=" ")  # 打印每个数码出现的次数

6. 区间DP

 

 

import os
import sys

n = int(input())  # n堆石子
sz = [int(i) for i in input().split()]  # 每堆石子的数量
w = [0 for _ in range(len(sz)+1)]
for i in range(1, len(sz)+1):
    w[i] = w[i-1] + sz[i-1]   # w[i]表示前i项和
# print(w)
dp = [[float('Inf') for i in range(n + 3)] for j in range(n + 3)]  # n+1报错
for i in range(1, n + 1):
    dp[i][i] = 0  # 每堆石子自己不用合并,是0花费
for len in range(2, n + 1):  # 最小合并为2堆,最大合并为n堆
    for i in range(1, n - len + 2):
        j = i + len - 1
        for k in range(i, j + 2):
            dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + w[j]-w[i-1])
print(dp[1][n])

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值