动态规划与贪婪算法

一、基本概念

本文内容主要讲述动态规划解题的主要思路,但并不是解题步骤,具体问题还要具体分析,但是基本思路都是如此。

动态规划解题的一个标志就是题目具有重叠子问题的性质,因此动态规划是典型的空间换时间的算法设计技术。

1.1. 自顶向下(递归+哈希)

依据动态规划重叠子问题的特性,在每次递归求解原问题时,都要重复的使用子问题的解,因此在每次求解成功之后,将解保存在一个哈希表中。之后再遇到相同的子问题时,直接查表即可。
举例说明。

找零问题
部分重叠子问题如图所示,因此如果再计算coinchange(9)时把问题解存起来后面就可以直接查表即可。

那么如何具体操作呢?
自顶向下的动态规划一般遵循以下过程:

  1. 得到子问题间的递推关系。上述几个问题都是在知道递推关系的基础上,画图进行子问题分析的。
  2. 写程序
  3. 添加哈希表的访问与存储部分。在原代码的基础上,加上哈希表的查询(哈希表中是否存在本问题的解)、存储(如果不存在,则计算并存储)。

针对第三点,写个例子

以找零问题为例,以递归的方式写出问题求解伪代码。


coinChange(coins,amount) {
    if (amount == 0) 
    	return 0;
    result = INT_MAX;
    for c in coins{
        if (amount - c < 0) 
        	continue;
        	
        subProb = coinChange(coins, amount - c);
        
        if (subProb == -1) 
        continue;
        
        result = min(result, subProb + 1);
        }
    

    return result;
}

然后考虑加入哈希表记忆结果。其实非常简单,在函数开头加上判断哈希表中是否存在解,在函数结尾将解填入哈希表即可。改变之后的代码如下


coinChange(coins,amount,hashmap[]) {
    if(hashmap[amount] is valid)
    	return hashmap[amount]; 
    
	if (amount == 0) 
    	return 0;
    result = INT_MAX;
    for c in coins{
        if (amount - c < 0) 
        	continue;
        	
        subProb = coinChange(coins, amount - c,hashmap[]);
        
        if (subProb == -1) 
        continue;
        
        result = min(result, subProb + 1);
        }
    
	hashmap[amount]=result;
    return result;
}

1.2. 自底向上(分类讨论)

其实大多数动态规划解题都是按照自底向上来设计的,因为递归对计算机需求较大,因此常常通过自底向上(即迭代)来实现。

自底向上的含义是,从问题的最平凡的情况入手(例如斐波那契F(1)和F(0)),依据递推式边计算边记录。直到最后的问题F(n)

那么如何具体操作呢?
自底向上的动态规划一般遵循以下过程:

  1. 确定状态变量,就是说哈希表hashmap[i]的含义是什么。一般来讲状态变量即为题目所求

一维状态变量:例如斐波那契数列中hashmap[i]的值表示第i个斐波那契数的值,最长上升子序列LIS问题中hashmap[i]表示以i开头的的LIS,找零问题中的hashmap[i]表示amount=i时最少金币数,最大子序列和问题中hashmap[i]表示序列{num[0],…,num[i]}最大子序列和。

二维状态变量:可能表区间,例如最长回文子序列问题中hashmap[i][j]表示子串s.substr(i,j-i+1)是否是回文,最长公共子序列问题中hashmap[i][j]表示s1.sub(0,i+1)和s2.sub(0,j+1)的公共子串个数,不同路径问题中hashmap[i][j]表示坐标(i,j),最大正方形问题中hashmap[i][j]表示以(i,j)为右下角的面积最大的正方形的面积。还可能表状态,例如打家劫舍系列问题中hashmap[i][j]表示第i天交易结束是否持有股票(j=1持有,j=0未持有),例如石子游戏中hashmap[i][j][k]表示石子排布为{num[i],…,num[j]}时是否是亚历克斯先手(k=1是,k=0否)。

选择状态变量的含义和维数之后,如果不能顺利写出递推式,则需要作出改变。

  1. 写出状态转移方程(递推式),即子问题之间的关系方程,即hashmap[i]与hashmap[i-1],…,hashmap[0]以及各元素num[i],…,nums[0[之间的关系,需要根据题意作出计算,同时一般也是动态规划解题中的困难所在。

但是状态转移方程的写法有一个重要的思想——分类讨论思想。

  • 如果是最优化问题,则考虑所有互斥的情况。
    例如最长上升子序列LIS问题,dp[i]表示{nums[0],…,nums[i]}的最长上升子序列长度,分为两种情况——不包括nums[i]的LIS或包括nums[i]的LIS。不包括nums[i]的LIS其实就是dp[i-1],而包括nums[i]的LIS分为多种情况——所有num[k]<num[i]的dp[k]+1。从而得到递推式

ans=max(dp[k])+1, k为所有满足nums[k]<nums[i]且属于{0,1,…,i-1}的值
dp[i]=max(dp[i-1],ans);

  • 如果是可行性问题,则考虑所有可能的情况。
    例如不同路径系列问题中,hashmap[i][j]表示由起点到达坐标(i,j)的路径种数。假设终点坐标是(fa,fb)因此到达终点的可能情况有两种,(fa-1,fb)→(fa,fb)以及(fa,fb-1)→(fa,fb),这样就完整地考虑了所有可能的情况。得到递推式

dp[fa][fb]=dp[fa-1][fb]+dp[fa][fb-1];

  1. 确定遍历方式,写出程序。在知道递推方程的情况下,如何实现从平凡情况推算到最后的问题F(n)呢(如何实现自底向上)?

本问题在多维动态规划中需要仔细思考。

最长回文子串(找到给定字符串中最长的回文字符串。回文是指字符串正序倒序相同,如abccba就是,bd就不是)为例。

定义状态变量dp[i][j]表示字符串中下标i到下标j构成的子串是否是回文,因此状态变量为bool型变量。可以得到递推式
 
dp[i][j]=dp[i+1][j-1] && (s[i]==s[j]);

意思是当且仅当下标i+1到j-1构成的子串是回文且下标i对应的字符和下标j对应的字符相同时,dp[i][j]=True。
比如string s=“asdffdsb“,s[2,5] == “dffd” 显然是回文,又由于s[1] = = s[6] = =‘s’因此s[1,6]是回文。

有了递推式关键在于如何遍历最终得到dp[0][n]的结果。显然从i=0,j=0到i=0,j=n是不可以的。可以发现递推式中dp[i][j]是对长度为j-i+1的某个子串进行判断,而它与dp[i+1][j-1],即长度为j-i-1的子串有关,因此遍历可以按照长度进行遍历。

从而得到伪代码

for length=1 to string.size():
	for start=0 to string.size()-length:
		end=start+length-1;
		dp[start][end]=dp[start+1][end-1] && (string[start]==string[end]);

1.3. 总结

动态规划实现方式两种

  1. 自顶向下:用递归方式写出原问题的代码+添加哈希表查询和添加部分
  2. 自底向上:确定状态变量+利用分类讨论思想写出状态转移方程+画表确定遍历方法

二、解题思路

2.1. 子数组子串问题

一维子数组(元素必须相邻的子集)问题
状态变量dp[i]:表示下标0到i的题目所求
(例如,最大子序和问题中dp[i]表示nums[0,…,i]的最大子序列和)
状态转移方程:dp[i]=f(dp[i-1],以i结尾的题目所求)
(例如最大子序问题状态转移方程为dp[i]=max(dp[i-1],sum[i]),其中sum[i]表示以i结尾的最大子序和)
二维子数组(元素必须相邻的子集)问题
状态变量dp[i][j]:表示下标i到j的题目所求
状态转移方程:dp[i][j]=f(dp[i+1][j-1],dp[i][j-1],dp[i+1][j],元素i和元素j)

2.2. 子序列问题

子序列(元素可以不相邻)问题
状态变量dp[i]:表示以i结尾的题目所求
(例如,最长上升子序列问题中dp[i]表示以nums[i]结尾的最长子序列)
状态转移方程:dp[i]=f(dp[i-1],dp[i-2],…,dp[0],nums[i])
(例如最长上升子序列问题状态转移方程为 dp[i]=max({dp[k]+1|nums[k]<nums[i],k∈0,1,…,i}) )

2.3. 零一背包问题

问题:有同样大小的数组weight和value,其中weight[i]表示第i种物品重量,value[i]表示第i种物品价值,每种物品仅有1个,现有背包承重amount,求背包能装的最大价值。

思路:使用动态规划方法,对于没一件物品,分为两类——装与不装,因此可以直接得到算法。
状态变量:dp[j]表示背包承重为j时的最大价值
状态转移方程:dp[j]=max(dp[j-weight[i]]+value[i]),i=0,1,…,weightsize-1;
给出简化后的代码

for i=0 to weight.size-1:
	for j=amount to weight[i]:
		dp[j]=max(dp[j],dp[j-weight[i]]+value[i];

2.4. 完全背包问题

问题:有同样大小的数组weight和value,其中weight[i]表示第i种物品重量,value[i]表示第i种物品价值,每种物品有无数个,现有背包承重amount,求背包能装的最大价值。

思路:使用动态规划方法,对于没一件物品,分为两类——装与不装,但是可能装多个。
状态变量:dp[j]表示背包承重为j时的最大价值
状态转移方程:dp[j]=max(dp[j-k*weight[i]]+k-value[i]) ,i=0,1,…,weightsize-1 ,k=0,…,j/weight[i];
给出简化后的代码

for i=0 to weight.size-1:
	for j=weight[i] to amount:
		dp[j]=max(dp[j],dp[j-weight[i]]+value[i];

2.4. 状态机问题

三、相关题目

3.1. 子数组子串问题

数组

字符串

i和j表示长度,因此创建的数组大小为s.size()+1,并注意初始化。


        for(int i=1;i<=word1size;i++)
            for(int j=1;j<=word2size;j++)
            {
                if(word1[i-1]==word2[j-1])
                    dp[i][j]=dp[i-1][j-1]+1;
                else
                    dp[i][j]=max(dp[i][j-1],dp[i-1][j]);  
            }
        

  • 两个字符串的删除操作

    给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符

二叉树、图

  • 打家劫舍III
    所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。
    计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。
    思路:注意动态规划可以自底向上实现,也要注意自顶向下的实现(递归+哈希)

  • 最大正方形
    注意二维DP问题是如何分类讨论的。

  • 不同路径
    由左上角出发,终点为右下角,求总的可行路径数量

  • 不同路径II
    由左上角出发,中间有障碍物,终点为右下角,求总的可行路径数量

  • 出界的路径数
    提示:结果数目较大因此最终结果要mod(1000000007)

数学

3.2. 子序列问题

数组

  • 摆动序列
    序列中连续数字的差严格在正数与负数之间变化,这样的序列为摆动序列。认为一个元素和两个元素的序列均为摆动序列,出现相等则不是摆动序列。输出给定数组的最长摆动序列的长度。
    例如 [1,17,5,10,13,15,10,5,16,8] 输出 7
    其中一个可为[1,17,10,13,10,16,8]。

  • 最大整除集h
    给出一个由无重复的正整数组成的集合,找出其中最大的整除子集,子集中任意一对 (Si,Sj) 都要满足:Si % Sj = 0 或 Sj % Si = 0。
    如果有多个目标子集,返回其中任何一个均可。

  • 等差数列划分

  • 最长上升子序列
    给定一个无序的整数数组,找到其中最长上升子序列的长度。

字符串

  • 单词拆分
    给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。

数学

  • 丑数II
    求第n个丑数,丑数是指只能被2,3,5整除的正整数。
    提示:虽然本题不是二叉树、图,但是本题蕴含着重要的分类讨论的思想——任何一个丑数一定是某个丑数乘2或乘3或乘5的结果。这非常类似于路径问题中,到达终点有上下左右这四种情况分类、买股票问题中的自动机原理。
    如果不使用动态规划,可以使用基于优先队列的贪心算法。
  • 完全平方数
    给定正整数n,将n表示成完全平方数的加和,输出加数的最小个数。例如n=12,可以写12=9+1+1+1和12=4+4+4,但是后者个数最少,输出3。

3.3. 零一背包问题

  • 分割等和子集
    给定一个只包含正整数的非空数组,判断是否能够将数组分成和相同的两个子集。

  • 一和零
    给定0的个数m和1的个数n,以及一个只由0和1构成的字符串数组,求数组中0的个数≤m且1的个数≤n的最大字符串个数。
    例如: strs = {“10”, “0001”, “111001”, “1”, “0”}, m = 5, n = 3
    输出: 4
    因为strs[0]、strs[1]、strs[3]和strs[4]中0的个数≤5且1的个数≤3,并且字符串个数达到最大

  • 目标和
    给定一个非负整数数组和一个目标数。数组中任意一个数都只可使用+或-添加到其前面,使式子的最终结果恰好等于目标数,返回符号添加种数
    例 :
    输入: nums: [1, 1, 1, 1, 1], S: 3
    -1+1+1+1+1 = 3
    +1-1+1+1+1 = 3
    +1+1-1+1+1 = 3
    +1+1+1-1+1 = 3
    +1+1+1+1-1 = 3 则输出5

3.4. 完全背包问题

  • 零钱兑换
    给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。例 coins=[1,2,5,7] amount=12 则返回2。
  • 组合总和IV
    提示:C++会int溢出,改成unsigned long long int
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Starry丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值