简要说明:
(1)题目来源LeetCode。
链接:https://leetcode-cn.com/problems/split-array-into-fibonacci-sequence/
(2)由于作者水平限制和时间限制,代码本身可能仍有一些瑕疵,仍有改进的空间。也欢迎大家一起来讨论。
——一个大二刚接触《数据结构》课程的菜鸡留
题目简介
给定一个字符串S,进行若干次分隔,使结果为Fibonacci序列。
假设输入字符串S=“123456579”,可以将它分割成一种Fibonacci序列[123, 456, 579]。
对于Fibonacci序列F,要求满足:
● F.length ≥ 3,即满足要求的Fibonacci序列至少要有三项。
● 0 ≤ F[i] ≤ 231-1,即Fibonacci序列的每一项都是非负整数,并且符合32位有符号整数类型。
●F[i+2] = F[i+1] + F[i] ( 0 ≤ i ≤ F.length-2 ),即每一项都等于其前两项之和(如果有的话)。
此外,F[i]不得以“0”开头,即数字02是不合法的,除非这个数字本身是0。
要求返回从字符串S拆分得到的任一组Fibonacci序列块,如果不能拆分则返回[]。
参考样例1:
输入: “123456579”
输出: [123, 456, 579]
参考样例2:
输入: “112358130”
输出: []
思路分析
注:仅代表个人思路。
(1) 与上一篇博客解数独问题有些类似,这道题我第一反应到的也是回溯法,即试探每一个可能的拆分,看看是否满足Fibonacci序列的要求。如果满足,可以继续拆分;不满足则分情况讨论,是延长当前块还是回到之前重新拆分。
(2) 由于返回值要求是一个序列,因此使用vector容器来实现。为了方便期间,之前拆分的记忆我也存放在vector中(这在之后会有体现)。
(3) 继续第1点的讨论,满足条件的拆分可以继续向后查看,不满足条件时需要分成两种情况。首先,如果目前拆分块翻译得到的int值大于预期值时,那么就需要回退了;如果小于,说明目前拆分块太短,可以继续粘下一个字符,查看是否新的拆分块翻译出来的int值合乎要求,如此往复。
(4) 那么如何实现回退的步骤?一种方法是放在stack中,但问题又是,stack应该储存什么内容呢?是具体的string还是?在这里,我用vector类似地实现了stack的功能,储存的是上一次操作的下标(因为代码中实际上关于vector的增删只用到了push_back()和pop_back(),这和stack的push()和pop()大同小异)。
(5) 此外还需要注意几点。一个是当试探前两个数即F[0]和F[1]时,是不需要判断是否满足条件的。其次,得到的划分块S未必能翻译成int型,可能越界,也可能以0开始(这在输入要求中被指明是不合法的),就必须要避免可能带来的overflow的问题。1
代码部分2
int STRtoINT(string& S) { //将string转换为int并返回.
int co=1;
int sum=0;
int i;
for (i=S.length()-1;i>0;i--) {
sum+=(S[i]-'0')*co;
co*=10;
}
sum+=(S[i]-'0')*co; //最后一步单独执行以防co自乘10时产生溢出.
return sum;
}
bool canSTRtoINT(string& S) { //判断给定string是否可以转换为int.
if (S.length()>1&&S[0]=='0') return false; //如"02"是不合法的.
if (S.length()<10) return true;
if (S.length()>10) return false;
char c[10]={'2','1','4','7','4','8','3','6','4','7'};
int sign;
for (int i=0;i<10;i++) {
sign=S[i]-c[i];
if (sign>0) return false;
if (sign<0) return true;
}
return true;
}
vector<int> splitIntoFibonacci(string S) {
vector<int> result;
vector<int> ptr;
int i=0;
int end;
long judge;
string cut;
ptr.push_back(0);
while (i<S.length()) {
//从ptr记录的尾指针向i截取string, 即可能产生的结果.
end=ptr.size()-1;
cut.clear();
for (int k=ptr[end];k<=i;k++) cut.push_back(S[k]);
//如果result已大于等于2个元素,则比较是否符合Fibonacci数列的要求.
//否则,直接放入作为可能的前两个数的划分. 如果无法组成,之后会回退到这里.
if (result.size()>1) {
//判断截取到的string转换int值是否等于前两项之和.
//分为三种情况,大于、等于和小于.
//首先还需要判断cut是否可以转换为int值以免越界. 不能的话就需要直接回退.
if (!canSTRtoINT(cut)) judge=1; //直接设judge=1, 即需要回退.
else {
judge=STRtoINT(cut)-result[result.size()-1];
judge-=result[result.size()-2];
}
if (judge==0) { //相等的情况
//直接放入result,指针后移.
result.push_back(STRtoINT(cut));
ptr.push_back(++i);
}
else if (i==S.length()-1||judge>0) { //需要回退的情况(包括大于)
//说明需要回退. 如果i已经到最后一位下标了且不满足judge==0,那么也需要回退.
//在回退时,需要更改int的值,即下一次截取的终点.
//同时还会有一种极端情况,如果ptr单元素,说明无法截取到Fibonacci序列, 此时返回空vector.
if (ptr.size()==1) {
ptr.pop_back();
return ptr; //此时ptr即空vector.
}
else {
i=ptr[ptr.size()-1];
ptr.pop_back();
result.pop_back();
}
}
else { //小于的情况,那么指针仍然需要后移.
++i;
}
}
else { //result元素数小于2,只需要考虑加入元素
//此时考虑到有两种需要回退的情况.
//第一种是i已经到了尾部.
//第二种是cut无法转换为int类型,同样需要回退.
//回退时考虑到ptr.size(),判断是否确认必找不到Fibonacci序列.
if (i==S.length()-1||!canSTRtoINT(cut)) { //无法转换为int,此时需要回退.
if (ptr.size()==1) {
ptr.pop_back();
return ptr; //此时返回空vector.
}
else { //操作同回退操作.
i=ptr[ptr.size()-1];
ptr.pop_back();
result.pop_back();
}
}
else {
result.push_back(STRtoINT(cut));
ptr.push_back(++i);
}
}
}
return result;
}
●关于这段代码,其实如果空说也很难说,因为这也是我几乎磨了一个小时有余磨出来的代码(虽然当时在参加一个活动并且我甚至有种它在干扰我的感觉【苦笑】),很多地方也已经有些抽象了。可以在纸上或白板上画一张图,模拟一下这个步骤。我在调试bug的时候,也是一步步画来确定一些变量到底需要设为几、是先出vector后改值还是先改值之类的。
●为什么ptr不用stack的原因,我给自己找了个好理由:(其实有想过但是后来没有改)在一些返回空vector的情况下,这样只需要操作ptr即可。
改进空间
(1) 说句实话目前我在LeetCode上做题,消耗时间的测量似乎不太精准,偶尔蹦出个0ms的反馈。不过这道题内存能超过近70%的用户,已经是突破个人到目前的最高记录了。(希望以后还能破)不过也是找到了些问题。
(2) 承接上文,事实上在具体操作时,不出现太奇怪的问题的话,ptr向量的长度总是恰等于result的长度加1,因此在外层while循环体的第一句if分支处内部的判断ptr.length()==1与否是多余的。
(3) 代码中的judge部分为了方便起见使用了long型(因为可能前两项都比较大导致使用int时会溢出),可能会有更好的解决方案。坦率地说,这类溢出的问题是我个人感到挺头疼的问题,所幸单单一个long和int内存使用差距并不大。
(4) 还是那句话,总会有一些更好的算法及数据结构。
补充部分
(1) 代码可以修改成输出所有可能的合法的Fibonacci序列划分,由于时间原因我并没有改。
(2) 暂时想不到什么了。说句实话,这道题的难度比之前做到的那些每日一题都更困难些,所花费的时间也比前几天更多了。不过也正是如此,题目难度在加大,我也并非在原地踏步。期待之后的学习。