动态规划难题:转盘问题

学到现在,动态规划的全套流程我们已经彻底掌握了,最后我再写几道较难、较实用的应用题,喜欢程序设计的朋友们可以看看。

LeetCode514 自由之路(转盘问题)

题目介绍:

给定一个字符串 ring ,表示刻在外环上的编码;给定另一个字符串 key ,表示需要拼写的关键词。您需要算出能够拼写关键词中所有字符的最少步数。

最初,ring 的第一个字符与 12:00 方向对齐。您需要顺时针或逆时针旋转 ring 以使 key 的一个字符在 12:00 方向对齐,然后按下中心按钮,以此逐个拼写完 key 中的所有字符。

旋转 ring 拼出 key 字符 key[i] 的阶段中:

  1. 您可以将 ring 顺时针或逆时针旋转 一个位置 ,计为1步。旋转的最终目的是将字符串 ring 的一个字符与 12:00 方向对齐,并且这个字符必须等于字符 key[i]

  2. 如果字符 key[i] 已经对齐到12:00方向,您需要按下中心按钮进行拼写,这也将算作 1 步。按完之后,您可以开始拼写 key 的下一个字符(下一阶段), 直至完成所有拼写。

题目分析:

先把问题转换一下,转动圆盘,指针不动其实等价于 圆盘不动,转动指针。所以我们可以分析出这道题目有两种状态:指针指向字符串ring 的哪个位置此时需要找key的第几个字符,也就是到哪一步了,该指向哪个字符才能进行下一步了

现在考虑dp函数该怎么定义

dp函数如何定义呢?我们先考虑从起点到终点的正向转移能否适用:

从起点考虑对应的dp[i][j]为 当指针指向ring[i]时,输入字符串key[0...j]至少需要dp[i][j] 步操作。该怎么进行状态转移呢?要想转第j个字符,必须先把前j-1个字符转好,但是怎么从第j-1个字符递推到第j个字符呢?

按照往常思路,找到第j-1个字符的最少操作数再加上从第j-1个字符的位置转移到第j个字符的位置所需要的最少操作数就可以了,细致的朋友会发现,这种思路就是贪心算法的思路。但是,贪心的思路可能会让模型陷入局部最优解的困境之中,我们最终得到的结果并不是整体的最小值,待会我会举个例子来说明确实存在陷入局部最优的情况。

通过 选择 来确定怎么进行状态转移

选择 就是 如何拨动指针得到待输入的字符 ,有几种选择形式呢?通过这个选择的内容,我们大致可以推出:

  • 是逆时针拨动还是顺时针拨动?

  • 待输入的字符可能有相同的好几个,该选择哪一个呢,直接选择离第j-1个字符最近的待输入字符吗?

第一个问题好解决,逆时针还是顺时针拨,只需要判断是顺时针需要的步数少,还是逆时针需要的步数少即可。直接用下面这一段代码进行体现:

 int delta = std::min(abs(l-k),n-abs(l-k)); 
 //l为待输入字符的下标,k为输入第j-1个字符完毕时指针指向的下标,n为字符串`ring`的字符个数。

第二个问题与状态转移有直接关系,多个字符我该选择哪一个呢?直接选择最近的吗?我们先试用一下贪心算法:

贪心思路:

j-1个字符确定下来,直接在指针指向第j-1个字符的基础上找最近的第j个字符。先给出代码框架,

 int dp(String ring,int i,String key,int j){
     int subProblem = dp(ring,i,key, j-1); //找第j-1个字符时
     
     for(int k : 找出所有第j个字符的下标){
         int delta = abs(???(j-1) - k);
         //确认顺时针还是逆时针
         delta = min(delta,ring.size()-delta);
     }
     return subProblem + delta;
 }

我们发现,代码框架给不出来,缺少指向第j-1个字符的指针。这时只要更改一下dp函数定义,同时返回一个下标来指向第j-1个字符即可,如下:

 int dp(String ring,int i,String key,int j,int &index){
     int subProblem = dp(ring,i,key, j-1); //找第j-1个字符时
     
     int pre_index = index;
     int result = INT_MAX;
     for(int k : 找出所有第j个字符的下标){
         int delta = abs(pre_index - k);
         //确认顺时针还是逆时针
         delta = min(delta,ring.size()-delta);
         
         if(result > 1 + delta + subProblem){
             result =  + delta + subProblem;
             index = k;
         }
     }
     return result;
 }

以上是贪心算法的代码,但是我可以告诉你,贪心算法的思路是错的,会陷入局部最优解的困境。请看下面这个例子

动态规划从终点开始分析问题思路:

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

假设现在我想输入的字符为key[j] = 'D',圆盘中有两个字符'D',可以顺时针找,也可以逆时针找,可以找右边第一个‘D’,也可以找左边第二个’D’,有四种选择方法,我们找次数最少的那个。

刚刚的代码已经告诉了我们,顺时针还是逆时针转取决于哪个值更小,按照贪心算法的思路(刚刚的代码逻辑),我们应该选择右边第一个‘D’,但是选择完D后再选择I时,一共进行了 1+1+3+1,6步操作(确定字符吗,按下button也是一步),但是,如果选择左边第二个‘D’,再选择I时,我们只进行了2+1+1+1,5步操作。所以,从起点开始利用贪心算法分析会陷入局部最优解困境,不可取

从这个例子可以看出,当我们对dp(i,j) 进行分析时,还需要考虑选择了第j 个字符之后,j+1,j+2,...这些字符该怎么选取。既然从起点开始分析不行,那我们考虑从终点开始分析,既然选择第j个字符时,需要考虑j+1,j+2,...这些字符,那我们直接先把后面的字符考虑清楚之后再考虑第j个字符。

重新定义dp函数

dp(i,j)为当指针指向ring[i]时,输入key[j,j+1,...] 字符串所需要的最少操作数。根据这个定义,最终我们要求的结果其实就是dp(0,0)

重新思考状态转移

根据定义,我们可以得出base case: dp[...][n] = 0,key 为空时自然不需要任何操作,那如何求dp[i][j]呢?直接给出代码:

 int dp(String ring,int i,String key,int j){
     //base case:
     if(j == key.size()){
         return 0;
     }   
     int result = INT_MAX;
     for(int k : 找出所有第j个字符的下标){
         int delta = abs(i - k);
         //确认顺时针还是逆时针
         delta = min(delta,ring.size()-delta);
         int subProblem = dp(ring,k,key,j+1);
         result = min(result,1+delta+subProblem);
     }
     return result;
 }
完整代码:
 //建立字符与索引映射 字符->索引列表
 unordered_map<char, vector<int>> charToIndex;   //map存储键值对,没有这个键则会新建一个键值对
 //备忘录
 vector <vector<int>> memo;
 ​
 int findRotateSteps(string ring, string key) {
     int m = ring.size();
     int n = key.size();
     //备忘录全部初始化为0
     memo =  vector <vector<int>>(m, vector<int>(n, 0));
 ​
     //存储映射关系
     for (int i = 0; i < m; ++i) {
         char c = ring[i];
         charToIndex[c].push_back(i);
     }
     //圆盘最初指向12点钟方向,从第一个字符开始输入key
     return dp(ring, 0, key, 0);
 }
 ​
 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 result = INT_MAX;
     for (int k: charToIndex[key[j]]) {
         int delta = abs(i - k);
         //确认顺时针还是逆时针
         delta = min(delta, (int)ring.size() - delta);
         int subProblem = dp(ring, k, key, j + 1);
         result = min(result, 1 + delta + subProblem);
     }
     memo[i][j] = result;
     return result;
 }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

东秦小熊

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

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

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

打赏作者

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

抵扣说明:

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

余额充值