后台回复进群一起刷力扣????

点击卡片可搜索关键词????

读完本文,可以去力扣解决如下题目:

514. 自由之路(Hard

本文的封面图是一款叫做《辐射4》的游戏中的一个任务剧情画面:

这个可以转动的圆盘类似是一个密码机关,中间偏上的位置有个红色的指针看到没,你只要转动圆盘可以让指针指向不同的字母,然后再按下中间的按钮就可以输入指针指向的字母。

只要转动圆环,让指针依次指向 R、A、I、L、R、O、A、D 并依次按下按钮,就可以触发机关,打开旁边的门。

至于密码为什么是这几个字母,在游戏中的剧情有暗示,这里就不多说了。

那么这个游戏场景和动态规划有什么关系呢?

我们来没事儿找事儿地想一想,拨动圆盘输入这些字母还挺麻烦的,按照什么顺序才能使得拨动圆盘所需的操作次数最少呢?

拨动圆盘的不同方法所需的操作次数肯定是不同的。

比如说你想把一个字母对准到指针上,你可以顺时针转圆盘,也可以逆时针转圆盘;而且某些字母可能不止出现一次,比如上图中大写字母 O 就在圆盘的不同位置出现了三次,你到时候应该拨哪个 O 才能使得整体的操作次数最少呢?

我们之前也多次说过,遇到求最值的问题,基本都是由动态规划算法来解决,因为动态规划本身就是运筹优化算法的一种嘛。

力扣上就有一道这个转盘游戏的算法题,难度还是 Hard,但我当时看了一眼就做出来了,因为我以前思考过生活中一个非常有意思的例子可以类比到这个问题,下面来简单介绍一下。

关注了我的视频号的朋友,知道我弹过李斯特和肖邦的几首钢琴曲,但是没练过钢琴的读者可能不知道,练习钢琴曲谱是需要提前确定「指法」的。

五线谱的音符七上八下的,两个手的手指必须互相配合,也就是说你必须确定好每个音符用哪只手的哪个手指来弹奏,写到谱子上。

比如说我很喜欢的一首曲子叫做《爱之梦》:

这是我当时练习使用的谱子,可以看到上面标记了很多指法:

音符上的数字 1 代表用大拇指,2 代表用食指,以此类推。按照确定下来的指法不断练习,形成肌肉记忆,就算是练会一首曲子了。

指法这东西因人而异,比如手大的人可以让中指跨到大拇指的左边,手小的人可能就有些别扭,那同一段谱子对应的指法可能就不一样。

那么问题来了,我应该如何设计指法,才能最小化手指切换的「别扭程度」,也就是最大化演奏的流畅度呢

这里我就借助了动态规划算法技巧:手指的切换不就是状态的转移么?参考前文 动态规划套路详解,只要明确「状态」和「选择」就可以解决这个问题。

状态是什么?状态就是「当前需要弹奏的音符」和「当前的手的状态」

当前需要弹奏的音符,无非就是钢琴上 88 个琴键中的一个;手的状态也很简单,五个手指头,每个手指头要么按下去了要么没按下去,2 的 5 次方 32 种情况,5 个二进制位就可以表示。

选择是什么?选择就是「这个音符应该由哪个手指头来弹」,无非就是穷举五个手指头。

当然,结合当前手的状态,做出每个选择需要对应代价的,刚才说过这个代价是因人而异的,所以我需要给自己定制一个损失函数,计算不同指法切换的「别扭程度」。

现在的问题就变成了一个标准的动态规划问题,根据损失函数做出「别扭程度」最小的选择,使得整段演奏最流畅……

当然,最后这个算法时间复杂度太高了,我们刚才分析的只是单个的音符,但如果串成曲子,时空复杂度还得再乘曲子的音符数,很大。

而且,这个损失函数很难量化,钢琴的黑白键命中难度不同,而且「别扭程度」只能靠感觉,有点不严谨……

不过,本就没必要计算整首曲子的指法,只需要计算某些复杂段落的指法即可,这个算法还是比较有效的。

扯了这么多题外话终于要步入正题了,今天要讲的力扣第 514 题「自由之路」和钢琴指法问题有异曲同工之妙,如果你能理解钢琴的例子,相信你也能很快做出这道算法题。

题目给你输入一个字符串ring代表圆盘上的字符(指针位置在 12 点钟方向,初始指向ring[0]),再输入一个字符串key代表你需要拨动圆盘输入的字符串,你的算法需要返回输入这个key至少进行多少次操作(拨动一格圆盘和按下圆盘中间的按钮都算是一次操作)。

函数签名如下:

int findRotateSteps(string ring, string key);

比如题目举的例子,输入ring = "godding", key = "gd",对应的圆盘如下(大写只是为了清晰,实际上输入的字符串都是小写字母):

我们需要输入key = "gd",算法返回 4。

因为现在指针指的字母就是字母"g",所以可以直接按下中间的按钮,然后再将圆盘逆时针拨动两格,让指针指向字母"d",然后再按一次中间的按钮。

上述过程,按了两次按钮,拨了两格转盘,总共操作了 4 次,是最少的操作次数,所以算法应该返回 4。

我们这里可以首先给题目做一个等价,转动圆盘是不是就等于拨动指针?

原题可以转化为:圆盘固定,我们可以拨动指针;现在需要我们拨动指针并按下按钮,以最少的操作次数输入key对应的字符串。

那么,这个问题如何使用动态规划的技巧解决呢?或者说,这道题的「状态」和「选择」是什么呢?

「状态」就是「当前需要输入的字符」和「当前圆盘指针的位置」

再具体点,「状态」就是ij两个变量。我们可以用i表示当前圆盘上指针指向的字符(也就是ring[i]);用j表示需要输入的字符(也就是key[j])。

这样我们可以写这样一个dp函数:

int dp(string& ring, int i, string& key, int j);

这个dp函数的定义如下:

当圆盘指针指向ring[i]时,输入字符串key[j..]至少需要dp(ring, i, key, j)次操作

根据这个定义,题目其实就是想计算dp(ring, 0, key, 0)的值,而且我们可以把dp函数的 base case 写出来:

int dp(string& ring, int i, string& key, int j) {// base case,完成输入if (j == key.size()) return 0;// ...
}

接下来,思考一下如何根据状态做选择,如何进行状态转移?

「选择」就是「如何拨动指针得到待输入的字符」

再具体点就是,对于现在想输入的字符key[j],我们可以如何拨动圆盘,得到这个字符?

比如说输入ring = "gdonidg",现在圆盘的状态如下图:

假设我想输入的字符key[j] = "d",圆盘中有两个字母"d",而且我可以顺时针也可以逆时针拨动指针,所以总共有四种「选择」输入字符"d",我们需要选择操作次数最少的那个拨法。

大致的代码逻辑如下:

int dp(string& ring, int i, string& key, int j) {// base case 完成输入if (j == key.size()) return 0;// 做选择int res = INT_MAX;for (int k : [字符 key[j] 在 ring 中的所有索引]) {res = min(把 i 顺时针转到 k 的代价,把 i 逆时针转到 k 的代价);}return res;
}

至于到底是顺时针还是逆时针,其实非常好判断,怎么近就怎么来;但是对于圆盘中的两个字符"d",还能是怎么近怎么来吗?

不能,因为这和key[i]之后需要输入的字符有关,还是上面的例子:

如果输入的是key = "di",那么即便右边的"d"离得近,也应该去左边的"d",因为左边的"d"旁边就是"i",「整体」的操作数最少。

那么,应该如何判断呢?其实就是穷举,递归调用dp函数,把两种选择的「整体」代价算出来,然后再做比较就行了。

讲到这就差不多了,直接看代码吧:

// 字符 -> 索引列表
unordered_map<char, vector<int>> charToIndex;
// 备忘录
vector<vector<int>> memo;/* 主函数 */
int findRotateSteps(string ring, string key) {int m = ring.size();int n = key.size();// 备忘录全部初始化为 0memo.resize(m, vector<int>(n, 0));// 记录圆环上字符到索引的映射for (int i = 0; i < ring.size(); i++) {charToIndex[ring[i]].push_back(i);}// 圆盘指针最初指向 12 点钟方向,// 从第一个字符开始输入 keyreturn dp(ring, 0, key, 0);
}// 计算圆盘指针在 ring[i],输入 key[j..] 的最少操作数
int dp(string& ring, int i, string& key, int j) {// base case 完成输入if (j == key.size()) return 0;// 查找备忘录,避免重叠子问题if (memo[i][j] != 0) return memo[i][j];int n = ring.size();// 做选择int res = INT_MAX;// ring 上可能有多个字符 key[j]for (int k : charToIndex[key[j]]) {// 拨动指针的次数int delta = abs(k - i);// 选择顺时针还是逆时针delta = min(delta, n - delta);// 将指针拨到 ring[k],继续输入 key[j+1..]int subProblem = dp(ring, k, key, j + 1);// 选择「整体」操作次数最少的res = min(res, 1 + delta + subProblem);// PS:加一是因为按动按钮也是一次操作}// 将结果存入备忘录memo[i][j] = res;return res;
}

这段代码是 C++ 写的,因为我觉得涉及字符串的算法 C++ 更方便一些,这里说一些语言相关的细节问题:

1、unordered_map就是哈希表,当访问不存在的键时,会自动创建对应的值,所以可以直接push_back而不用担心空指针错误。

2、min函数的参数都是 int 型,所以必须先用一个 int 型变量n存储ring.size(),然后调用min(delta, n - delta),否则会报错。

至此,这道题就解决了,是不是和钢琴的例子有异曲同工之妙呢?

如果本文对你有帮助,可以点个「在看」或者「赞」,这样微信会给你推荐更多相似内容。

精华文章目录点这里 ????

_____________

学好算法靠套路,认准 labuladong,知乎、B站账号同名。公众号后台回复「进群」可加我好友,拉你进算法刷题群。

扫码关注我的微信视频号,不定期发视频、搞直播:

练琴时悟出的动态规划算法,帮我通关了《辐射4》相关推荐

  1. 动态规划算法帮我通关了“魔塔”

    读完本文,可以去力扣解决如下题目:174.地下城游戏(Hard) 「魔塔」是一款经典的地牢类游戏,碰怪物要掉血,吃血瓶能加血,你要收集钥匙,一层一层上楼,最后救出美丽的公主. 现在手机上仍然可以玩这个 ...

  2. 动态规划算法帮我通关了魔塔!

    后台回复进群一起刷力扣???? 点击卡片可搜索关键词???? 读完本文,可以去力扣解决如下题目: 174.地下城游戏(Hard) 「魔塔」是一款经典的地牢类游戏,碰怪物要掉血,吃血瓶能加血,你要收集钥 ...

  3. 在练琴时关于音乐与计算机结合的碎思考

    正经人在练琴时开的小差 读心术 智能谱曲 今天练琴时突然明白了之前的一些误区,结果如下:我练琴的目的暂时就是下面几个--1.自己弹出好听的曲子/通过即兴伴奏把曲子钢琴化.优化/与多功能电子琴等结合后创 ...

  4. 要想成为郎朗,请AI监督练琴可不行!

    文 | 魏启扬 来源 | 智能相对论(ID:aixdlun) 在决定让小孩学琴之前,老师就一再强调:"学琴是个长久的事情,三分学,七分练,课后的练习很重要!" 作为一个琴童的家长, ...

  5. 动态规划算法的优化技巧

    动态规划是信息学竞赛中一种常用的程序设计方法,本文着重讨论了运用动态规划思想解题时时间效率的优化.全文分为四个部分,首先讨论了动态规划时间效率优化的可行性和必要性,接着给出了动态规划时间复杂度的决定因 ...

  6. 每日练琴菜单 ---非常有效的古筝学习方法

    1 .时间分配 弹古筝者,必须每天练琴.常言道:"一天不练自己知道,两天不练同行知道,三天不练老师知道,四天不练大家都知道."特别是学习古筝专业的学生,每天一定要保证3-5小时的练 ...

  7. 五大常用算法之二:动态规划算法

    基本概念 动态规划过程是:每次决策依赖于当前状态,又随即引起状态的转移.一个决策序列就是在变化的状态中产生出来的,所以,这种多阶段最优化决策解决问题的过程就称为动态规划. 基本思想与策略 基本思想与分 ...

  8. 矩阵连乘问题(动态规划算法)

    动态规划算法思想简介: 将一个问题分解为多个子问题,这点和分治法类似,但是每个子问题不是独立的而是相互联系的,所以我们在求解每个子问题的时候可能需要重复计算到其他的子问题,所以我们将计算过的子问题的解 ...

  9. 【字符串】最长回文子串 ( 动态规划算法 ) ★

    文章目录 一.回文串.子串.子序列 二.最长回文子串 1.动态规划算法 2.动态规划算法代码示例 一.回文串.子串.子序列 " 回文串 ( Palindrome ) " 是 正反都 ...

最新文章

  1. Python3 迭代器与生成器
  2. 手机屏幕镜像翻转软件_可以把ipad投屏到电视吗?屏幕镜像一键投屏
  3. 标准C++的类型转换符:static_cast、dynamic_cast、reinterpret_cast和const_cast(转载)
  4. 为什么说减速带是脑残的设计?
  5. wamp2.5可用php5.6,局域网访问,多站点配置
  6. Java FlameGraph 火焰图
  7. 单尺度二维离散小波重构(逆变换)idwt2
  8. SQLite入门笔记
  9. Java 数组转字符串
  10. 人体面部检测python_使用Python检测面部特征
  11. fpgrowth算法c语言,关联分析:Apriori和FPgrowth算法原理
  12. Python_把csv文件导入数据库
  13. 一个简洁好用的翻译程序
  14. 旧的华为手机刷Android9,直播mate9刷安卓10!
  15. 【MySQL学习】DQL语言的学习 Data Query Language
  16. 机器学习-数据科学库 13 政治献金数据案例
  17. 计算机更改刷新频率,win10系统更改屏幕刷新频率的设置方案
  18. CVE-2017-0199漏洞分析
  19. RP产品原型资源分享-论坛类
  20. 抛物型偏微分方程的Crank-Nicolson 方法; Richardson 外推法;紧差分法

热门文章

  1. C++break语句用法
  2. 2020-01-15 Oracle JDK Migration Guide
  3. 集训队每周一赛2020-03-06(思维+暴力)
  4. 计算机图形学中消隐的相关概念及算法
  5. Ubuntu系统性能优化详细教程
  6. python 判断节假日,Python3自动签到 定时任务 判断节假日的实例
  7. 风口上的低代码:诱人的故事,危险的豪赌
  8. 惠普LaserJet M1216nfh MFP激光打印机开机提示“安装黑色碳粉盒”亮黄灯
  9. 国外LEAD联盟赚钱,日付款联盟推荐
  10. 四六级考试报名系统c语言,四六级考试系统课程设计报告.doc