算法学习-动态规划

本文深入探讨了动态规划的概念,强调了其在解决存在重叠子问题的优化问题中的优势。介绍了使用DP的三个关键条件,并通过矩阵链乘法、最长公共子序列和背包问题等经典例子详细阐述了DP的应用。此外,还讨论了动态规划在时间复杂度分析、状态表示和优化策略等方面的具体实现。文章最后列举了不同类型的背包问题,包括01背包和完全背包的优化解法。
摘要由CSDN通过智能技术生成

动态规划

本博客结合了acwing算法基础课

为什么要使用DP

用分治法解决的问题中存在重叠子问题,分治方法将重复计算公共子问题,造成许多不必要的计算

使用DP的条件

  1. 具有优化子结构
    原问题的优化解包含了子问题的优化解
  2. 具有重叠子问题
    问题求解过程中很多子问题的解被重复利用

DP的理解方式

  1. 状态表示:
    集合:所有选法的集合
    属性:状态集合里的最大值,最小值,数量
  2. 状态计算:
    集合的划分
    划分原则:不漏

DP的时间复杂度分析

状态数量*计算每一个状态所需的时间复杂度

几种经典的DP问题

矩阵链乘法问题及其变式

  1. 问题的定义
    在这里插入图片描述
  2. 问题的求解
  • 一些记号:在这里插入图片描述
    在这里插入图片描述

  • 证明优化子结构(反证)
    在这里插入图片描述

  • 证明重叠子问题
    在这里插入图片描述

  • 递归方程
    在这里插入图片描述
    -== 算法伪代码,输出优化解==

在这里插入图片描述
在这里插入图片描述

注:计算顺序其实就对应了这张图
在这里插入图片描述

  • 算法的时间复杂度和空间复杂度
    在这里插入图片描述

最长公共子序列问题及其变式

  1. 问题的定义
    在这里插入图片描述
  2. 问题的求解
  • 一些记号
    在这里插入图片描述
  • 证明优化子结构
    在这里插入图片描述
  • 证明重叠子问题
    在这里插入图片描述
  • 递归方程
    在这里插入图片描述
  • 算法伪代码,输出优化解
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  • 算法的c++代码
    f [ i ] [ j ] f[i][j] f[i][j]分成了
  1. a[i],b[j]都不选:f[i-1][j-1];
  2. a[i]没有在最后的公共子序列中:f[i-1][j]
  3. b[j]没有在最后的公共子序列中:f[i][j-1];
  4. a[i]=b[j],即ai在最后的公共子序列中:f[i-1][j-1]+1
    其中1包含于2,3两种情况中,所以1不用单独列出来
#include <iostream>
using namespace std;
const int N=1010;
int f[N][N];
char A[N],B[N];
int main()
{
    int n,m;
    int maxitem=0;
    cin>>n>>m;
    for (int i=1;i<=n;i++)
    cin>>A[i];
    for (int i=1;i<=m;i++)
    cin>>B[i];
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            {
                maxitem=max(f[i-1][j],f[i][j-1]);
                if(A[i]==B[j])
                maxitem=max(f[i-1][j-1]+1,maxitem);
            f[i][j]=maxitem;
            }
            
    cout<<f[n][m];
  • 算法的时间复杂度
    在这里插入图片描述

动态时间规整算法

  1. 问题的定义
    在这里插入图片描述
  2. 问题的解决
    类似于最长公共子序列

背包模型

01背包

  1. 问题的定义
    在这里插入图片描述
  2. 问题的求解
  • 一些定义
    在这里插入图片描述

  • 递归方程
    在这里插入图片描述

  • 算法伪代码
    在这里插入图片描述
    在这里插入图片描述

  • 算法c++代码

二维数组朴素写法
# include <iostream>
using namespace std;
const int N=1001;
int f[N][N];
int v[N];
int w[N];
int main()
{
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=n;i++)cin>>v[i]>>w[i];
    for(int i=1;i<=n;i++)
        for(int j=0;j<=m;j++)
        {
            f[i][j]=f[i-1][j];
            if(j>=v[i])
            f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i]);
        }
    cout<<f[n][m];
}
一维数组优化写法

将二维数组优化为一维数组的思路:

  1. 不需要保存任意前i种物品背包容量为j时的最大价值,只需要考虑最后一轮循环即前N种物品背包容量为j时的最大价值
  2. 由于正序循环背包容量时 f [ j ] , 即 f [ i ] [ j ] f[j],即f[i][j] f[j]f[i][j]状态的更新靠的是当前第i轮也就是 f [ i ] [ j − v [ i ] ] f[i][j-v[i]] f[i][jv[i]]的已决策量来更新的,与应该靠上一轮 f [ i − 1 ] [ j − v [ i ] ] f[i-1][j-v[i]] f[i1][jv[i]]来更新不相符,故应该逆序循环背包容量k
  3. 可以优化背包容量循环的结束条件为 j ≥ v [ i ] j\geq v[i] jv[i],因为若 j ≤ v [ i ] j\le v[i] jv[i]表示第i个物品放不进背包,可直接结束循环

第i轮循环时, f ( j ) f(j) f(j)表示对i种物品背包容量为j时最大价值

# include <iostream>
using namespace std;
const int N=1001;
int f[N];
int v[N];
int w[N];
int main()
{
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=n;i++)cin>>v[i]>>w[i];
    for(int i=1;i<=n;i++)
        for(int j=m;j>=v[i];j--)
        {
            f[j]=max(f[j],f[j-v[i]]+w[i]);
        }
    cout<<f[m];
}

完全背包

第i个物品可以选无限个,直到装满

两维的朴素写法

原理 f ( i , j ) = m a x ( f ( i − 1 , j ) , f ( i − 1 , j − v ) + w , f ( i − 1 , j − 2 v ) + 2 w . . . . ) = m a x ( f ( i − 1 , j ) , m a x ( f ( i − 1 , j − v ) + w , f ( i − 1 , j − 2 v ) + 2 w . . . ) = m a x ( f ( i − 1 , j ) , f ( i , j − v ) + w ) f(i,j)=max(f(i-1,j),f(i-1,j-v)+w,f(i-1,j-2v)+2w....)=max(f(i-1,j),max(f(i-1,j-v)+w,f(i-1,j-2v)+2w...)=max(f(i-1,j),f(i,j-v)+w) f(i,j)=max(f(i1,j),f(i1,jv)+w,f(i1,j2v)+2w....)=max(f(i1,j),max(f(i1,jv)+w,f(i1,j2v)+2w...)=max(f(i1,j),f(i,jv)+w)

#include<iostream>
using namespace std;
const int N=1001;
int f[N][N];
int v[N],w[N];
int main()
{
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	cin>>v[i]>>w[i];
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			{
			f[i][j]=f[i-1][j];
			if(j>=v[i])
			f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]);
			}
	cout<<f[n][m]<<endl;
	return 0;
}

优化为一维的写法
优化为一维的两种理解方式
  • 直观理解: f [ j ] f[j] f[j]可以理解为当背包容量为j时所有可行的背包装填方法中总价值最大的那一个价值,我们可以把求解 f [ j ] f[j] f[j]的问题划分为i类,每类代表最后一个装入背包的物品是第i个物品,而 f [ j ] f[j] f[j]就是这i类中使得背包总价值最大的那一个,故 f [ j ] = m a x ( f [ j ] , f [ j − v [ i ] ] + w [ i ] ) f[j]=max(f[j],f[j-v[i]]+w[i]) f[j]=max(f[j],f[jv[i]]+w[i])
  • 通过代码的等价变形理解
    观察代码段
for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			{
			f[i][j]=f[i-1][j];
			if(j>=v[i])
			f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]);
			}

由于最后求解的问题只需要求 f [ n ] [ m ] f[n][m] f[n][m]没必要把每个状态的最优解都保存下来,所以i重循环做的过程中可以动态地更新一维数组,最后数组中保存下来的就是前n个物品中背包容量为j(j=0,1,2…m)时最大价值

f[0]=0;//初始状态,因为f已经设为全局变量所以这步可忽略
for (int i=1;i<=n;i++)//枚举物品
	for(int j=v[i];j<=m;j++)//枚举背包容量,循环开始条件是v[i]是因为j<v[i]时装不下
		f[j]=max(f[j],f[j-v[i]]+w[i]);
cout<<f[m]<<endl;

线性DP

数字三角形

从起点走到 f ( i , j ) f(i,j) f(i,j)所有路线的最大值等于从左上方和从正上方走,即 m a x ( f ( i − 1 , j − 1 ) , f ( i − 1 , j ) ) max(f(i-1,j-1),f(i-1,j)) max(f(i1,j1),f(i1,j))

#include <iostream>
using namespace std;
const int N=510,inf=1e9;
int a[N][N],f[N][N];
int main()
{
    int n,maxnum=-inf;
    cin>>n;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=i;j++)
            cin>>a[i][j];
       
    for(int i=1;i<=n;i++)
        for(int j=0;j<=i+1;j++)
            f[i][j]=-inf;
    f[1][1]=a[1][1];
    for(int i=1;i<=n;i++)
        for(int j=1;j<=i;j++)
            f[i][j]=max(f[i-1][j-1]+a[i][j],f[i-1][j]+a[i][j]);
    for(int i=1;i<=n;i++)
        if(f[n][i]>=maxnum)maxnum=f[n][i];
    cout<<maxnum;
}

最长上升子序列

思路:状态表示 f [ i ] f[i] f[i]存的是以第i个元素结尾的最长上升子序列的长度的最大值

# include <iostream>
using namespace std;
const int N=1010;
int f[N],a[N];
int main()
{
    int n;
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
        f[i]=1;//作为计算f[i]分类时最长上升子序列长度为1,就是其本身的那一类
    }
    for(int i=1;i<=n;i++)
        for(int j=1;j<i;j++)
        {
            if(a[i]>a[j])
            f[i]=max(f[i],f[j]+1);
        }
    int res=0;
    for(int i=1;i<=n;i++)
    if(f[i]>res)res=f[i];
    cout<<res<<endl;
}

区间DP

石子合并

状态表示思路 f [ i ] [ j ] f[i][j] f[i][j]表示所有第i堆石子到第j堆石子的合并方式里的代价最小值
状态计算思路:以第i到第j中任意一处的分界线来分类,先将分界线左边的石子合并,再将分界线右边的石子合并,再将左右两堆合并
代码思路:先枚举区间长度,再枚举左端点,再枚举分界线

# include <iostream>
using namespace std;
const int N=310;
int f[N][N];
int s[N];
int main()
{
    int n,temp;
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>temp;
        s[i]=s[i-1]+temp;
    }
    for(int len=2;len<=n;len++)//枚举区间长度
        for(int i=1;i+len-1<=n;i++)//枚举左端点
            {
                int l=i,r=i+len-1;//确定左右端点
                 f[l][r]=1e9;//先将f[l][r]赋一个较大的数,否则因为f[l][r]是全局变量,在后面比较min时都是0
                for(int k=l;k<=r-1;k++)
                {
                    
                    f[l][r]=min(f[l][r],f[l][k]+f[k+1][r]+s[r]-s[l-1]);
                }
            }
    cout<<f[1][n]<<endl;
}

最优二叉搜索树

类似于矩阵链乘法问题

问题的定义

给定二叉树的叶子节点和非叶节点,并给出每个节点被搜索的概率,求如何构造二叉树使树的期望搜索代价最小

问题的解决
  • 一些定义:
  1. 树的期望搜索代价:
    在这里插入图片描述
    每个节点所在的层数(根节点是0层)+1为该节点的深度,深度乘该节点被搜索的概率为该搜索该节点的代价,所有节点的代价之和为树的期望搜索代价

  2. E [ i , j ] E[i,j] E[i,j]:包含内节点 { k i , k i + 1 . . . . k j } \{ k_i,k_{i+1}....k_j \} {ki,ki+1....kj}和叶节点 { d i − 1 , d i . . . . d j } \{d_{i-1},d_i....d_j \} {di1,di....dj}的最优二叉搜索树的期望搜索代价,特别地当 j = i − 1 j=i-1 j=i1 E [ i , j ] = d i − 1 E[i,j]=d_i-1 E[i,j]=di1

  3. w [ i , j ] w[i,j] w[i,j] ∑ l = i j p l + ∑ l = i − 1 j q l \sum_{l=i}^{j}p_l+\sum_{l=i-1}^{j}q_l l=ijpl+l=i1jql,即i号到j号内节点的搜索概率+i-1号到j号叶节点的搜索概率

  • 动态规划算法描述
    求解 E [ i , j ] E[i,j] E[i,j]的问题可以划分为以下步骤
  1. 在i号到j号内节点中找一个r号节点作为根,代价为r号内节点的搜索代价 p r ( i ≤ r ≤ j ) p_r (i\leq r \leq j) pr(irj)

  2. 将求解 E [ i , j ] E[i,j] E[i,j]的问题划分为两个子问题 E [ i , r − 1 ] E[i,r-1] E[i,r1] E [ r + 1 , j ] E[r+1,j] E[r+1,j],若 r − 1 = i − 1 r-1=i-1 r1=i1则说明此时 E [ i , r − 1 ] E[i,r-1] E[i,r1]只包含一个叶节点 q i − 1 q_{i-1} qi1

  3. 将r号节点作为根后左子树为包含内节点 { k i , k i + 1 . . . . k r − 1 } \{ k_i,k_{i+1}....k_{r-1}\} {ki,ki+1....kr1}和叶节点 { d i − 1 , d i . . . . d r − 1 } \{d_{i-1},d_i....d_{r-1}\} {di1,di....dr1}的最优二叉搜索树,右子树为包含内节点 { k r + 1 , k r + 2 . . . . k j } \{ k_{r+1},k_{r+2}....k_{j}\} {kr+1,kr+2....kj}和叶节点 { d r , d r + 1 . . . . d j } \{d_{r},d_{r+1}....d_{j}\} {dr,dr+1....dj}的最优二叉搜索树,因为将r号节点作为了根,所以这两棵子树中的所有内节点和叶节点的深度增加1,相应的二叉树的期望搜索代价增加
    在这里插入图片描述

  4. 经过计算可知最后将问题分为两个子问题后最后总的期望搜索代价增加 w [ i , j ] w[i,j] w[i,j]

  • 证明优化子结构
    在这里插入图片描述
  • 递归方程
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  • 算法的伪代码
    在这里插入图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值