今天这题用到了动态规划,对于我来说有一丢丢难度,不过还是弄懂了。
一开始我是使用了贪心算法去做的:(此方法无法得到最优解)
思路是使用两个标识,一个顺时针查找,一个逆时针查找,选取查找到相应字母所用步数的最小值。
class Solution {
public:
int findRotateSteps(string ring, string key) {
int z1=0;//顺时针查找标识
int z2=0;//逆时针查找标识
int sum=0;//次数计算
for(int i=0;i<key.size();i++)//针对每一个key里面的字符
{
while(ring[z1]!=key[i]&&ring[z2]!=key[i])//寻找匹配字符,因为题目说了一定会有匹配
{
z2 = z2-1;
z1 = z1+1;
sum = sum+1;
if(z2<0)
{
z2 = ring.size()-1;//越界重置位置
}
if(z1==ring.size())
{
z1 = 0;//越界重置位置
}
}
if(ring[z1]==key[i])
{
z2=z1;//查找到了则将标识位置重置
}
else
{
z1=z2;//查找到了则将标识位置重置
}
}
sum = sum + key.size();//需要加上按下按钮次数,即key字符个数
return sum;
}
};
之后,会有一个解答错误的提示:
然后我就悟了,因为在Y之后选取N的时候,会涉及到一个选哪个N的问题,这个选N可能在当下是最优解,但是再到l的时候,会变成局部最优解而不是全局最优解(贪心算法的不足之一)。
比如你选了第三个N,那么再让字符旋转到 l 时,就会慢于第一个n。
于是,就有了官方题解的动态规划法:
记录每一种选法的路径值最后相加
官方的方法就类似于寻找最短路径或者是最小生成树的算法。
1.先用vector pos【26】数组记录每个字母出现在Ring中的位置
2.用一个二维数组记录当前状态下Key【i】字符到对应Ring【j】字符的距离的最小值,即是选择逆时针旋转还是顺时针旋转(也就是dp【i】【j】,这里dp【i】【j】矩阵的初始值要“无穷大”,因为需要最后计算总距离)
3.最后统计每列的值,最小的即为最短距离。
以下是算法带测试
#include<iostream>
#include<string>
#include<string.h>//memset()
#include<algorithm>//min,max
#include<cmath>
#include<vector>
using namespace std;
class Solution {
public:
int findRotateSteps(string ring, string key) {
int n = ring.size(), m = key.size();
vector<int> pos[26];//pos中有26个元素,每一个都是vector<int>类型
for (int i = 0; i < n; ++i) {
pos[ring[i] - 'a'].push_back(i);//记录ring中相同字母出现的位置
}
int dp[m][n];
memset(dp, 0x3f3f3f3f, sizeof(dp));//将数组都初始化为无穷大
for (auto& i: pos[key[0] - 'a']) {//对POS中每一个字母进行遍历
dp[0][i] = min(i, n - i) + 1;
}
for (int i = 1; i < m; ++i) {//对Key的每个字母循环
for (auto& j: pos[key[i] - 'a']) {//对当前比对循环
for (auto& k: pos[key[i - 1] - 'a']) {//对上一个Key值所在ring中的位置循环
dp[i][j] = min(dp[i][j], dp[i - 1][k] + min(abs(j - k), n - abs(j - k)) + 1);//其中 min(abs(j - k), n - abs(j - k)) + 1表示在当前第 k个字符与 12:00方向对齐时第 j 个字符旋转到 12:00方向并按下拼写的最少步数。
//dp[i][j] 表示从前往后拼写出 key 的第 i个字符,ring 的第 j 个字符与 12:00方向对齐的最少步数
}
}
}
return *min_element(dp[m - 1], dp[m - 1] + n);//最后把对应列加在一起,找出最小值
}
};
int main()
{
string ring = "godding";
string key = "gd";
Solution s;
int sum = s.findRotateSteps(ring,key);
cout<<sum;
}
最后,附上这次代码中一些解析:
1.auto是自动识别变量的类型,就是根据后面的值来决定自己是什么,太久不用都快忘了。
有以下特性:
(1)auto不能作为函数参数
(2)auto不能直接声明数组
(3)为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
(4)auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等进行配合使用。
(5)auto不能定义类的非静态成员变量
(6)实例化模板时不能使用auto作为模板参数
auto详情请见
2.关于无穷大0x3f3f3f的一些知识
(1)0x3f3f3f3f的十进制是1061109567,也就是109级别的(和0x7fffffff一个数量级),而一般场合下的数据都是小于109的,所以它可以作为无穷大使用而不致出现数据大于无穷大的情形。
(2)另一方面,由于一般的数据都不会大于10^9,所以当我们把无穷大加上一个数据时,它并不会溢出(这就满足了“无穷大加一个有穷的数依然是无穷大”),事实上0x3f3f3f3f+0x3f3f3f3f=2122219134,这非常大但却没有超过32-bit int的表示范围,所以0x3f3f3f3f还满足了我们“无穷大加无穷大还是无穷大”的需求。
(3)最后,0x3f3f3f3f还能给我们带来一个意想不到的额外好处:如果我们想要将某个数组清零,我们通常会使用memset(a,0,sizeof(a))这样的代码来实现(方便而高效),但是当我们想将某个数组全部赋值为无穷大时(例如解决图论问题时邻接矩阵的初始化),就不能使用memset函数而得自己写循环了(写这些不重要的代码真的很痛苦),我们知道这是因为memset是按字节操作的,它能够对数组清零是因为0的每个字节都是0,现在好了,如果我们将无穷大设为0x3f3f3f3f,那么奇迹就发生了,0x3f3f3f3f的每个字节都是0x3f!所以要把一段内存全部置为无穷大,我们只需要memset(a,0x3f,sizeof(a))。 所以在通常的场合下,const int INF = 0x3f3f3f3f;真的是一个非常棒的选择。