DP:将一个问题拆成几个子问题,分别求解这些子问题,即可推断出大问题的解。
思路主要是三步走:
一.确定(定义)状态;就是我们要保存哪些东西,这个总要知道吧.(可以从时间或者是空间上进行划分)
二.确定决策并写出状态转移方程.(当我们确定了状态,就需要知道状态是怎么转移的) 感觉这个就是描述子问题的通式
三.寻找边界条件。英文我们的状态转移方程是一个递推式,所以需要一个或者是多个终止条件(或者说是边界条件吧)
而在状态保存方面,我们可以采用多种形式进行保存,一般有以下三种形式
第一种:可以采用多个变量进行保存。第二种:可以采用一维数组进行保存。第三种:采用二维数组进行保存。
如果我们需要多个状态需要保存的话,这就涉及到多维的问题了,那就需要使用多个数据形式进行保存,例如使用两个二维数组之类的方法对状态进行保存。
总体上思路差不多是这个样子,最重要的其实还是要找到状态转移方程,找到了这个问题解决了一大半了。
背包问题(01背包问题,完全背包,多维背包问题)
【注】关于“恰好装满”
- 如果要求恰好装满背包,可以在初始化时将
dp[0] / dp[i][0]
初始化0
,其他初始化为-INF
。这样即可保证最终得到的dp[N] / dp[N][M]
是一种恰好装满背包的解; - 如果不要求恰好装满,则全部初始化为
0
即可。 - 可以这样理解:初始化的 dp 数组实际上就是在没有任何物品可以放入背包时的合法状态。
- 如果要求背包恰好装满,那么此时只有容量为 0 的背包可能被价值为 0 的物品“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,它们的值就都应该是
-INF
。 - 如果背包并非必须被装满,那么任何容量的背包都有一个合法解,即“什么都不装”,这个解的价值为0,所以初始时状态的值也全部为 0 。
- 如果要求背包恰好装满,那么此时只有容量为 0 的背包可能被价值为 0 的物品“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,它们的值就都应该是
01 背包:中每个物品只有一个,所以只存在选或不选;(体积或者是重量,价值)
完全背包:中每个物品可以选取任意件。
多维背包问题:01背包问题的扩展。(体积和重量,价值)
在这里我就用python实现了,在下面链接中使用得C++实现,就不在赘述了:动态规划
在下面分析的过程中,以前面三个方向来分析,确定或者是定义状态,动态转移方程,边界条件
01背包问题
- 定义:
dp[i][j] := 从前 i 个物品中选取总重量不超过 j 的物品时总价值的最大值
i
从 1 开始计,包括第i
个物品 - 初始化
dp[0][j] = 0
- 状态转移
dp[i][j] = dp[i-1][j] if j < w[i] (当前剩余容量不够放下第 i 个物品) = max{ else (取以下两种情况的最大值) dp[i-1][j], // 不拿第 i 个物品 dp[i-1][j-w[i]] + v[j] // 拿第 i 个物品 }
dp数组中,行是物品的种类(从第一个物品到最后一个物品),列是总的重量从1到C
def knapsack02(w,v,C):#w是重量数组,v是价值数组,C是总的体积或者是重量
length=len(w)
if length==0:
return 0
memo=[[-1 for j in range(C+1)]for i in range(len(w))]
for j in range(0,C+1):
if j>w[0]:#先写第一排
res=v[0]
else:
res=0
memo[0][j]=max(res,0)
for i in range(1,length):
for j in range(0,C+1):
memo[i][j]=memo[i-1][j]
if j>=w[i]:
memo[i][j]=max(memo[i][j],v[i]+memo[i-1][j-w[i]])
return memo[length-1][C]
后面还有优化空间:具体代码可以看上面的那个链接,就不再写了。
二维 DP(滚动数组)
- 在上述递推式中,
dp[i+1]
的计算实际只用到了dp[i+1]
和dp[i]
; - 因此可以结合奇偶,通过两个数组滚动使用来实现重复利用。
一维 DP
- 定义:
dp[j] := 重量不超过 j 公斤的最大价值
- 递推公式
dp[j] = max{dp[j], dp[j-w[i]] + v[i]} 若 j > w[i]
完全背包
问题描述
01 背包中每个物品只有一个,所以只存在选或不选;
完全背包中每个物品可以选取任意件。
注意:本题要求是背包恰好装满背包时,求出最大价值总和是多少。如果不能恰好装满背包,输出 NO
二维 DP(无优化)
- 直观思路:在 01 背包的基础上在加一层循环
- 递推关系:
dp[0][j] = 0 dp[i][j] = max{dp[i - 1][j - k * w[i]] + k * v[i] | 0 <= k}
优化下面的
for (int i = 1; i <= N; i++) {
for (int j = 0; j <= V; j++) {
if (w[i] > j)
dp[i][j] = dp[i - 1][j];
else
dp[i][j] = max(dp[i - 1][j], dp[i][j - w[i]] + v[i]);
// dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]); // 对比 01 背包
// ---------(唯一区别)
}
}
多维背包问题
在这里我们讨论二维01背包问题(重量,体积,价值)
即给定n种物品和一个背包。物品i的重量是Wi,体积为Bi,其价值为Vi,背包的容量为C,容积为D.问:应该如何选择转入背包的物品,使得物品总价最大。
首先我们分析二维0-1背包的最优子结构在,其实非常简单只要在在0-1背包的基础上扩充m将其添上一维k即可
定义:
m[i][j][k]:表示第i件物品放入容量为j,容积为k的背包时的最大价值。
对于第i件物品只有两种状况:放入与不放入。
动态转移方程:
接下来我们可以m(i,j,k)的计算公式
可以放入时条件(j >= w&& k >= b): m(i,j,k) = max( m[i+1][j][k], m[i+1, j-w, k-b) + Vi )
不可放入时 :m(i,j,k) = m[i+1][j][k]
说明:
第n个物品
m[n][j][k] 放入时m = v
不放入时 m = 0
C1 = [3,2,6,7,1,4,9,5]
C2 = [6,2,4,6,7,3,8,5]
V = [6,3,5,8,3,1,6,9]
#Count = [3,5,1,9,3,5,6,8]
target1 = 20
target2 = 25
n = len(C1)
F = [[0] * (target2+1) for i in range(0,target1+1)]
for i in range(0,n):
for j in reversed(range(C1[i],target1+1)):
for m in reversed(range(C2[i],target2+1)):#逆序遍历
F[j][m] = max(F[j][m],F[j-C1[i]][m-C2[i]] + V[i])
print (F[target1][target2])
硬币问题(硬币找零,硬币组合问题)
硬币找零
问题描述
给定不同面额的硬币 coins 和一个总金额 amount。
编写一个函数来计算可以凑成总金额所需的最少的硬币个数。
如果没有任何一种硬币组合能组成总金额,返回 -1。
示例 1:
输入: coins = [1, 2, 5], amount = 11
输出: 3
解释: 11 = 5 + 5 + 1
示例 2:
输入: coins = [2], amount = 3
输出: -1
说明:
你可以认为每种硬币的数量是无限的。
思路
- 定义:
dp[i] := 组成总金额 i 时的最少硬币数
- 初始化:
dp[i] = 0 若 i=0 = INF 其他
- 状态转移
dp[j] = min{ dp[j-coins[i]] + 1 | i=0,..,n-1 } 其中 coins[i] 表示硬币的币值,共 n 种硬币
def coin_change(coins,amount):
if amount<=0 or len(coins)<=0:
return 0
dp=[0 for _ in range(amount+1)]
dp[0]=1
for coin in coins:
for i in range(coin,amount+1):#在这里要包含上amount
if i==coin:
dp[i]=1
elif dp[i]==0 and dp[i-coin]!=0:
dp[i]=dp[i-coin]+1
elif dp[i-coin]!=0:
dp[i]=min(dp[i],dp[i-coin]+1)
print(dp)
res=dp[-1]
if dp[-1]==0:
res=-1
return res
coins=[1,2,5]
print(coin_change(coins,11))
硬币组合问题
硬币可以重复使用,看组合成我们需要的种类需要多少种
def coin_change(coins,amount):
if amount==0:
return 1
if len(coins)==0 or coins==None:
return 0
dp=[0 for _ in range(amount+1)]
dp[0]=1
for coin in coins:
for i in range(coin,amount+1):
dp[i]=dp[i]+dp[i-coin]
return dp[amount]
coins=[1,2,5]
print(coin_change(coins,5))
最长公共子串,最长公共子序列,最长递增子序列,最长回文子序列,最长连续子序列和
最长公共子串
思路 - DP
-
DP 定义
- 记
s[0:i] := s 长度为 i 的**前缀**
- 定义
dp[i][j] := s1[0:i] 和 s2[0:j] 最长公共子串的长度
dp[i][j]
只有当s1[i] == s2[j]
的情况下才是s1[0:i] 和 s2[0:j] 最长公共子串的长度
- 记
-
DP 初始化
dp[i][j] = 0 当 i=0 或 j=0 时
-
DP 更新
dp[i][j] = dp[i-1][j-1] + 1 if s[i] == s[j] = ; else pass
def find_lcsubstr(s1, s2):
m=[[0 for i in range(len(s2)+1)] for j in range(len(s1)+1)] #生成0矩阵,为方便后续计算,比字符串长度多了一列
mmax=0 #最长匹配的长度
p=0 #最长匹配对应在s1中的最后一位
for i in range(len(s1)):
for j in range(len(s2)):
if s1[i]==s2[j]:
m[i+1][j+1]=m[i][j]+1
if m[i+1][j+1]>mmax:
mmax=m[i+1][j+1]
p=i+1
return s1[p-mmax:p],mmax #返回最长子串及其长度
print find_lcsubstr('abcdfg','abdfg')
最长公共子序列
- 求两个序列的最长公共字序列
- 示例:s1: "BDCABA" 与 s2:"ABCBDAB" 的一个最长公共字序列为 "BCBA"
- 最长公共子序列不唯一,但是它们的长度是一致的
- 子序列不要求连续
思路
- DP 定义
- 记
s[0:i] := s 长度为 i 的**前缀**
- 定义
dp[i][j] := s1[0:i] 和 s2[0:j] 最长公共子序列的长度
- 记
- DP 初始化
dp[i][j] = 0 当 i=0 或 j=0 时
- DP 更新
- 当
s1[i] == s2[j]
时dp[i][j] = dp[i-1][j-1] + 1
- 当
s1[i] != s2[j]
时dp[i][j] = max(dp[i-1][j], dp[i][j-1])
- 当
- 完整递推公式
dp[i][j] = 0 当 i=0 或 j=0 时 = dp[i-1][j-1] + 1 当 `s1[i-1] == s2[j-1]` 时 = max(dp[i-1][j], dp[i][j-1]) 当 `s1[i-1] != s2[j-1]` 时
def lengthOfLCS(s1,s2):
n1=len(s1)
n2=len(s2)
dp=[[0 for _ in range(len(s2)+1)]for _ in range(len(s1)+1)]
for i in range(1,n1+1):
for j in range(1,n2+1):
if(s1[i-1]==s2[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 dp[n1][n2]
print(lengthOfLCS('abcdef','abcef'))
最长递增子序列
#最长递增子序列,dp表示递增子序列的数量
#nums[i]放在nums[j]之后
def LIS(arr):
if len(arr)==1:
return 1
dp=[1 for _ in range(len(arr))]
for i in range(len(arr)-1):
for j in range(i+1):
if arr[i+1]>arr[j]:
dp[i+1]=max(dp[i+1],dp[j]+1)
return max(dp)
arr=[-2,6,-1,5,4,-7,2,3]
print(LIS(arr))
最长回文子序列
class Solution:
def longestPalindromeSubseq(self, s):
# write your code here
if s == s[::-1]:
return len(s)
dp = [[1 for i in range(len(s))] for j in range(len(s))]
for j in (range(len(s))):
for i in range(j - 1, -1, -1):
if s[i] == s[j]:
dp[i][j] = 2 + dp[i + 1][j - 1] if i + 1 <= j - 1 else 2
else:
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])
return dp[0][len(s) - 1]
s=Solution()
print(s.longestPalindromeSubseq('abca'))#aba
最长连续子序列和
#和股票有关
def LCS(arr):
if len(arr)==1:
return arr[0]
dp=res=arr[0]
for i in range(1,len(arr)):
dp=max(arr[i],dp+arr[i])
res=max(dp,res)
return res
arr=[-2,6,-1,5,4,-7,2,3]
print(LCS(arr))
编辑距离
问题描述
给定两个单词 word1 和 word2,计算出将 word1 转换成 word2 所使用的最少操作数。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符
示例:
输入: word1 = "horse", word2 = "ros"
输出: 3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
- 注意:编辑距离指的是将 word1 转换成 word2
思路
-
用一个 dp 数组维护两个字符串的前缀编辑距离
-
DP 定义
- 记
word[0:i] := word 长度为 i 的**前缀子串**
- 定义
dp[i][j] := 将 word1[0:i] 转换为 word2[0:j] 的操作数
- 记
-
初始化
dp[i][0] = i // 每次从 word1 删除一个字符 dp[0][j] = j // 每次向 word1 插入一个字符
-
递推公式
word1[i] == word1[j]
时dp[i][j] = dp[i-1][j-1]
word1[i] != word1[j]
时,有三种更新方式,取最小// word[1:i] 表示 word 长度为 i 的前缀子串 dp[i][j] = min({ dp[i-1][j] + 1 , // 将 word1[1:i-1] 转换为 word2[1:j] 的操作数 + 删除 word1[i] 的操作数(1) dp[i][j-1] + 1 , // 将 word1[0:i] 转换为 word2[0:j-1] 的操作数 + 将 word2[j] 插入到 word1[0:i] 之后的操作数(1) dp[i-1][j-1] + 1 }) // 将 word1[0:i-1] 转换为 word2[0:j-1] 的操作数 + 将 word1[i] 替换为 wor
def edit_distance(word1,word2):
if word1==None or word2==None:
return 0
m=len(word1)
n=len(word2)
dp=[[0 for i in range(n+1)]for j in range(m+1)]
for i in range(m+1):
dp[i][0]=i
for j in range(n+1):
dp[0][j]=j
for i in range(m+1):
for j in range(n+1):
if word1[i-1]==word2[j-1]:
dp[i][j]=dp[i-1][j-1]
else:
dp[i][j]=min(dp[i-1][j-1],dp[i-1][j],dp[i][j-1])+1
return dp[m][n]
word1='horse'
word2='ros'
print(edit_distance(word1,word2))
矩阵中最大的正方形
问题描述
在一个由 0 和 1 组成的二维矩阵 M 内,找到只包含 1 的最大正方形,并返回其面积。
示例:
输入:
1 0 1 0 0
1 0 1 1 1
1 1 1 1 1
1 0 0 1 0
输出:
4
思路
- DP 定义:
dp[i][j] := 以 M[i][j] 为正方形**右下角**所能找到的最大正方形的边长
- 注意保存的是边长
- 因为
dp
保存的不是全局最大值,所以需要用一个额外变量更新结果
- 初始化(边长)
dp[i][0] = M[i][0] dp[0][j] = M[0][j]
- 递推公式
dp[i][j] = min{dp[i-1][j], dp[i][j-1], dp[i-1][j-1]} + 1 若 M[i][j] == 1 = 0 否则
注意到,本题的递推公式与 编辑距离 完全一致
class Solution:
def maximalSquare(self, matrix: List[List[str]]) -> int:
h = len(matrix)
if h==0:return 0
w = len(matrix[0])
dp = [[0 for i in range(w)] for i in range(h)]
ans = 0
for i in range(h):
for j in range(w):
if i==0 or j==0:
dp[i][j] = int(matrix[i][j])
else:
if matrix[i][j]!='0':
dp[i][j] = 1 + min(dp[i-1][j],dp[i][j-1],dp[i-1][j-1])
else:
dp[i][j]=0
ans = max(ans,dp[i][j])
return ans**2
鹰蛋问题
问题描述
教授手上有`M`个一模一样的鹰蛋,教授想研究这些蛋的硬度`E`,测试方法是将蛋从高为`N`层的楼上不断自由落下;
每个蛋在`E+1`层及以上掉下都会碎,而在`E`层及以下不会碎;每个蛋可以重复测试直到它碎了为止。
例如:蛋从第 1 层掉下碎了,则`E=0`;蛋从第`N`层掉下未碎,则`E=N`。
求在给定`M`和`N`下为了确定`E`在**最坏情况下**需要测试的最少次数。
如果比较的次数大于 32,输出 "Impossible"。
范围:1 ≤ N ≤ 2000000007,1 ≤ K ≤ 32
示例:`N=10, K=1`,则`ans=10`
说明:如果只有一个蛋,那么只能将这个蛋一层层往上尝试;
因此在最坏情况下,它最少要测试 10 次才能确定 `E`
分析
- 如果只有
M=1
个蛋,那么只能从第一层开始一层一层往上尝试,最坏情况下的最少次数为N
- 如果蛋的数量足够多,那么问题转变为二分查找,最坏情况下的最少次数为
logN
上取整
思路
假设有n楼层,k个鹰蛋,则在第i层试探时会出现两种状态,一种状态是鹰蛋摔破了,则我们下一步只有n-1个鹰蛋,同时总楼层数也缩减为i-1,另一种状态是鹰蛋没有摔破,那么鹰蛋总数不变,还是n个,楼层数则缩减为n-i层。
第w层是刚好没碎的情况
分析
- 如果只有
M=1
个蛋,那么只能从第一层开始一层一层往上尝试,最坏情况下的最少次数为N
- 如果蛋的数量足够多,那么问题转变为二分查找,最坏情况下的最少次数为
logN
上取整
思路
-
DP 定义:
dp[i][j] := i 个蛋比较 j 次所能确定的最高楼层
-
DP 初始化
dp[i][1] = 1 // i 个蛋比较 1 次所能确定的最高楼层是 1 dp[1][j] = j // 1 个蛋比较 j 次所能确定的最高楼层为 j
-
DP 更新(没理解稍后专研一下)
dp[i][j] = dp[i][j-1] + dp[i-1][j-1] + 1
#include <cstdio>
typedef long long LL;
const int MAX_K = 32 + 1;
const int MAX_T = 32 + 1;
LL dp[MAX_K][MAX_T]; // 使用 LL 防止溢出,long 不保证比 int 更大
// dp[i][j] := i 个蛋比较 j 次所能确定的最高楼层
void init() {
// 初始化
for (int i = 1; i < MAX_K; i++)
dp[i][1] = 1;
for (int j = 1; j < MAX_T; j++)
dp[1][j] = j;
// 更新
for (int i = 2; i < MAX_K; i++)
for (int j = 2; j < MAX_T; j++)
dp[i][j] = dp[i][j - 1] + dp[i - 1][j - 1] + 1;
}
void solve() {
init();
//printf("%lld", dp[32][32]); // 4294967295 == 2^32 - 1,用 int 会溢出
int T; // 1 ≤ T ≤ 10000
scanf("%d", &T);
while (T--) {
int N, K; // 1 ≤ N ≤ 2000000007 < 2^31, 1 ≤ K ≤ 32
scanf("%d %d", &N, &K);
int ret = 0;
for (int j = 1; j < MAX_T; j++) {
// 注意:dp[i][j] 表示的是 i 个蛋比较 j 次所能确定的最高楼层
if (dp[K][j] >= N) {
ret = j;
break;
}
}
if (ret) printf("%d\n", ret);
else puts("Impossible");
}
}
int main() {
solve();
return 0;
}
矩阵链乘法
RecurMatrixChain(P,i,j)
输入:矩阵链Ai..j的输入为向量P=<Pi-1,Pi,···,Pj>,其中1≤i≤j≤n
输出:计算Ai..j的所需最小乘法运算次数m[i,j]和最后一次运算的位置s[i][j]
p = [30, 35, 15, 5, 10, 20, 25]
def matrix_chain_order(p):
n = len(p) - 1 # 矩阵个数
m = [[0 for i in range(n)] for j in range(n)]
s = [[0 for i in range(n)] for j in range(n)] # 用来记录最优解的括号位置
for l in range(1, n): # 控制列,从左往右
for i in range(l-1, -1, -1): # 控制行,从下往上
m[i][l] = float('inf') # 保存要填充格子的最优值
for k in range(i, l): # 控制分割点
q = m[i][k] + m[k+1][l] + p[i]*p[k+1]*p[l+1]
if q < m[i][l]:
m[i][l] = q
s[i][l] = k
return m, s
def print_option_parens(s, i, j):
if i == j:
print('A'+str(i+1), end='')
else:
print('(', end='')
print_option_parens(s, i, s[i][j])
print_option_parens(s, s[i][j]+1, j)
print(')', end='')
r, s = matrix_chain_order(p)
print_option_parens(s, 0, 5)
有代价最短路径
瓷砖覆盖
稍后再做吧,有点小难