动态规划题目详解

动态规划问题主要的应用

DP:将一个问题拆成几个子问题,分别求解这些子问题,即可推断出大问题的解

思路主要是三步走:

一.确定(定义)状态;就是我们要保存哪些东西,这个总要知道吧.(可以从时间或者是空间上进行划分)

二.确定决策并写出状态转移方程.(当我们确定了状态,就需要知道状态是怎么转移的)   感觉这个就是描述子问题的通式

三.寻找边界条件。英文我们的状态转移方程是一个递推式,所以需要一个或者是多个终止条件(或者说是边界条件吧)

而在状态保存方面,我们可以采用多种形式进行保存,一般有以下三种形式

第一种:可以采用多个变量进行保存。第二种:可以采用一维数组进行保存。第三种:采用二维数组进行保存。

如果我们需要多个状态需要保存的话,这就涉及到多维的问题了,那就需要使用多个数据形式进行保存,例如使用两个二维数组之类的方法对状态进行保存。

总体上思路差不多是这个样子,最重要的其实还是要找到状态转移方程,找到了这个问题解决了一大半了。

背包问题(01背包问题,完全背包,多维背包问题)

【注】关于“恰好装满”

  • 如果要求恰好装满背包,可以在初始化时将 dp[0] / dp[i][0] 初始化 0,其他初始化为 -INF。这样即可保证最终得到的 dp[N] / dp[N][M] 是一种恰好装满背包的解;
  • 如果不要求恰好装满,则全部初始化为 0 即可。
  • 可以这样理解:初始化的 dp 数组实际上就是在没有任何物品可以放入背包时的合法状态。
    • 如果要求背包恰好装满,那么此时只有容量为 0 的背包可能被价值为 0 的物品“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,它们的值就都应该是 -INF 。
    • 如果背包并非必须被装满,那么任何容量的背包都有一个合法解,即“什么都不装”,这个解的价值为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)

有代价最短路径

Dijkstra算法和Floyd算法(单源最短路径)

瓷砖覆盖

稍后再做吧,有点小难

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值