Master theorem是用來快速估計一個能寫成recurrence relation的演算法的running time,就是一個被證明的公式:
舉例來說,之前的naiive multiplication algorithm的recurrence relations如下,用master theorem分析之:
改良過的karatsuba乘法,將subproblem減少成只有三個,所以a = 3,用master theorem分析如下:
大約是O(n^1.58),這不是常數倍數的改良,是指數的改良,相當的好。
如果有人能把乘法演算法改成只有2個subproblem呢?事實上目前沒發現過,不過這個recurrence relations就是merge sort的特徵:
所以目前sorting最好的running time只能是O(nlogn)。
這個是什麼?沒錯,就是Binary search:
code
2016年11月22日 星期二
2016年11月21日 星期一
Algorithms筆記12 - Binary search using D&C strategy
Binary Search
Binary search是一個divide and conquer algorithm:binary search將一個problem分成兩個等分的subproblem,由於input是sorted array,所以每次都能刪除一個不可能得到答案的subproblem,等於每次problem size變成一半:
最後的combine所有subproblem的答案時,那些被捨棄掉的subproblem是空集合,所以最後一個subproblem中找到(或確定沒找到)的index就是整個母問題的解。
Worst-case Running time analysis
binary search最差的結果就是找不到這個數的index,如果寫成recurrence relation(recursive equations) 的話:最差都沒找到的話,總共做了 log_2(n) + 1次的midpoint check,以n = 4來說,總共找了n = 4, n = 2, n = 1,總共3次的midpoint check。所以這是theta(log(n))。
Algorithms筆記11 - Divide and Conquer
分而擊之
聽起來很酷,這種策略有以下幾個特徵:1. 分成許多一樣問題但是input size較小者:
一樣的subproblem很重要,跟greedy algorithm一樣,都必須是同類型的,這樣能解決小的,就能解決大的,例如把一個矩形面積拆成好幾個矩形來算:
但是不能拆成其他形狀,因為這就不是subproblem:
2. 各個擊破後,要把答案拼成原本大問題的答案,這是一個很大的特徵,所以除了切成不同size的subproblem之外,這些subproblems必須要剛好能組合成母問題答案,所以以下的分法是錯的,因為subproblems重疊了:
3. 根據1. 的特性,自然會形成recursive algorithms。
一個D&C linear search例子
這個例子其實沒有什麼divide and conquer 的好處,因為每個subproblem只是原本的input size - 1。這邊是用來解釋如何估算running time:
每次recursion level都做了O(1)的事情,最差會總共做了n次,所以是O(n),或更精確地說是theta(n)。
2016年11月18日 星期五
Algorithm筆記 10.5 - greedy演算法練習: 將一數拆成找出最多相異數字
如果你也在修Coursera Algorithm Toolbox的話,這是解答!請斟酌使用
問題定義
如何將某數n拆成最多個相異正整數?例如6可以拆成{1,2,3},10可以拆成{1,2,3,4}。看起來可以提出一個safe move的假設,“永遠從目前最小的未選過的正整數開始”。
證明safe move
假設一數n,其optimal solution為拆成m個數 n = k1 + k2 + k3 + ... + km,且a < k1 < k2 < ... < km。令 x = k1 - a。我們很輕易可以改寫成以下:
n = (k1-x) + k2 + ... + (km+x),也是一個optimal solution,因為k1-x = a < k2 < ... <km < km+x,都是相異自然數,故此策略為safe move
實作
選取最小正整數a有一個條件,就是n' = n-a >= a+1 = a',這樣在下一個subproblem中最小能選的數a'才能小於等於n',才有解。舉例來說,8要怎麼拆成最多個正整數相加?按照我們演算法:
8-1 = 7, (7 >= 2)
7-2 = 5, (5 >= 3)
5-5 = 0, (分配完畢)
private static List<Integer> optimalSummands(int n) { List<Integer> summands = new ArrayList<Integer>(); //find the min int min = 1; while(n != 0) { int a = n - min; if (a == 0 || a >= min+1) { summands.add(min); n = a; } min = min + 1; } //write your code here return summands; }
running time為O(n)。
2016年11月15日 星期二
Algorithm筆記 10.4 - greedy演算法練習: 最少停留點問題
如果你也在修Coursera Algorithm Toolbox的話,這是解答!請斟酌使用
問題定義
有一棟建築物住了n個人,每個人在家的時間都不一樣(可能有重疊),如何能最少次抵達此建築物而能拜訪所有人?這個問題可以map成數線上的線段和點,假設有3個人,在家的時段分別為(1點~3點), (2點~5點), (3點~6點),則在數線上表示
0 [1 [2 [3] 4 5] 6] 7 8 9 10 ....
由上圖來推論,我們可以假設safe move為“從最左邊的區間終點a找起(上圖為3),包含a的區間都能用a來表示可以拜訪的時段”。
證明safe move
假設某optimal solution中,最左邊的區間終點y屬於線段(x,y),且y不屬於任何線段的代表點:1. 某線段(w,z)終點z >= y,且此線段包含y,可以用y表示,則optimal solution會減少1。
_ _ (x _ _ _ y) _ _ z) _
2. 某線段終點z <=y,不成立,因為y是最左邊的區間終點。
3. 某線段(w,z) 終點z > y,且w > y,y不能代表(w,z),不改變optimal solutoin。
以上三種情況包含了任意線段與y所有可能的關係,故根據1. 選擇y為代表點是符合optimal solution,此策略為safe move。
演算法
private static int[] optimalPoints(Segment[] segments) { ArrayList<Integer> result = new ArrayList<>(); //O(nlogn) sort segments ascendingly based on end point Arrays.sort(segments, new Comparator<Segment>() { @Override public int compare(Segment o1, Segment o2) { return o1.end - o2.end; } }); //O(n) int leftmostEnd = segments[0].end; for(int i = 0; i < segments.length; i++) { if (segments[i].contains(leftmostEnd)) continue; else { result.add(leftmostEnd); leftmostEnd = segments[i].end; } } //add the last match result.add(leftmostEnd); int[] r = new int[result.size()]; for (int i = 0; i < r.length; i++) r[i] = result.get(i); return r; }
Algorithm筆記 10.2 - greedy演算法練習: 最大化dot-product
如果你也在修Coursera Algorithm Toolbox的話,這是解答!請斟酌使用
問題定義
給兩個整數vector v1 = {a1,a2, ... , an} , v2 = {b1, b2, ... , bn},找出最大的dot product?按照greedy algorithm步驟,我們要先找一個safe move,並且證明其為某個optimal solution的第一步。依照題目,我們假設“v1和v2從大到小排序後所得的dot product為optimal”。
證明safe move
假設一optimal solution w = {a1*b1 + a2*b2 +.. + aj*bj + .... + ak*bk + .... + an*bn},且ak = max(a1, a2,..,an), bj = max(b1, b2, ...,bn)。如果w' = 把w中的 aj*bj 和 ak*bk 交換成 ak*bj和 aj*bk,則w' - w = ak*bj+aj*bk - aj*bj - ak*bk = ak*(bj-bk) + aj*(bk-bj) = (ak-aj)*(bj-bk) > 0,因為ak 和 bj分別是兩個數列中的最大值。
演算法
注意要integer相乘可能會oveflow,先cast成為long
private static long maxDotProduct(int[] a, int[] b) { Integer[] A = new Integer[a.length]; Integer[] B = new Integer[a.length]; //O(n) for (int i = 0; i < a.length; i++) { A[i] = a[i]; B[i] = b[i]; } //O(nlogn) Arrays.sort(A, new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o2 - o1; } }); //O(nlogn) Arrays.sort(B, new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o2 - o1; } }); //O(n) long result = 0; for (int i = 0; i < a.length; i++) { result += (long)A[i] * (long)B[i]; } return result; }
2016年11月14日 星期一
Algorithm筆記 10.3 - greedy演算法練習: 注意眉角
如果你也在修Coursera Algorithm Toolbox的話,這是解答!請斟酌使用
public class FractionalKnapsack { private static double getOptimalValue(int capacity, int[] values, int[] weights) { double value = 0; class Item { public int value; public int weight; public double valuesPerWeight; public Item(int value, int weight) { this.value = value; this.weight = weight; this.valuesPerWeight = (double)value / (double)weight; } } //O(n) initalization Item[] items = new Item[values.length]; for (int i = 0; i < items.length; i++) { items[i] = new Item(values[i], weights[i]); } //O(nlogn) sort Arrays.sort(items, new Comparator<Item>() { @Override public int compare(Item o1, Item o2) { if (o2.valuesPerWeight - o1.valuesPerWeight > 0) return 1; else if (o2.valuesPerWeight - o1.valuesPerWeight < 0) return -1; else return 0; } }); for(int i = 0; i < items.length; i++) { double load = Math.min((double)capacity, (double)items[i].weight); if (load >= items[i].weight) { //use all items[i] value += items[i].value; capacity -= load; } else { //use fracation of items[i] value += load * items[i].valuesPerWeight; return value; } } return value; } public static void stressTest() { while (true) { int n = ThreadLocalRandom.current().nextInt(1, 1000); int capacity = ThreadLocalRandom.current().nextInt(0, 2000000); int[] values = new int[n]; int[] weights = new int[n]; for (int i = 0; i < n; i++) { values[i] = ThreadLocalRandom.current().nextInt(0, 2000000); weights[i] = ThreadLocalRandom.current().nextInt(0, 2000000); } System.out.println(getOptimalValue(capacity, values, weights)); } } public static void main(String args[]) { Scanner scanner = new Scanner(System.in); int n = scanner.nextInt(); int capacity = scanner.nextInt(); int[] values = new int[n]; int[] weights = new int[n]; for (int i = 0; i < n; i++) { values[i] = scanner.nextInt(); weights[i] = scanner.nextInt(); } System.out.println(getOptimalValue(capacity, values, weights)); //stressTest(); }
Algorithm筆記 10.1 - greedy演算法練習: 找零問題
如果你也在修Coursera Algorithm Toolbox的話,這是解答!請斟酌使用
問題定義
給一個金額n,怎麼找出最少的零錢幣數目?限定幣值為{1, 5, 10}。按照greedy algorithm步驟,我們要先找一個safe move,並且證明其為某個optimal solution的第一步。依照題目,我們假設“盡量先找幣值大的零錢是一個safe move”。
證明safe move
假設某個optimal solution = w的硬幣中有x個一元硬幣,y個5元硬幣,z個10元硬幣,如果x >= 5,我們可以拿出5個一元硬幣換成1個5元硬幣,所以optimal solution會變成w - 5 + 1 = w - 4,同理把2個y硬幣換成1個z硬幣的話,w值也會變少,得證。演算法
private static int getChange(int m) { //write your code here int[] denominations = {10, 5, 1}; int count = 0; for (int i = 0; i < denominations.length; i++) { int coins = m / denominations[i]; count += coins; m = m - coins*denominations[i]; if (m == 0) return count; } return count; }
這是O(n),因為我們人工去排序了幣值了。
Algorithm筆記 9 - Knapsack演算法 (maximization problem)
問題定義
假設有個set = {(x1,y1), (x2,y2), ... (xn, yn)},我們想要取某個subset使得其中的元素的 y值相加的和最大,前提是x值相加的和不能超過W。x值可以只取部分,例如x1/2, x2/3, ....
以裝袋為例
一個袋子最多只能裝7個units的物體,我們有三個物體分別是(4 unit, $20), (3 unit, $18), (7 unit, $14),試問要怎麼最大化裝入袋子的錢?
這個knapsack問題已經被證明可以用greedy algorithm找出最佳解:
其safe move就是“優先使用單位額度($/unit)最高者”。
證明Safe move
我們還是要走過一遍證明過程,才能對以上的lemma產生信心對吧?假設我們有一個optimal solution,裡面至少裝入某些非最高單位額度的物體X。
如果我們把袋中的X取出一單位,放入最高單位額度的物體Y的一單位,則此袋子的總體額度增加了,所以原本的optimal solution並不是optimal solution,且必定至少包含一單位的Y。
我們如果對剩下的物體們的單位一一抽換成Y,此袋子的總體額度就會繼續增加,直到所有袋子中的物體被替換完成,或是Y被完全使用為止。若是後者,則次高單位額度的物體Z也遵從以上的邏輯,每次替換成Z都會增加總體額度。
得證。
演算法pseudocode和running time
上面的implementation是O(n^2),因為外層有一個for n loop,內層要找max單位額度的時候,又要看過所有的Wi,所以是O(n),這個很明顯有改善空間。
如果先把 vi/wi算出來並且排序(最好是O(nlogn)),就可以免去loop body中的linear search,所以knapsack algorithm最後會是O(nlogn) + O(n) = O(nlogn)。
2016年11月11日 星期五
Algorithm筆記 8 - 小孩分群演算法
分群問題
如何將一群n個小孩根據某條件p來分成最少的群呢?最直接(笨)的方法就是把所有的可能分法都窮舉出來,亦即找出一個set的所有subset,當然空集合是一個合法的subset。
最小限度來說,如果要把n個物體分到相異2群中 (某群可以完全是空),至少就要花上2^n次方次的對條件p的篩選,因為對每個小孩來說都有2種選法,所以總共是2^n個選法,也就是running time O(2^n)。
然而分成相異兩群的所有方法只是窮舉所有分群法中的一個subset,所以O(2^n)是窮舉法的下限running time,也就是OMEGA2(^n)。
這是一個exponential (變數n在指數) running time algorithm,執行時間隨n變化將極為陡峭劇烈,我們必須尋求更好asymptotic running time的方法。
Greedy approach
解決問題通常將之轉成數學model,這有助找出特性與轉化成簡單問題的方式。假設這個條件p為 每群中的小孩年齡相差不能超過一歲
一個數學上的模型是轉成數線,數線上的點(值) 代表每個小孩的年齡:
同群中任何兩人不能相差超過一歲的限制,就用一個長度為1的線段來表示,所以每個線段涵蓋的點,就是一個滿足條件p的同一群人,以下是其中一種分法 (分成四群):
如果一個點同時被兩個線段涵蓋,那並不構成model上的任何意義,因為此點只會屬於其中一群。
當然也可以這樣分 (更好一些,分成三群):
能不能有一個greedy algorithm?
首先我們必須要找出一個safe move,也就是某個optimal solution的第一步,假設以下敘述是一個safe move:
長度為1的線段最左緣,如果重合了最左邊的點(年紀最小者),則這是一個safe move。記得我們必須證明safe move成立,否則不能使用greedy approach,因為不見得是optimal solution。如何證明上述描述是一個safe move?
greedy algorithm
如果我們把任意一個optimal solution的第一條線段 (group 1)的左緣,拉到與數線上最左邊的點重和的話,原本在線段1涵蓋的點的總數不會改變,所以group 1的人數也不會改變:上圖中第3個點雖然同時被涵蓋在第一條紅線和第二條藍線中,但是並沒有什麼影響,它仍然屬於group 2的點。 由於紅線位置的改變並不影響optimal solution最後的output,所以這個移動方式是一個optimal solution,所以是一個safe move。
有了safe move,根據greedy algorithm的實作方式,我們把剃除掉第一群的點 (上圖紅線中的前兩個點),剩下的所有點就構成了一樣的分群問題,就是一個subproblem。
所以這是一個greedy algorithm可以找到optimal solution的問題。演算法如下:
首先所有的小孩的年紀被map到一個sorted array{x1, x2, ... , xn},R是最後的分法集合,指標i從第一個點x1開始檢查。
先列出先把這個線段的涵蓋範圍l和r設定為x1和x1+1,+1的意思是線段長度。
然後inner while loop找出下一個xi,使得此xi > r,亦即找出下一個不在此線段涵蓋範圍內的點,當作下一個subproblem的最左邊的點。
pseudo-code大概就這樣,來分析它的running time。
Polynomial time
這個演算法從頭到尾走了n次,所以是O(n),不過這是建立在我們先把x1..xn排序過的前提下,如果沒有排序的話,必須要進行排序,則目前已知最好的排序演算法的running time為O(nlogn)。所以整體running time = O(nlogn) + O(n) = O(nlogn)。
相對於OMEGA(2^n)的 naiive algorithm的改善相當的大!
2016年11月9日 星期三
Algorithm筆記 7 - 最少加油次數演算法分析
承筆記6,我們嘗試來寫此問題的解決演算法。
這個演算法接受三個參數:
x是一個array,包含從出發點A, 終點B, 和所有加油站的位置:
n是個加油站的array index
L是加滿油可以跑得最大距離
所以演算法寫起來相當簡單:
不過這是一個O(n^2)的演算法,因為有nested loop,而內外loop各是跑最多n次。
藉由不同implementation的方式,我們可以將之變成O(n)的演算法:
以上的演算法沒驗證過,但大概就是這樣。
這個演算法接受三個參數:
x是一個array,包含從出發點A, 終點B, 和所有加油站的位置:
n是個加油站的array index
L是加滿油可以跑得最大距離
所以演算法寫起來相當簡單:
不過這是一個O(n^2)的演算法,因為有nested loop,而內外loop各是跑最多n次。
藉由不同implementation的方式,我們可以將之變成O(n)的演算法:
public static int minRefills(int[] positions, int secondLastGasStationIndex, int farestDistanceCanGo) { int numRefills = 0; int currentPosition = 0; int lastRefill = currentPosition; while (currentPosition <= secondLastGasStationIndex) { if (positions[currentPosition + 1] - positions[lastRefill] <= farestDistanceCanGo ) currentPosition += 1; else { lastRefill = currentPosition; numRefills += 1; } } return numRefills; }
以上的演算法沒驗證過,但大概就是這樣。
這邊的教訓是implementation optimization是很有影響(廢話一句)。
2016年11月8日 星期二
Algorithm筆記 6 - Greedy Algorithm
一般演算法課程都會先介紹greedy系列,因為這跟人類的天性比較相近(短視近利?!),但這個策略事實上在某些應用中的確是很有效的解題方式。
Greedy strategy牽涉到三個步驟:
1. 如何做一個符合(假設存在)optimal solution的貪婪決定?(稱為safe move)
2. 如何把大問題劃成許多相同的較小問題 (稱為subproblem)
3. 如何持續不斷做1和2
每次加滿油可以跑400 km,怎麼加油才會最少次呢?
1. 如果以greedy的含義來看的話,應該要往加最少次的方向去思考,則合理的一個greedy choice嘗試就是只在能跑得最遠能到達的加油站加油。
2. 假設採用1的greedy strategy在某個加油站G加油,則剩下的路程就變成一個subproblem,因為是一模一樣的問題,只是出發點不是A而是G。
3. 持續不斷把剩下的路程做1. 和 2.的步驟。
假設從A 到 B有最少次加油的optimal solution解決方案,而其中第一次選的加油站為G1, 而G為離A能到達的最遠的加油站。
第一種可能G和G1重合,則得證。
剩下一種可能必然是G1離A比較近,因為我們已經定義G是離A最遠能到達的加油站。
假設我們在G1加油之後,下一個在optimal solution中要加油的是G2。
此時又有三種可能:
1. G在G1和G2中間:
此時如果要在G2加油的話,我們可以不選G1為第一個加油站,而選G為第一個加油站,因為在G1加滿油可以抵達G2的話,代表在G加滿油一定可以抵達G2,因為G1到G2的路程比G到G2來得遠。
所以如果用G代替G1當作第一個加油站的話,也是一個optimal solution,因為此solution的總加油次數不變。得證。
2. 第二種可能是G2在G1和G中間:
但這是不可能的,因為如果真是這樣,那我們完全可以選G2為第一個加油站就好,這樣整體加油次數會比原本的optimal solution少一次,這違反假設,不成立。
另外我們甚至可以選G為第一個加油站,因為G2到下一個加油站G3的距離大於G到G3的距離,所以在G加滿油一定可以抵達G3,整體加油次數也比optimal solution少2,違反假設,不成立。
3. G2和G重和,一樣違反假設,不成立,原因與2.理由一樣。
所以我們證明了G一定是optimal solution的第一個加油站
Greedy strategy牽涉到三個步驟:
1. 如何做一個符合(假設存在)optimal solution的貪婪決定?(稱為safe move)
2. 如何把大問題劃成許多相同的較小問題 (稱為subproblem)
3. 如何持續不斷做1和2
怎樣加最少次的油?
以car-fueling problem為例,假設有從A到B路上有以下幾個加油站:每次加滿油可以跑400 km,怎麼加油才會最少次呢?
1. 如果以greedy的含義來看的話,應該要往加最少次的方向去思考,則合理的一個greedy choice嘗試就是只在能跑得最遠能到達的加油站加油。
2. 假設採用1的greedy strategy在某個加油站G加油,則剩下的路程就變成一個subproblem,因為是一模一樣的問題,只是出發點不是A而是G。
3. 持續不斷把剩下的路程做1. 和 2.的步驟。
Safe Move需要證明!
我們要證明在每次能到達最遠距離的加油站加油是一個safe move。假設從A 到 B有最少次加油的optimal solution解決方案,而其中第一次選的加油站為G1, 而G為離A能到達的最遠的加油站。
第一種可能G和G1重合,則得證。
剩下一種可能必然是G1離A比較近,因為我們已經定義G是離A最遠能到達的加油站。
假設我們在G1加油之後,下一個在optimal solution中要加油的是G2。
此時又有三種可能:
1. G在G1和G2中間:
此時如果要在G2加油的話,我們可以不選G1為第一個加油站,而選G為第一個加油站,因為在G1加滿油可以抵達G2的話,代表在G加滿油一定可以抵達G2,因為G1到G2的路程比G到G2來得遠。
所以如果用G代替G1當作第一個加油站的話,也是一個optimal solution,因為此solution的總加油次數不變。得證。
2. 第二種可能是G2在G1和G中間:
但這是不可能的,因為如果真是這樣,那我們完全可以選G2為第一個加油站就好,這樣整體加油次數會比原本的optimal solution少一次,這違反假設,不成立。
另外我們甚至可以選G為第一個加油站,因為G2到下一個加油站G3的距離大於G到G3的距離,所以在G加滿油一定可以抵達G3,整體加油次數也比optimal solution少2,違反假設,不成立。
3. G2和G重和,一樣違反假設,不成立,原因與2.理由一樣。
所以我們證明了G一定是optimal solution的第一個加油站
2016年11月7日 星期一
Algorithm筆記 5 - 一些解題的思考方式
剛做完Algorithmic Toolbox這門課的第一個assignment,還真的被跟Pisano period相關的某一題難住了,有點出乎意料之外,趕快趁熱把一些經驗談寫下來,希望之後用得到:
(以下的題目,Java grader要求在1.5秒內要pass所有的test)
一樣用簡單的dynamic programming來計算Fib(n):
a = GCD(a,b) * x
b = GCD(a,b) * y
LCM(a,b) = GCD(a,b) * x * y = (a * b) / GCD(a,b)
所以asymptotic runtime應該和GCD(a,b)一樣為 O(log(a*b))
這題其實就是用了上面幾題帶來的解題觀念!綜合應用之~
要能跟getFibonacciSumLastDigit扯上關係,必須要證明
的確是可以,以下:
所以我們可以簡單地用上一題的output來實作本題:
(以下的題目,Java grader要求在1.5秒內要pass所有的test)
計算小的Fibonacci Number
第一題沒啥特別,就是要讓我們知道不要根據題目的數學定義直接轉換成code,因為這可能是最沒效率的事,見我筆記2。這邊可以用簡單的dynamic programming來解決問題,由於grader只要求能在時間內完成Fib(45)的數就可以,所以沒什麼問題。找出較大的Fibonacci數中的個位數字 (最大到Fib(10^7))
這一題其實就要引入一個觀念:轉化與簡化題目。由於計算fibonacci數字是O(n^2) runtime,所以我們不可能直接去計算Fib(10^7),然後將之除以10來找個位數字。但是因為我們只關心個位數字,所以只要計算每個Fib(n)的個位數字就好,十位數字以上的我們不關心,這樣就可以避免大數字的計算,但實際asymtotic time我也不確定,我猜是O(n)。一樣用簡單的dynamic programming來計算Fib(n):
private static int calc_fib_mod10(int n) { int[] F = new int[n+2]; F[0] = 0; F[1] = 1; for (int i = 2; i <= n; i++) F[i] = (F[i-2] + F[i-1])%10; return F[n]; }
計算兩數最大公因數GCD (a,b 兩數分別最大到2*10^9)
這題也只是讓你練習筆記2中講的概念:先利用數學關係把題目簡化過一遍,這樣大大省略計算時間。O(log(a*b))計算兩數最小公倍數LCM (a, b 兩數分別最大到2*10^9)
最小公倍數可以簡化成 a*b/GCD(a,b),因為a = GCD(a,b) * x
b = GCD(a,b) * y
LCM(a,b) = GCD(a,b) * x * y = (a * b) / GCD(a,b)
所以asymptotic runtime應該和GCD(a,b)一樣為 O(log(a*b))
計算大Fibonacci數 Fib(n) 除以m的餘數 (1≤𝑛≤1018,2≤𝑚≤105)
這一題就是我卡關的題目,主要是沒意識到要把計算限縮在餘數就好,因為這個input 已經太大了,沒把計算限縮在餘數的話,即便利用題目中提示的Pisano period簡化計算,也絕對跑不動。
這題其實就是用了上面幾題帶來的解題觀念!綜合應用之~
/*1 <= n <= 10^18 < Long.MAX2 <= m <= 10^5 (a + b) % m === (a % m + b % m) % m */ private static long getFibonacciHugePisano(long n, long m) { if (n <= 1) return n; long beforePrevious = 0; long previous = 1; long a = -1; long current = -1; for (long i = 2; i <= n; i++) { current = (beforePrevious + previous) % m; if (a == 0 && current == 1) { long period = (i + 1) - 2; long reductionN = n % period; return getFibonacciHugePisano(reductionN, m); } else { a = current; beforePrevious = previous; previous = current; } } return current; }
計算Fibonacci數字連加的和的個位數字 (0 ≤ 𝑛 ≤ 1014)
這應該有兩種做法,第一種(我沒嘗試的)就是把計算限縮在個位數來算Fib(0) + Fib(1) + ... + Fib(n)的和,但我懷疑是否能通過grader的1.5秒時間限制?這感覺上是O(n^2)。
第二種就是找看這個數列有無規則?還真的有,原來SumFib(n)的個位數隨著n變大,每60次會循環一次,所以只要找出這60個數字,之後用查表法就超快:
這題的啟示就是,執行觀察各種input的結果,是否有可能有規律性,一但有規律性就非常容易解決問題。
上一題的Pisano period無法有一個modulo m 的 P(m)的函數,只能在loop裡面找重複的時機,當然就沒有這一題執行快了。不考慮 n % Period的話,O(1)。
第二種就是找看這個數列有無規則?還真的有,原來SumFib(n)的個位數隨著n變大,每60次會循環一次,所以只要找出這60個數字,之後用查表法就超快:
private static final int PERIOD = 60; private static int[] LastDigitOfFibSums = new int[] { 0, 1, 2, 4, 7, 2, 0, 3, 4, 8, 3, 2, 6, 9, 6, 6, 3, 0, 4, 5, 0, 6, 7, 4, 2, 7, 0, 8, 9, 8, 8, 7, 6, 4, 1, 6, 8, 5, 4, 0, 5, 6, 2, 9, 2, 2, 5, 8, 4, 3, 8, 2, 1, 4, 6, 1, 8, 0, 9, 0};
private static int getFibonacciSumLastDigit(long n) { return LastDigitOfFibSums[(int)(n % PERIOD)]; }
這題的啟示就是,執行觀察各種input的結果,是否有可能有規律性,一但有規律性就非常容易解決問題。
上一題的Pisano period無法有一個modulo m 的 P(m)的函數,只能在loop裡面找重複的時機,當然就沒有這一題執行快了。不考慮 n % Period的話,O(1)。
找出Fibonacci數列中部分和 𝐹𝑚 + 𝐹𝑚+1 + · · · + 𝐹𝑛 的個位數字 (0 ≤ 𝑚 ≤ 𝑛 ≤ 1018)
這一題乍看之下,跟上一題很像,但可惜其output沒有規律性。不過跟上一題還是有關係,因為它可以寫成上一題的式子:(F(3) + F(4) + F(5) + F(6) + F(7)) % 10
= (F0 + F1 + F2 + .. + F7 - (F0 + F1 + F2)) % 10
= (SumFib(7) - SumFib(2)) % 10
要能跟getFibonacciSumLastDigit扯上關係,必須要證明
SumFib(7)%10 - SumFib(2)%10 == getFibonacciSumLastDigit(7) - getFibonacciSumLastDigit(2)
的確是可以,以下:
if x > y,
(x - y)% 10 == (x%10 - y%10), if x%10 >= y%10
(x - y)% 10 == x%10 - y%10 + 10, if x%10 < y%10
所以我們可以簡單地用上一題的output來實作本題:
private static long getFibonacciPartialSumLastDigit(long from, long to) { int x = getFibonacciSumLastDigit(to); int y = getFibonacciSumLastDigit(from-1); if (x >= y) return x - y; else return x - y +10; }
結論
如果一個問題能以數學來model最好,因為通常有規律性或是簡化轉化問題的機會!2016年11月5日 星期六
Algorithms筆記 3 - Big-O
Actual Runtime
我們無法預測一個program的實際執行時間,因為那牽涉到了執行程式的軟體環境和硬體架構等的所有細節條件,所以我們想要知道的是,我們的program是不是scalable? 想知道當input size變大的時候,我們的執行時間會怎麼變化?不過可以確定,通常不同電腦執行程式的速度差異只會是一個常數級的差別,(不過有時候常數很大...例如一般PC 和超級電腦的差別)。
Asymptotic Runtime
一個program的asymptotic runime在n非常大的時候,會有天差地遠的差別(遠超過常數級別的差異):Asymptotic runtime就是在看一個演算法的runtime成長趨勢如何。
Big-O Notation
f is Big-O of g的意思是f這個runtime 函數被g bound在下,所以g至少是c*f。正確定義如下:如果f <= c * g,而h和g有相同最高次項,我們通常會把O(g)簡化成O(h),因為h和g有相同的asymptotic runtime(只有常數倍數的差異)。
注意事項!
Big-O只有在比較演算法的asymptotic runtime時有效,但是如果實際input n size並不大,那不見得要選asymptotic runtime小的演算法,因為可能過了某個input size k之後才有runtime優勢,而常用的input size可能遠小於k。另一個要注意的是,兩個演算法即便都是同樣的O(g),可是常數倍數差異可能很大(即便只差兩倍也是很大),這時後當然要選常數項越小者,因為實際跑起來就是有倍數的差距。
Fibonacci asymptotic analysis
(1)
這是O(n),因為每個memory cell可能要被初始化成0,而每個初始化的動作應該是個固定常數runtime,所以是O(1),做了n次,就是O(n)
(2)
簡單的assignment動作一定都是constant runtime,所以是O(1)
(3)
loop部分牽涉到每個iteration要做i <= n以及 i++的operation,所以是n*O(1) = O(n)。
loop body乍看應該要是O(1),但是這邊分析成O(n)是因為F[i]的數其實是跟著n長大的,fibonacci number有可能會大到一個machine word不能表達,所以addition operation所花的時間會隨著n增加,所以分析成O(n),這是比較特別的地方
(4)
這沒有疑問是O(1)
所以這個Euclidean Fibonnaci asymptotical runtime =
我之前以為loop body是O(1),所以以為整體應該是O(n),看來錯了,所以naive recursion版本的fibonacci會比O(n^2)更慢!
訂閱:
文章 (Atom)

































