code

顯示具有 Algorithms 標籤的文章。 顯示所有文章
顯示具有 Algorithms 標籤的文章。 顯示所有文章

2016年11月22日 星期二

Algorithms筆記14 - Master Theorem

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:



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) 的話:


對problem size = n的problem來說,判斷midpoint是否為key所花的時間是一個常數c,如果midpoint不是key,則我們可以拿掉n/2的input,所以剩下的時間就是剩下的n/2 size 的subproblem所需要的worst case running time。



最差都沒找到的話,總共做了 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-3 = 2, (2 < 4)
5-4 = 1, (1 < 5)
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的話,這是解答!請斟酌使用


本來這題功課沒什麼好po的,但是有個implementation眉角要注意,所以寫出來警惕一下。下面粉色標出的區塊就是Java comparator的實作,但是由於compare要回傳int,所以不能直接把o2 - o1的值cast成int回傳,這樣可能會變成零,如果兩者的差<1.0的話。


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)的演算法:

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


怎樣加最少次的油?

以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)

計算小的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個數字,之後用查表法就超快:

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日 星期六

Algorithm筆記 4 - 常用的lograithm equation


另外定義如下:

Recall that logan is the power to which you need to raise a in order to obtain n.

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)更慢!