0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

论动态规划穷举的两种视角

算法与数据结构 来源:labuladong 作者:labuladong 2022-07-11 14:49 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

挺久没写动态规划相关的题目了,本文我带大家复习一下动态规划相关问题的一系列解题套路,然后着重讨论一下动态规划穷举时不同视角的问题。

动态规划解题组合拳

首先,前文我的刷题心得讲了,我们刷的算法问题的本质是「穷举」,动态规划问题也不例外,你必须想办法穷举所有可能的解,然后从中筛选出符合题目要求的解。

另外,动态规划问题穷举的过程中会出现重叠子问题导致的冗余计算,所以前文动态规划核心套路框架中告诉你如何一步一步把暴力穷举解法优化成效率更高的动态规划解法。

然而,想要写出暴力解需要依据状态转移方程,状态转移方程是动态规划的解题核心,可不是那么容易想出来的。不过,前文动态规划设计:数学归纳法告诉你,思考状态转移方程的一个基本方法是数学归纳法,即明确dp函数或数组的定义,然后使用这个定义,从已知的「状态」中推导出未知的「状态」。

还没完,比如高楼扔鸡蛋问题中对dp函数/数组的定义不见得是唯一的,不同的定义会导致状态转移方程发生变化,解题效率也有高低之分,所以我们应该给dp函数尽可能想出更合适的定义来解题。

接下来就是本文要着重探讨的问题了:就算dp函数/数组的定义相同,如果你使用不同的「视角」进行穷举,效率也不见得是相同的

关于穷举「视角」的问题,前文回溯算法穷举视角:子集划分问题讲了回溯算法中不同的穷举视角导致的不同解法,其实这种视角的切换在动态规划类型问题中依然存在。前文对排列的举例非常有助于你理解穷举视角的问题,这里再简单提一下。

排列问题的两种视角

我们先回顾一下以前学过的排列组合知识:

1、P(n, k)(也有很多书写成A(n, k))表示从n个不同元素中拿出k个元素的排列(Permutation/Arrangement);C(n, k)表示从n个不同元素中拿出k个元素的组合(Combination)总数。

2、「排列」和「组合」的主要区别在于是否考虑顺序的差异。

3、排列和组合总数的计算公式如下:

56b5fdd6-00e4-11ed-ba43-dac502259ad0.png

好,现在我问一个问题,这个排列公式P(n, k)是如何推导出来的?为了搞清楚这个问题,我需要讲一点组合数学的知识。

排列组合问题的各种变体都可以抽象成「球盒模型」,P(n, k)就可以抽象成下面这个场景:

56d49020-00e4-11ed-ba43-dac502259ad0.jpg

即,将n个标记了不同序号的球(标号为了体现顺序的差异),放入k个标记了不同序号的盒子中(其中n >= k,每个盒子最终都装有恰好一个球),共有P(n, k)种不同的方法。

现在你来,往盒子里放球,你怎么放?其实有两种视角。

首先,你可以站在盒子的视角,每个盒子必然要选择一个球。

这样,第一个盒子可以选择n个球中的任意一个,然后你需要让剩下k - 1个盒子在n - 1个球中选择:

56eaff90-00e4-11ed-ba43-dac502259ad0.jpg

另外,你也可以站在球的视角,因为并不是每个球都会被装进盒子,所以球的视角分两种情况:

1、第一个球可以不装进任何一个盒子,这样的话你就需要将剩下n - 1个球放入k个盒子。

2、第一个球可以装进k个盒子中的任意一个,这样的话你就需要将剩下n - 1个球放入k - 1个盒子。

结合上述两种情况,可以得到:

57055a98-00e4-11ed-ba43-dac502259ad0.jpg

你看,两种视角得到两个不同的递归式,但这两个递归式解开的结果都是我们熟知的阶乘形式:

57182146-00e4-11ed-ba43-dac502259ad0.png

至于如何解递归式,涉及数学的内容比较多,这里就不做深入探讨了,有兴趣的读者可以自行学习组合数学相关知识。

当然,以上只是纯数学的推导,P(n, k)的计算结果也仅仅是一个数字,所以以上两种穷举视角从数学上讲没什么差异。但从编程的角度来看,如果让你计算出来所有排列结果,那么两种穷举思路的代码实现可能会产生性能上的差异,因为有的穷举思路难免会使用额外的 for 循环拖慢效率,这也是前文回溯算法穷举视角:子集划分问题主要探讨的。

本文不讲回溯算法和排列组合,不过请你记住这个例子,待会会把这种穷举视角的差异运用到动态规划题目当中。

例题分析

看一下力扣第 115 题「不同的子序列」:给你输入一个字符串s和一个字符串t,请你计算在s的子序列中t出现的次数。比如题目给的例子,输入s = "babgbag", t = "bag",算法返回 5:

57242356-00e4-11ed-ba43-dac502259ad0.jpg

函数签名如下:

intnumDistinct(Strings,Stringt);

你要数一数s的子序列中有多少个t,说白了就是穷举嘛,那么首先想到的就是能不能把原问题分解成规模更小的子问题,然后通过子问题的答案推导出原问题的答案。

首先,我们可以这样定义一个dp函数:

//定义:s[i..]的子序列中 t[j..]出现的次数为 dp(s, i, t, j)
intdp(Strings,inti,Stringt,intj)

这道题对dp函数的定义很简单直接,题目让你求出现次数,那你就定义函数返回值为出现次数就可以。

有了这个dp函数,题目想要的结果是dp(s, 0, t, 0),base case 也很容易写出来,解法框架如下:

intnumDistinct(Strings,Stringt){
returndp(s,0,t,0);
}

//定义:s[i..]的子序列中 t[j..]出现的次数为 dp(s, i, t, j)
intdp(Strings,inti,Stringt,intj){
//basecase1
if(j==t.length()){
//t已经全部匹配完成
return1;
}
//basecase2
if(s.length()-i< t.length() - j) {
        // s[i..]比 t[j..]还短,必然没有匹配的子序列
return0;
}

//...
}

好,接下来开始思考如何利用这个dp函数将大问题分解成小问题,即如何写出状态转移方程进行穷举。

回顾一下之前讲的排列组合的「球盒模型」,这里是不是很类似?t中的若干字符就好像若干盒子,s中的若干字符就好像若干小球,你需要做的就是给所有盒子都装一个小球。

所以这里就有两种穷举思路了,分别是站在t的视角(盒子选择小球)和站在s的视角(小球选择盒子)。

视角一,站在t的角度进行穷举

我们的原问题是求s[0..]的所有子序列中t[0..]出现的次数,那么可以先看t[0]s中的什么位置,假设s[2], s[6]是字符t[0],那么原问题转化成了在s[2..]s[6..]的所有子序列中计算t[1..]出现的次数。

写成比较偏数学的形式就是状态转移方程:

--定义:s[i..]的子序列中 t[j..]出现的次数为 dp(s, i, t, j)
dp(s,i,t,j)=SUM(dp(s,k+1,t,j+1)wherek>=iands[k]==t[j])

翻译成代码大致就是这个思路:

//定义:s[i..]的子序列中 t[j..]出现的次数为 dp(s, i, t, j)
intdp(Strings,inti,Stringt,intj){
intres=0;
//在s[i..]中寻找k,使得s[k]==t[j]
for(intk=i;k< s.length(); k++) {
        if(s.charAt(k)==t.charAt(j)){
//累加结果
res+=dp(s,k+1,t,j+1);
}
}
returnres;
}

这个思路应该不难理解吧,当然还可以加上备忘录消除重叠子问题,最终解法如下:

//备忘录
int[][]memo;

intnumDistinct(Strings,Stringt){
//初始化备忘录为特殊值-1
memo=newint[s.length()][t.length()];
for(int[]row:memo){
Arrays.fill(row,-1);
}
returndp(s,0,t,0);
}

//定义:s[i..]的子序列中 t[j..]出现的次数为 dp(s, i, t, j)
intdp(Strings,inti,Stringt,intj){
//basecase1
if(j==t.length()){
return1;
}
//basecase2
if(s.length()-i< t.length() - j) {
        return0;
}
//查备忘录防止冗余计算
if(memo[i][j]!=-1){
returnmemo[i][j];
}
intres=0;
//执行状态转移方程
//在s[i..]中寻找k,使得s[k]==t[j]
for(intk=i;k< s.length(); k++) {
        if(s.charAt(k)==t.charAt(j)){
//累加结果
res+=dp(s,k+1,t,j+1);
}
}
//存入备忘录
memo[i][j]=res;
returnres;
}

这道题就解决了,不过效率不算很高,我们可以粗略估算一下这个算法的时间复杂度上界,其中M, N分别代表s, t的长度,算法的「状态」就是dp函数参数i, j的组合:

带备忘录的动态规划算法的时间复杂度
=子问题的个数x函数本身的时间复杂度
=「状态」的个数x函数本身的时间复杂度
=O(MN)*O(M)
=O(N*M^2)

当然,因为 for 循环的复杂度不总是 O(M) 且子问题个数肯定小于 O(MN),所以这是复杂度的粗略上界。不过根据前文算法时空复杂度使用指南的描述,这个上界还是说明这个算法的复杂度有些偏高。主要高在哪里呢?对「状态」的穷举已经有了memo备忘录的优化,所以 O(MN) 的复杂度是必不可少的,关键问题出在dp函数中的 for 循环。

是否可以优化掉dp函数中的 for 循环呢?可以的,这就需要另一种穷举视角来解决这个问题。

视角二,站在s的角度进行穷举

我们的原问题是计算s[0..]的所有子序列中t[0..]出现的次数,可以先看看s[0]是否能匹配t[0],如果不匹配,那没得说,原问题就可以转化为计算s[1..]的所有子序列中t[0..]出现的次数;

但如果s[0]可以匹配t[0],那么又有两种情况,这两种情况是累加的关系:

1、让s[0]匹配t[0],那么原问题转化为在s[1..]的所有子序列中计算t[1..]出现的次数。

2、不让s[0]匹配t[0],那么原问题转化为在s[1..]的所有子序列中计算t[0..]出现的次数。

为啥明明s[0]可以匹配t[0],还不让它俩匹配呢?主要是为了给s[0]之后的元素匹配的机会,比如s = "aab", t = "ab",就有两种匹配方式:a_b_ab

把以上思路写成状态转移方程:

//定义:s[i..]的子序列中 t[j..]出现的次数为 dp(s, i, t, j)
intdp(Strings,inti,Stringt,intj){
if(s[i]==t[j]){
//匹配,两种情况,累加关系
returndp(s,i+1,t,j+1)+dp(s,i+1,t,j);
}else{
//不匹配,在s[i+1..]的子序列中计算t[j..]的出现次数
returndp(s,i+1,t,j);
}
}

依照这个思路,再加个备忘录消除重叠子问题,可以写出如下解法:

int[][]memo;

intnumDistinct(Strings,Stringt){
//初始化备忘录为特殊值-1
memo=newint[s.length()][t.length()];
for(int[]row:memo){
Arrays.fill(row,-1);
}
returndp(s,0,t,0);
}

//定义:s[i..]的子序列中 t[j..]出现的次数为 dp(s, i, t, j)
intdp(Strings,inti,Stringt,intj){
//basecase1
if(j==t.length()){
return1;
}
//basecase2
if(s.length()-i< t.length() - j) {
        return0;
}
//查备忘录防止冗余计算
if(memo[i][j]!=-1){
returnmemo[i][j];
}
intres=0;
//执行状态转移方程
if(s.charAt(i)==t.charAt(j)){
//匹配,两种情况,累加关系
res+=dp(s,i+1,t,j+1)+dp(s,i+1,t,j);
}else{
//不匹配,在s[i+1..]的子序列中计算t[j..]的出现次数
res+=dp(s,i+1,t,j);
}
//结果存入备忘录
memo[i][j]=res;
returnres;
}

这个解法中dp函数递归的次数,即状态i, j的不同组合的个数为 O(MN),而dp函数本身没有 for 循环,即时间复杂度为 O(1),所以算法总的时间复杂度就是 O(MN),比刚才的解法要好一些,你提交这个解法代码,耗时明显比刚才的解法少一些。

至此,这道题就分析完了。我们分别站在t的视角和s的视角运用dp函数的定义进行穷举,得出两种完全不同但都是正确的状态转移逻辑,不过两种逻辑在代码实现上有效率的差异。

那么不妨进一步思考一下,什么样的动态规划题目可能产生「穷举视角」上的差异?换句话说,什么样的动态规划问题能够抽象成经典的「球盒模型」呢?

--- EOF ---

审核编辑 :李倩


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 函数
    +关注

    关注

    3

    文章

    4408

    浏览量

    66908
  • 数组
    +关注

    关注

    1

    文章

    420

    浏览量

    27129

原文标题:论动态规划穷举的两种视角

文章出处:【微信号:TheAlgorithm,微信公众号:算法与数据结构】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    用PLC实现卷径计算的两种算法

    卷径计算,是动态计算如钢卷,纸卷等存料量的一方法,它是实现张力控制和自动充放料、以及甩尾控制的重要前提。卷径计算目前主流的方法有两种,一是根据机列速度(产线速度)和和被测卷的转动角
    的头像 发表于 11-14 16:54 1449次阅读
    用PLC实现卷径计算的<b class='flag-5'>两种</b>算法

    ADI GMSL技术两种视频数据传输模式的区别

    本文深入介绍GMSL技术,重点说明用于视频数据传输的像素模式和隧道模式之间的差异。文章将阐明这两种模式之间的主要区别,并探讨成功实施需要注意的具体事项。
    的头像 发表于 10-10 13:49 1833次阅读
    ADI GMSL技术<b class='flag-5'>两种</b>视频数据传输模式的区别

    两种TVS有啥不同?

    当我们查看TVS二极管的规格书,常会看到有以下两种种引脚功能标识图:对于初学者,看到感到疑惑,他们一样吗?他们有啥区别?为啥有的个尖头往外,阳极连在一起,有的个尖头往里,阴极连在一起?一连三问。EMC小哥根据自己经验略作分析
    的头像 发表于 09-15 20:27 615次阅读
    这<b class='flag-5'>两种</b>TVS有啥不同?

    两种散热路径的工艺与应用解析

    背景:两种常见的散热设计思路 在大电流或高功率器件应用中,散热和载流能力是PCB设计中必须解决的难题。常见的两种思路分别是: 厚铜板方案:通过整体增加铜箔厚度(如3oz、6oz甚至更高),增强导热
    的头像 发表于 09-15 14:50 505次阅读

    CMOS 2.0与Chiplet两种创新技术的区别

    摩尔定律正在减速。过去我们靠不断缩小晶体管尺寸提升芯片性能,但如今物理极限越来越近。在这样的背景下,两种创新技术站上舞台:CMOS 2.0 和 Chiplet(芯粒)。它们都在解决 “如何让芯片更强” 的问题,但思路却大相径庭。
    的头像 发表于 09-09 15:42 748次阅读

    贴片晶振中两种常见封装介绍

    贴片晶体振荡器作为关键的时钟频率元件,其性能直接关系到系统运行的稳定性。今天,凯擎小妹带大家聊聊贴片晶振中两种常见封装——金属面封装与陶瓷面封装。
    的头像 发表于 07-04 11:29 995次阅读
    贴片晶振中<b class='flag-5'>两种</b>常见封装介绍

    AGV小车中的动态路径规划算法揭秘

    并非一成不变时,动态路径规划能力就显得至关重要。本文将深入探讨几种主流的动态路径规划算法(如A、Dijkstra、RRT等),并解析它们如何在AGV行业中大显身手。 为何需要
    的头像 发表于 06-17 15:54 1207次阅读
    AGV小车中的<b class='flag-5'>动态</b>路径<b class='flag-5'>规划</b>算法揭秘

    两种驱动方式下永磁直线开关磁链电机的研究

    摘要:永磁开关磁链电机数学模型可以等效为永磁无刷电机,普遍采用方波驱动方式。在有限元基础上分析6/7极直线式磁链电机反电势波形,采用方波和正弦波驱动方式,比较两种方式下的电流、电压、平均推力大小
    发表于 06-09 16:18

    两种感应电机磁链观测器的参数敏感性研究

    模式和发电模式下对闭环电压电流模型磁链观测器和滑模磁链观测器参数敏感性进行了研究,通过仿真和实验比较了这两种观测器对定、转子电阻及励磁电感的敏感性。同时还研究了基于这两种观测器的模型参考自适应系统
    发表于 06-09 16:16

    详解ADC电路的静态仿真和动态仿真

    ADC电路主要存在静态仿真和动态仿真类仿真,针对两种不同的仿真,我们存在不同的输入信号和不同的数据采样,因此静态仿真和动态仿真是完全不同的
    的头像 发表于 06-05 10:19 1579次阅读
    详解ADC电路的静态仿真和<b class='flag-5'>动态</b>仿真

    铷原子钟与CPT原子钟:两种时间标准的区别

    在物理学的世界中,精密的时间测量是至关重要的。这就需要一个高度准确且稳定的时间标准,这就是原子钟。今天我们将探讨两种重要的原子钟:铷原子钟和CPT原子钟,以及它们之间的主要区别。首先,我们来了解一下
    的头像 发表于 05-22 15:49 533次阅读
    铷原子钟与CPT原子钟:<b class='flag-5'>两种</b>时间标准的区别

    用TLC2551采外部电压,只有0和2096两种值是怎么回事?

    用TLC2551采外部电压,只有0和2096两种值是怎么回事?求解决办法。
    发表于 02-06 07:31

    覆铜的两种形式是什么

    在电子电路设计与制造领域,覆铜的实现形式多样,其中大面积的覆铜和网格铜是最为常见且各具特色的两种,它们在不同的应用场景下发挥着关键作用。 大面积的覆铜,顾名思义,是指在印刷电路板(PCB)的特定区域
    的头像 发表于 02-04 14:10 952次阅读

    ADS1259读取模数转换结果的时候是否是两种读取模式?

    咨询下ADS1259读取模数转换结果的时候是否是两种读取模式,一是读引脚(DIN),一是读寄存器,读寄存器的数据是进行数据校验? 还有不明白的是读寄存器的内容时,模数转化后的数据是放在9个寄存器哪几个里面呢?是否是可以随意
    发表于 01-22 07:18

    AMC1204有两种封装,SOIC-8和SOIC-16,功能一样吗?为什么要推出两种封装?

    呢?AMC1204,AMC1304这样做有什么好处吗? 2、AMC1204有两种封装,SOIC-8和SOIC-16,功能一样吗?为什么要推出两种封装?
    发表于 12-27 07:22