# APCS 202306 第3題 磁軌移動序列 (ZeroJudge k733) [ZJ題目連結](https://zerojudge.tw/ShowProblem?problemid=k733) (此題解先說明方法再揭示範例程式,包含python與C++) ###### tags: `apcs` ## 方法說明 這題要計算在一連串磁軌編號之間移動的距離總和。一個磁軌編號以Txx表示,其中xx是10~99的數字,例如T12T34表示先移動到12號磁軌接著移動到34號磁軌,題目中說明一開始的位置在10號磁軌,因此,如果輸入是T12T34,則移動距離是$|12-10|+|34-12|=2+22=24$。 比較困難的是輸入字串中可以包含迴圈,迴圈以Ly...E表示,其中y是1~9,表示迴圈重複y次,而...則是重複的部分。例如L2T12T34E就是T12T34重複兩次,也就是T12T34T12T34,所以輸入此字串的移動距離為$|12-10|+|34-12|+|12-34|+|34-12|=68$。 要展開一個迴圈看起來還不是太難,真正困難的是迴圈中還可以有迴圈,例如L3T20L2T12T34ET15E。此例中外層迴圈L3...E,中間的必須重複3次,中間的是T20L2T12T34ET15分成T20與L2T12T34E與T15,其中L2T12T34E是T12T34重複兩次=T12T34T12T34,所以整個字串是T20T12T34T12T34T15重複3次。 看似複雜的讓人昏頭了,但其實如果我們稱此種字串為S字串,則S字串總歸就是兩種樣子: * S字串 = Txx + S字串 * S字串 = Ly + S字串 + E 此處的+是字串連接的意思,這是一種遞迴方式定義的結構。要解遞迴定義的結構當然以遞迴程式最為合適。我們先來看看,所求的距離要如何計算。 對於第一種形式Txx+S的結構,距離的計算就是相鄰編號差的絕對值,多個Txx連接時,我們可以用變數記錄前一項編號與目前的編號,一路滑過去過去,不斷的計算差值並予以加總。 對於第二種迴圈結構Ly,我們需要三個資料$(cost,head,tail)$,其中$cost$為迴圈內部距離的總和、$head$是迴圈內的第一個磁軌編號、而$tail$是迴圈內的最後一向磁軌編號。迴圈內的總移動距離就可以計算為$cost\times y+|head-tail|\times (y-1)$,因為內部重複$y$次,而頭尾相鄰$y-1$次。 根據以上分析,我們的遞迴程式中就要計算cost,head,tail三個值。我們設計一個遞迴的副程式,主要流程如下: ``` 輸入字串track與目前位置idx放在global變數 副程式 seq() 將傳回cost,head,tail while true: head = tail = -1 // 初設-1表示沒有 total = 0 // 紀錄目前距離總和 if end of track or track[idx]=E then update idx and return if track[idx]=='T' then 計算目前編號 update idx,head,tail,total if track[idx]=='L' then 計算迴圈次數y並更新idx 遞迴呼叫seq()取得迴圈內部距離,第一個與最後編號 update head,tail,total end of while ``` **小技巧:如何計算第一個head與最後一個編號tail** 我們可以初設tail = -1,每次處理時,如果tail==-1就表示目前為第一個,此時便設定head。同時,每次都將目前的編號設為tail,離開時tail就是最後被設定的編號,也就是最後一個編號。 ## 完全解 直接說明完全解的範例程式。首先是Python的程式碼。Python的副程式可以回傳多個值,所以我們直接讓他回傳cost, head與tail。第8行開始無窮迴圈,每次依照字串目前的內容track[idx]來做不同的處理,首先判斷字串是否已經結束(第9行),若是,則回傳結束。然後是'T'的情形(第11行),先抓取後兩位數字,然後更新資料。如果是'L'的情形(第17行),先抓取迴圈次數,然後遞迴呼叫,再更新資料。最後是'E'的情形(第25行)。每一種狀況都要記得更新idx也就是下一個字串處理位置。 主程式只要輸入字串後呼叫此副程式就可以了,不要忘了加上起始位置10的距離。 ### Python code ```python= track = input() idx=0 # return cost and head and tail def seq(): global track,idx total = 0 head = tail = -1 while True: if idx >= len(track): # end of input return total,head,tail if track[idx]=='T': t = int(track[idx+1:idx+3]) idx += 3 if tail<0: head = t #first track else: total += abs(tail-t) tail = t elif track[idx]=='L': t = int(track[idx+1]) idx += 2 cost,h,e = seq() #recursive find a seq if tail<0: head = h else: total += abs(tail-h) tail = e total += cost*t+abs(h-e)*(t-1) else: # End of loop idx += 1 return total,head,tail #end while #end seq total,h,e = seq() print(total+abs(10-h)) ``` ### C++ code 以下是C\++的程式碼。C++的副程式只能回傳一個值,所以這裡我們讓他回傳cost,而head與tail則用傳址參數的方式將更新後的值回傳。第11行開始無窮迴圈,每次依照字串目前的內容track[idx]來做不同的處理,首先判斷字串是否已經結束(第12行),若是,則回傳結束。然後是'T'的情形(第13行),先抓取後兩位數字,然後更新資料。如果是'L'的情形(第19行),先抓取迴圈次數,然後遞迴呼叫,再更新資料。最後是'E'的情形(第28行)。每一種狀況都要記得更新idx也就是下一個字串處理位置。 主程式只要輸入字串後呼叫此副程式就可以了,不要忘了加上起始位置10的距離。 ```cpp= #include<bits/stdc++.h> using namespace std; typedef long long ll; int idx=0; string track; // return cost and head and tail ll seq(int &head, int &tail) { ll total = 0, t; head = tail = -1; while (true) { if (idx>=track.length()) return total; if (track[idx]=='T') { t = stoi(track.substr(idx+1,2)); idx += 3; if (tail<0) head = t; else total += abs(tail-t); tail = t; } else if (track[idx]=='L') { t = stoi(track.substr(idx+1,1)); idx += 2; int h,e; ll cost = seq(h,e); if (tail<0) head = h; else total += abs(tail-h); tail = e; total += cost*t+abs(h-e)*(t-1); } else { // E idx += 1; return total; } } } int main() { int xx,i,j; cin >>track; ll total = seq(i,j); total += abs(10-i); cout << total <<endl; return 0; } ``` ## 非遞迴版本 這題想要考的就是遞迴,所以遞迴版本應該在考試時可以得到100分。不過在ZeroJudge上的測資有包含迴圈層數非常大的情況,Python有遞迴層數的限制(default=1000),因此Python的遞迴版本在ZJ上只能得到80分,以下我們提出一個非遞迴的Python版本。 基本上,**每一個遞迴程式背後都有一個堆疊**。所以利用堆疊就可以寫出非遞迴的版本。主要的觀念是一樣的,我們沿著字串從前往後掃過去,我們時時記錄著head,cost,tail三個值。 Python的list當堆疊非常方便,append()就是尾端加入,pop()就是取出並刪除最後一個。此外list中不必相同型態,可以同時有的元素是整數而有的元素是list。 stk是我們要記錄用到的堆疊,第3行先放個-1是為了省掉後面檢查堆疊是否為空。另外再用一個loop也是當作堆疊,紀錄迴圈的圈數。 當碰到迴圈的開頭時(第7行),我們在堆疊上放個-1當作迴圈的界線,然後將圈數抓出來放入loop中(第9行)。如果碰到的是'T',我們抓出編號t後,要檢查前方是否為分界,也就是這個是否是迴圈或全體的第一個,如果是第一個,我們在堆疊中放上[t,0,t],意思是頭尾都是t,距離總和=0。如果不是第一個,我們就將目前編號併入之前計算的結果,也就是更新距離總和以及tail。 如果是'E',也就是迴圈結尾,我們從loop中取出此次迴圈重複數,並從stk中取出head,cost,tail,計算此迴圈的距離總和後,與T的狀況一樣,看看是否要併入前面的資料中。 最後的答案就在堆疊的最後一個,但不要忘了加上起始位置10的距離。 ```python= # stack, non-recursive s = input() stk = [-1] #sentinel loop = [] #loop times of the previous loop i = 0 while i<len(s): if s[i]=='L': #loop start stk.append(-1) # delimiter loop.append(int(s[i+1])) #loop times i += 2 elif s[i]=='T': t = int(s[i+1:i+3]) i += 3 if stk[-1]==-1: #first of the loop stk.append([t,0,t]) # (head, cost, tail) else: #merge with previous track h,c,e = stk[-1] stk[-1] = [h,c+abs(e-t),t] else: # End of loop i += 1 w = loop.pop() h,c,e = stk.pop() c = c*w + abs(h-e)*(w-1) stk.pop() #pop delimiter (-1) if stk[-1]==-1: #no previous stk.append([h,c,e]) else: #merge with previous (head,cost,end) stk[-1][1] += c + abs(stk[-1][2]-h) stk[-1][2] = e print(stk[-1][1]+abs(10-stk[-1][0])) ```