題目描述:
一條包含字母 A-Z
的消息通過以下方式進行了編碼:
'A' -> 1
'B' -> 2
...
'Z' -> 26
給定一個只包含數字的非空字符串,請計算解碼方法的總數。
示例 1:
輸入: "12"
輸出: 2
解釋: 它可以解碼為 "AB"(1 2)或者 "L"(12)。
示例 2:
輸入: "226"
輸出: 3
解釋: 它可以解碼為 "BZ" (2 26), "VF" (22 6), 或者 "BBF" (2 2 6) 。
要完成的函數:
int numDecodings(string s)
說明:
1、這道題給定一個字符串,字符串中只含有數字,數字1可以解碼為A,數字2可以解碼為B……數字26可以解碼為Z。
要求判斷有多少種解碼的方式,最終返回解碼方式的個數。
比如[2,2,6],那么可以解碼為2-2-6也就是BBF,也可以解碼為2-26也就是BZ,也可以解碼為26-2也就是ZB。
一共三種解碼方式,最終返回3。
2、這道題有兩種做法,一種是動態規划,另一種是遞歸。
動態規划比較快,可以beats 100.00% of cpp submissions,而遞歸只能beats 2.67% of cpp submissions。筆者把兩種做法都實現了一下,附在下文。
我們先看一下動態規划的思路,動態規划關鍵在於每個階段狀態的把握。
舉個例子,比如12224,我們首先對第一位,只能1這種解碼方法。
對第二位2,可以1-2(獨立),也可以12(合並),兩種解碼方法。
對第三位2,可以1-2-2(獨立),也可以12-2(獨立),也可以1-22(合並),三種解碼方法。
在這一步我們發現其實到當前位為止的解碼方法個數,就是上一步的解碼方法個數(在后面直接添加獨立的一位)+上一步獨立的個數(當然這里要判斷能不能合並在一起)。
所以我們只需要記住上一步的解碼方法個數和上一步的獨立的個數,就可以分不同階段去處理。
再接下來對第四位2,可以1-2-2-2,也可以12-2-2,也可以1-22-2,這三個都是直接在后面添加獨立的一位,也可以1-2-22,也可以12-22,這兩個就是把先前獨立的一位給合並了,所以當前總的解碼個數是3+2=5,當前獨立的個數就是3,也是上一步的總解碼個數。
過程如下:
1 | 2 | 2 | 2 | ||
獨立可合並下一位的個數 | 1 | 1 | 2 | 3 | |
總的解碼方式的個數 | 1 | 2 | 3 | 5 | |
當前例子 | 1 | 1-2 12 |
1-2-2 12-2 1-22 |
1-2-2-2 12-2 1-22-2 1-2-22 12-22 |
|
規律十分清晰,但我們還有一個情況沒有考慮到,就是可能會出現數字0。
比如110,第二個1這一步,當前總的解碼方式有1-1和11,兩種,獨立可合並下一位的個數有一種。
然后到了0這一步,只能合並了,於是總的解碼方式變成上一步獨立可合並下一位的個數1,解碼方式是1-10,當前這一位的獨立可合並個數清空為0。
那還有不能合並的呢,比如130,3這一步,仍然是1-3和13,總的有2種,獨立的有1種。
到0這一步,不能合並,於是總的解碼方式變成0,完全不能解碼,返回0。
所以,構造代碼如下:(附詳解)
int numDecodings(string s)
{
if(s[0]=='0')return 0;//邊界條件,如果第一位是字符0,那么完全不能解碼,直接返回0
int t1=1,t2=1,sum1,t3;//t1表示當前獨立可合並下一位的個數,t2表示當前總的解碼方式的個數
for(int i=1;i<s.size();i++)//從第二位開始處理
{
sum1=(s[i-1]-'0')*10+s[i]-'0';//如果跟前一位合並,計算合並之后的數值
if(sum1>=1&&sum1<=26)//如果數值符合條件,那么可以合並
{
if(s[i]!='0')//當前位不是0,那么t2加上t1的值,t1變成原本t2的值
{
t3=t2;
t2+=t1;
t1=t3;
}
else//當前位是0,比如10這種情況,那么t2變成t1的值,t1清空
{
t2=t1;
t1=0;
}
}
else//如果計算出來不能合並
{
if(s[i]!='0')//如果當前位不是0,比如227,后面的27就不能合並,於是t2不變,t1變成t2的數值
t1=t2;
else//如果當前位是0,又不能合並,比如30這種情況,那么直接返回0
return 0;
}
}
return t2;//最終返回t2這個總的解碼方式的個數
}
上述代碼實測0ms,beats 100.00% of cpp submissions。
如果有時間的話可以再看一下遞歸的做法,筆者最開始也是遞歸的思路,不斷地試探,這種思路比較熟悉。
沒時間的話就算啦,下面的文字可以直接略過。
舉個例子[2,2,2,2,2],我們先不斷遞歸,逐個處理,直至超出范圍,此時我們次數+1。
接着回退到上一層,也就是最后一個2,發現不能跟下一個數合並,於是再退到上一層,也就是倒數第二個2。
在這個時候發現可以合並,於是進入遞歸,但這時候下一個處理的數的位置要+2,而不是逐個處理。
接着再回退到上一層,發現第三個2和倒數第二個2可以合並,於是進入遞歸,這時候下一個要處理的數的位置+2,到達最后一個2那里。
……
我們可以總結出遞歸的操作,對於每一位而言,分兩個步驟:
①進入對下一位的遞歸處理。
②結束①之后,判斷能否與下一位合並,進入對下下位的遞歸處理。
所以我們可以構造出如下代碼:
int count=0,t;//全局變量
void digui(string& s,int index)
{
if(index==s.size())//如果超出了范圍,說明當前的嘗試是成功的
{
count++;
return;
}
digui(s,index+1);//第一個步驟
t=(s[index]-'0')*10+s[index+1]-'0';//第二個步驟
if(index+1<s.size()&&t<=26&&t>=1)
digui(s,index+2);
}
int numDecodings(string s)
{
digui(s,0);
return count;
}
上述代碼可以解決大部分情況,但是對於0的存在無能為力。比如10,只有一種解碼方式,但按照上述代碼,返回結果是2。
但其實處理到0這一位的時候,當前嘗試是失敗的,應該結束這種嘗試。
所以我們稍微修改一下代碼,如下:
int count=0,t;
void digui(string& s,int index)
{
if(s[index]=='0')//增加了對於0的判斷
return;
if(index==s.size())
{
count++;
return;
}
digui(s,index+1);
t=(s[index]-'0')*10+s[index+1]-'0';
if(index+1<s.size()&&t<=26&&t>=1)
digui(s,index+2);
}
int numDecodings(string s)
{
digui(s,0);
return count;
}
上述代碼可以通過測試,但是實測484ms,beats 2.67% of cpp submissions。遞歸太耗時間了。