Posted on

Prefix Sums – GenomicRangeQuery解題紀錄

題目內容

題目頁面: https://app.codility.com/programmers/lessons/5-prefix_sums/genomic_range_query/

題目理解

天啊~我最討厭有故事性的題目了如汽車或DNA,好長阿~~~讓我認真看他在說啥!

DNA和factor可表示為: {‘A’:1,’C’:2,’G’:3,’T’:4},S = CAGCCTA,P=[2,5,0],Q=[4,5,6]

然後要找第 K 個查詢(0 ≤ K < M),找到 P[K] 和 Q[K](含)之間的 DNA 序列中包含的核苷酸的最小影響因子。也就是要計算出一個長度為M(M為P、Q的長度)的陣列,然後返回最小影響因子

例如M[0] = Math.min(S[2],S[3]) = Math.min(‘G’,’C’) = Math.min(2,3) =2

陷阱在這: each element of arrays P and Q is an integer within the range [0..N – 1]; => 所以不一定一樣長

解題思路

先用暴力法寫出一個可行解

// you can write to stdout for debugging purposes, e.g.
// console.log('this is a debug message');

function solution(S, P, Q) {
    // Implement your solution here
    factor = {'A':1,'C':2,'G':3,'T':4}
    var length_M = Math.min(P.length, Q.length)
    var M = []
    for( var i = 0 ; i < length_M; i++){
        var inclusive_S = S.slice(P[i], Q[i]+1).split("")
        var minimum = Infinity
        for(var j =0;j<inclusive_S.length;j++){
            //console.log(minimum,  factor[inclusive_S[j]])
            if(minimum > factor[inclusive_S[j]]){
                minimum = factor[inclusive_S[j]]
            }
        }
        M.push(minimum)
    }
    return M
}

因為邏輯有一點複雜,先把暴力解送出驗證一下正確性,之後再來優化

好的!!62%!!至少邏輯正確,那我們在想想要如何優化。讓我先觀察一下是死在那些效能測試上。分別是GGGGGG..??..GGGGGG..??..GGGGGG、large random string, length、all max ranges,首先先看到almost_all_same_letters,所以先把CAGC對應數字的部分做掉,如: S = CAGCCTA = [2,1,3,2,2,4,1]

下面為改善後的程式碼,可以減少重複將字串轉換為數字的部分。雖然沒有很樂觀,因為還是有先暫存中間值的可能性,先送看看有沒有改善,並評估一下效能瓶頸是否有改變。

function solution(S, P, Q) {
    // Implement your solution here
    factor = {'A':1,'C':2,'G':3,'T':4}
    var length_M = Math.min(P.length, Q.length)
    var S = S.split("").map(x => factor[x])
    var M = []
    for( var i = 0 ; i < length_M; i++){
        M.push(Math.min(...S.slice(P[i], Q[i]+1)))
    }
    return M
}

結果並沒有改善

— M = 0
minimum: Infinity
minimum: 3
minimum: 2
— M = 1
minimum: Infinity
— M = 2
minimum: Infinity
minimum: 2
minimum: 1
minimum: 1
minimum: 1
minimum: 1
minimum: 1

觀察一下現在的迴圈狀況,來找尋可改善的重複步驟,首先最明顯的,就是若遇到1應該可直接break,但我猜這影響不大。另外我發現,因為factor = {‘A’:1,’C’:2,’G’:3,’T’:4}是固定的,且最多就是4種,所以與其在那邊一個個轉換並且比較大小,還不如用4個if…else來從小尋找到最大的值。

function solution(S, P, Q) {
    // Implement your solution here
    //factor = {'A':1,'C':2,'G':3,'T':4}
    var length_M = Math.min(P.length, Q.length)
    var M = []
    for( var i = 0 ; i < length_M; i++){
        var inclusive_S = S.slice(P[i], Q[i]+1).split("")
        //console.log(inclusive_S)
        if(inclusive_S.indexOf('A') >= 0) M.push(1)
        else if(inclusive_S.indexOf('C') >= 0) M.push(2)
        else if(inclusive_S.indexOf('G') >= 0) M.push(3)
        else M.push(4)
    }
    return M
}

仍然沒有好轉

事已至此,感覺卡在一個切入觀點了,所以只好上網搜尋一下: http://myjavawar.blogspot.com/2018/10/genomicrangequery.html

原來要改變一下思考方向,要把原本的思維從”在一個已被切好的陣列中,搜尋某字元有沒有出現過”的思維,改成”計算從0到M以來某字元出現的次數”。接著若是陣列要從N開始,就直接把M-N,就會是從N~M之間某字元出現的次數”了

function solution(S, P, Q) {
    // Implement your solution here
    //factor = {'A':1,'C':2,'G':3,'T':4}
    var length_M = Math.min(P.length, Q.length)
    var M = []
    var showTimes = []
    var currentShowTime =  {'A':0,'C':0,'G':0,'T':0}
    for( var i = 0 ; i < S.length; i++){
        currentShowTime[S[i]]++
        showTimes.push({'A':currentShowTime['A'],'C':currentShowTime['C'],'G':currentShowTime['G'],'T':currentShowTime['T']})
    }
    for( var i = 0 ; i < length_M; i++){
        //var inclusive_S = S.slice(P[i], Q[i]+1).split("")
        //console.log(inclusive_S)
        if(showTimes[Q[i]]['A'] - showTimes[P[i]]['A'] > 0) M.push(1)
        else if(showTimes[Q[i]]['C'] - showTimes[P[i]]['C'] > 0) M.push(2)
        else if(showTimes[Q[i]]['G'] - showTimes[P[i]]['G'] > 0) M.push(3)
        else M.push(4)
    }
    return M
}

結果雖然效率過了,但是會有錯誤的結果,檢查了一下,這是因為當P[i]與Q[i]一樣大小時,就會出現錯誤,所以要加一個if用別的方法找答案

function solution(S, P, Q) {
    // Implement your solution here
    //factor = {'A':1,'C':2,'G':3,'T':4}
    var length_M = Math.min(P.length, Q.length)
    var M = []
    var showTimes = []
    var currentShowTime =  {'A':0,'C':0,'G':0,'T':0}
    for( var i = 0 ; i < S.length; i++){
        currentShowTime[S[i]]++
        showTimes.push({'A':currentShowTime['A'],'C':currentShowTime['C'],'G':currentShowTime['G'],'T':currentShowTime['T']})
    }
    //console.log(showTimes)
    for( var i = 0 ; i < length_M; i++){
        //var inclusive_S = S.slice(P[i], Q[i]+1).split("")
        //console.log(inclusive_S)
        if(P[i] == Q[i]){
            var factor = {'A':1,'C':2,'G':3,'T':4}
            M.push(factor[S[P[i]]])
        }else{
            if(showTimes[Q[i]]['A'] - showTimes[P[i]]['A'] > 0) M.push(1)
            else if(showTimes[Q[i]]['C'] - showTimes[P[i]]['C'] > 0) M.push(2)
            else if(showTimes[Q[i]]['G'] - showTimes[P[i]]['G'] > 0) M.push(3)
            else if(showTimes[Q[i]]['T'] - showTimes[P[i]]['T'] > 0) M.push(4)
        }
        
    }
    return M
}

結果還是有錯

仔細檢查了一下,錯誤的情況是solution(‘AC’, [0, 0, 1], [0, 1, 1]),發現P[i] 應該要減1,但這樣子P[i]==0時會出現問題,所以要給個預設值

function solution(S, P, Q) {
    // Implement your solution here
    //factor = {'A':1,'C':2,'G':3,'T':4}
    var length_M = Math.min(P.length, Q.length)
    var M = []
    var showTimes = []
    var currentShowTime =  {'A':0,'C':0,'G':0,'T':0}
    for( var i = 0 ; i < S.length; i++){
        currentShowTime[S[i]]++
        showTimes.push({'A':currentShowTime['A'],'C':currentShowTime['C'],'G':currentShowTime['G'],'T':currentShowTime['T']})
    }
    //console.log(showTimes)
    for( var i = 0 ; i < length_M; i++){
        var inclusive_S = S.slice(P[i], Q[i]+1).split("")
        //console.log(inclusive_S)
        if(P[i] == Q[i]){
            var factor = {'A':1,'C':2,'G':3,'T':4}
            M.push(factor[S[P[i]]])
        }else{
            var showTimesMax = showTimes[Q[i]]
            var showTimesMin = P[i] == 0 ? {'A':0,'C':0,'G':0,'T':0}:showTimes[P[i]-1]
            //console.log(showTimesMax, showTimesMin)
            if(showTimesMax['A'] - showTimesMin['A'] > 0) M.push(1)
            else if(showTimesMax['C'] - showTimesMin['C'] > 0) M.push(2)
            else if(showTimesMax['G'] - showTimesMin['G'] > 0) M.push(3)
            else M.push(4)
        }
    }
    return M
}

好了~終於100%了,真是個身心俱疲的一題阿! (https://app.codility.com/demo/results/trainingYXRWFN-H4J/)

Posted on

Prefix Sums – CountDiv解題紀錄

題目內容

題目頁面: https://app.codility.com/programmers/lessons/5-prefix_sums/count_div/

解題思路

給定三個整數 A、B 和 K,返回 [A..B] 範圍內可被 K 整除的整數個數,且A 和 B 是 [ 0 .. 2,000,000,000 ]範圍內的整數; K 是 [ 1 .. 2,000,000,000 ]範圍內的整數。

嗯…看起來這是一題用效能卡死人的題目,因為最大值可到2,000,000,000,但是為了求正確性,我們還是先從簡單的方法寫起,再來慢慢改善效能,不要一下想太難的方法。先把暴力破解法寫出來

function solution(A, B, K) {
    // Implement your solution here
    var counter = 0;
    for(var i = A;i<=B;i++){
        if(i%K == 0) counter++
    }
    return counter
}

但是有了上次的經驗,我根本不想送出這個答案被打槍。所以先來想改善方法,首先,我想找到比A大,最小能夠被整除的數字,和比B小最大能夠被整除的數字然後相減後直接除以K

function solution(A, B, K) {
    var minimum_divisible = K*Math.ceil(A/K)
    var maximum_divisible = K*Math.floor(B/K)
    
    return (maximum_divisible - minimum_divisible)/K + 1
}

結果!!甚麼!!!中等程度這麼快解決!!太棒了!!!
https://app.codility.com/demo/results/trainingCYGC6K-3H8/

Posted on

Prefix Sums(前綴和)概念

甚麼是Prefix Sums(前綴和)

當需要快速查詢數組中某一區間的元素和時,Prefix Sums可以幫助我們在O(1)的時間複雜度內進行查詢。

具體做法是先對數組進行預處理,計算出從第一個元素到當前位置的所有元素的和,然後通過兩個元素的前綴和之差來計算出任意區間的元素和。例如,要查詢數組中第i到第j個元素的和,只需要計算sums[j+1] – sums[i]即可。

Prefix Sums通常用於需要頻繁查詢數組中某一區間的元素和的情況,例如在數學、統計學、計算機科學和機器學習等領域中。Prefix Sums的計算可以在預處理的階段中完成,因此可以在之後的查詢中以O(1)的時間複雜度快速地回答各種區間求和問題。

例如,在計算機科學中,Prefix Sums通常用於解決數組中的區間查詢問題,例如求解區間最大子段和、區間平均值、區間中位數等。Prefix Sums還可以用於在數據壓縮、圖像處理和信號處理等領域中,進行離散化、濾波和卷積等操作。

計算出從第一個元素到當前位置的所有元素的和,是因為這樣可以在之後的區間求和操作中,通過兩個元素的前綴和之差來快速計算任意區間的元素和。例如,要查詢數組中第i到第j個元素的和,只需要計算sums[j+1] – sums[i]即可。這樣可以大大降低區間求和操作的時間複雜度,提高算法效率。

Prefix Sums使用時機


在許多需要進行區間求和操作的問題中,Prefix Sums都是一種常用的解決方案。因此,在識別哪些問題需要使用Prefix Sums時,可以考慮以下幾點:

  1. 問題是否需要進行區間求和操作:如果問題需要計算數組中某一區間的元素和,那麼Prefix Sums可能是一個合適的解決方案。
  2. 數據集的大小:Prefix Sums通常適用於數據集較小的情況下,因為在預處理階段中需要計算每個元素的前綴和,這可能需要額外的空間和時間。
  3. 數據是否可以修改:如果數據集可以修改,那麼在每次修改後,需要重新計算前綴和,這可能會影響算法的效率。因此,在這種情況下,需要仔細考慮使用Prefix Sums的適當性。
  4. 計算區間求和的頻率:如果需要頻繁地進行區間求和操作,那麼使用Prefix Sums可能比其他方法更有效。

綜合以上幾點,可以初步判斷哪些問題可能需要使用Prefix Sums。在實際應用中,還需要進一步評估和優化算法,以確保其效率和準確性。

使用Binary Indexed Tree來計算前綴和

BIT的基本思想是將原始數組轉換為一個二進制表示的數組,並在其中儲存一些特定位置的前綴和。BIT的建立和查詢操作都可以在O(n log n)的時間複雜度內完成,其中n是數組的長度。BIT的更新操作可以在O(log n)的時間複雜度內完成。

更多詳細介紹: https://yuihuang.com/binary-indexed-tree/

範例題目 – PassingCars

題目連結

https://app.codility.com/programmers/lessons/5-prefix_sums/passing_cars/

我的第一次解法(javascript)

function solution(A) {
    // Implement your solution here
    var current_sum = 0
    for(var i=0;i<A.length;i++){
        if(A[i] == 0){
            for(var j = i;j<A.length;j++){
                if(A[j] == 1){
                    current_sum++
                }
            }
        }
    }
    return current_sum
}

成績如下,雖然答案是正確的,但是效率不夠,為O(N**2),只拿到70%的分數

改善方向

我發現if(A[j] == 1)和if(A[i] == 0)這段其實重複跑了,其實可能在之前的檢查中早就會知道有那些是1哪些是0,重複遍歷是不必要的,因此我決定嘗試用另外的陣列來儲存index為1的index值(value_1_index)和index為0的index(value_0_index)

接下來因為 0 ≤ P < Q < N,所以我們會需要找value_1_index裡面的key值大於value_0_index的,所以會使用var j = value_1_index.findIndex(x=>x>value_0_index[i])來找尋。

function solution(A) {
    // Implement your solution here
    var current_sum = 0
    var value_1_index = [], value_0_index = []
    for(var i=0;i<A.length;i++){
        if(A[i] == 0) value_0_index.push(i)
        else value_1_index.push(i)
    }
    for(var i=0;i<value_0_index.length;i++){
        var j = value_1_index.findIndex(x=>x>value_0_index[i])
        if(j >= 0){
            while(j < value_1_index.length){
                j++
                current_sum++
            }
        }
    }
    return current_sum
}

結果也太哭了,居然沒省多少XD只多了一個勾勾

Detected time complexity:O(N ** 2)

接著我發現這句話!!

The function should return −1 if the number of pairs of passing cars exceeds 1,000,000,000.

因此我決定加上if(current_sum > 1000000000){ return -1 }的判斷在current_sum ++的後面,但結果仍然一樣,但是可以發現至少程式有跑完(不是顯示Killed),只是速度達不到標準,尤其是large_random和large_alternate差異很大,我的程式跑了5.336秒,要求卻是0.304秒,差了10倍以上(汗顏)。仔細觀察了一下,我個人覺得在large_alternate的題目代表是0和1頻繁交替的狀況下,我的演算法的效能會很差。

因此我猜測這個效能瓶頸和var j = value_1_index.findIndex(x=>x>value_0_index[i])這個搜尋的動作有關,我決定改成當i和j用完就把用完的部分砍掉,以減少搜尋量。為此,我必須把for迴圈改成while,並且讓value_0_index與value_1_index都能隨著時間變小。

function solution(A) {
    // Implement your solution here
    var current_sum = 0
    var value_1_index = [], value_0_index = []
    for(var i=0;i<A.length;i++){
        if(A[i] == 0) value_0_index.push(i)
        else value_1_index.push(i)
    }
    while(value_0_index.length > 0){
        var j = value_1_index.findIndex(x=>x>value_0_index[0])
        if(j >= 0){
            value_1_index = value_1_index.slice(j,value_1_index.length)
            var pointer = 0
            while(pointer < value_1_index.length){
                pointer++
                current_sum++
                if(current_sum > 1000000000){
                    return -1
                }
            }
        }
        value_0_index.shift();
    }
    return current_sum
}

所以問題是出在value_1_index = value_1_index.slice(j,value_1_index.length)這邊太耗時,尤其是最後一項,可以看出是在當某個數字多的時候會更耗時。在這邊我嘗試把slice改成splice,並且在j>0時才去做

function solution(A) {
    // Implement your solution here
    var current_sum = 0
    var value_1_index = [], value_0_index = []
    for(var i=0;i<A.length;i++){
        if(A[i] == 0) value_0_index.push(i)
        else value_1_index.push(i)
    }
    while(value_0_index.length > 0){
        var j = value_1_index.findIndex(x=>x>value_0_index[0])
        if(j >= 0){
            if(j > 0){
                value_1_index.splice(0,j)
            }
            var pointer = 0
            while(pointer < value_1_index.length){
                pointer++
                current_sum++
                if(current_sum > 1000000000){
                    return -1
                }
            }
        }
        value_0_index.shift();
    }
    return current_sum
}

結果有改善,但仍需努力

接著我發現,等等…value_1_index好像不用刪啊!!先決定不要先搜尋好了,把條件設定成從最後一個value_1_index去找起,找到value_1_index裡的值小於value_0_index[0]時停止

function solution(A) {
    // Implement your solution here
    var current_sum = 0
    var value_1_index = [], value_0_index = []
    for(var i=0;i<A.length;i++){
        if(A[i] == 0) value_0_index.push(i)
        else value_1_index.push(i)
    }
    while(value_0_index.length > 0){
        var point = value_1_index.length-1
        while(value_1_index[point] > value_0_index[0]){
            current_sum++
            point--
        }
        value_0_index.shift();
    }
    return (current_sum > 1000000000) ? -1 : current_sum 
}

結果仍然超時,我發現似乎value_0_index也可以不用刪,改回使用for迴圈看看

function solution(A) {
    // Implement your solution here
    var current_sum = 0
    var value_1_index = [], value_0_index = []
    for(var i=0;i<A.length;i++){
        if(A[i] == 0) value_0_index.push(i)
        else value_1_index.push(i)
    }
    for(var i=0;i<value_0_index.length;i++){
        var pointer = value_1_index.length-1
        while(value_1_index[pointer] > value_0_index[i]){
            current_sum++
            pointer  --
        }
    }
    return (current_sum > 1000000000) ? -1 : current_sum 
}

繞了這麼久,我覺得最大的問題應該是在 (0, 1), (0, 3), (0, 4), (2, 3), (2, 4)中,  (2, 3), (2, 4)中的3,4其實是(0, 1), (0, 3), (0, 4)中的1,3,4的子集合,所以應該是要做暫存,把第一次的集合儲存起來,之後直接減掉A[0] = 0 A[1] = 1 A[2] = 0之中兩個0中間的1這個值然後加上去就可以了

function solution(A) {
    // Implement your solution here
    var current_sum = 0
    var value_1_index = [], value_0_index = []
    //紀錄兩個0中間經過的1
    var value_0_interval = []
    for(var i=0;i<A.length;i++){
        if(A[i] == 0) {
            value_0_index.push(i)
            value_0_interval[value_0_index.length-1] = 0
        }else {
            value_1_index.push(i)
            value_0_interval[value_0_index.length-1]++
        }
    }
    var current_value_1 = value_1_index.length
    for(var i=0;i<value_0_index.length;i++){
        current_sum = current_sum + current_value_1
        current_value_1 = current_value_1 - value_0_interval[i]
    }
    return (current_sum > 1000000000) ? -1 : current_sum 
}

好…90%,快過了,一開始就要除去第一個value_0_index之前的value_1_index

function solution(A) {
    // Implement your solution here
    var current_sum = 0
    var value_1_index = [], value_0_index = []
    //紀錄兩個0中間經過的1
    var value_0_interval = []
    for(var i=A.indexOf(0);i<A.length;i++){
        if(A[i] == 0) {
            value_0_index.push(i)
            value_0_interval[value_0_index.length-1] = 0
        }else {
            value_1_index.push(i)
            value_0_interval[value_0_index.length-1]++
        }
    }
    var current_value_1 = value_1_index.length
    for(var i=0;i<value_0_index.length;i++){
        current_sum = current_sum + current_value_1
        current_value_1 = current_value_1 - value_0_interval[i]
    }
    return (current_sum > 1000000000) ? -1 : current_sum 
}

終於100%了….https://app.codility.com/demo/results/trainingWK5QAP-PP8/

結論,一個easy程度的我玩這麼多次,真的很哭…但滿好玩的(有嗎?)

Posted on

Counting Elements

概念介紹

Counting Elements是一種計算數組中元素出現次數的概念。它通常用於解決一些與計數有關的問題,例如找出數組中出現次數超過一定閾值的元素,計算數組中不同元素的個數等。

在Counting Elements中,我們可以使用一個額外的數組counts,將數組中每個元素出現的次數記錄下來。counts[i]表示數組中元素i出現的次數,如果數組中不存在元素i,則counts[i]=0。

具體來說,Counting Elements算法的步驟如下:

  1. 創建一個長度為k的counts數組,其中k是數組中最大元素的值加1。
  2. 遍歷原始數組A,對於每個A[i],增加counts[A[i]]的值。
  3. 計算任意元素的出現次數,只需要查詢counts數組中對應元素的值即可。

Counting Elements算法的時間複雜度為O(n),其中n是數組的長度。它可以在線性時間內計算數組中元素的出現次數,因此非常適用於需要頻繁計算元素次數的問題。另外,Counting Elements算法還可以與其他算法結合使用,例如快速排序和二分查找等,以進一步優化計算效率。

使用時機


在許多與計數有關的問題中,Counting Elements是一種常見的解決方案。因此,在考慮使用Counting Elements時,可以考慮以下幾點:

  1. 問題是否需要計算元素出現的次數:如果問題需要計算數組中每個元素出現的次數,那麼Counting Elements可能是一個合適的解決方案。
  2. 數據集的大小:Counting Elements通常適用於數據集較小的情況下,因為需要額外的空間來存儲counts數組,並且在預處理階段中需要遍歷數組,這可能需要額外的時間和空間。
  3. 需要找出出現次數超過一定閾值的元素:如果需要找出數組中出現次數超過一定閾值的元素,例如Majority Element,那麼Counting Elements可能是一個合適的解決方案。
  4. 需要計算數組中不同元素的個數:如果需要計算數組中不同元素的個數,那麼Counting Elements也可能是一個合適的解決方案。

綜合以上幾點,可以初步判斷哪些問題可能需要使用Counting Elements。在實際應用中,還需要進一步評估和優化算法,以確保其效率和準確性。

練習題目1

題目連結: FrogRiverOne – VIEW

我的解法

function solution(X, A) {
    // Implement your solution here
    var filled = new Array(X).fill(false);
    var current_filled = 0;
    for (let index = 0; index < A.length; index++) {
        //console.log(filled)
        if(!filled[A[index]-1]){
            filled[A[index]-1] = true
            current_filled ++;
            if(current_filled == X){
                return index
            }
        }
    }
    
    return -1
}

測試通過,為100%!

練習題目2

題目連結: PermCheckVIEW

我的作答

function solution(A) {
    // Implement your solution here
    var arr = new Array(A.length).fill(0)
    for(var i=0;i<A.length;i++){
        arr[A[i]-1] = 1
    }
    return (arr.indexOf(0) == -1) ? 1 :0
}

練習題目3- MaxCounters

題目連結: MaxCountersVIEW

我的解法

function solution(N, A) {
    // Implement your solution here
    var arr = new Array(N).fill(0)
    var max_value = 0
    for(var i=0;i<A.length;i++){
        if(A[i]<=N){
            arr[A[i]-1]++
            if(max_value < arr[A[i]-1]){
                max_value = arr[A[i]-1]
            }
        }else{
            arr.fill(max_value)
        }
    }
    return arr
}

結果正確,但時間超時,只得到66的分數,代表需要再修改做法

我檢查了一下寫法,發現arr.fill(max_value)這行雖然只有一行,但是會是O(N)的複雜度,會增加許多的時間複雜度,因此,我嘗試最後再一次性的更新這個值,並且在每一步做加總時,檢查這個值是否需要被更新,就可以避免每次當A[i]>N時都得增加一次O(N)的時間複雜度。

https://app.codility.com/demo/results/training556V6C-HBV/

function solution(N, A) {
    // Implement your solution here
    var arr = new Array(N).fill(0)
    var max_value = 0
    var minimum_value = 0
    for(var i=0;i<A.length;i++){
        if(A[i]<=N){
            if(arr[A[i]-1] < minimum_value) arr[A[i]-1] = minimum_value
            arr[A[i]-1]++
            if(max_value < arr[A[i]-1]){
                max_value = arr[A[i]-1]
            }
        }else{
            //arr.fill(max_value)
            minimum_value = max_value
        }
    }
    
    for(var i=0;i<arr.length;i++){
        arr[i] = Math.max(arr[i], minimum_value)
    }
    return arr
}

練習題目4- MissingInteger

題目連結: MissingInteger: VIEW

這題的意思是說要找到A這個陣列裡面缺少的最小的正整數,所以我的解題思路就是先尋找陣列裡面的最大值,接下來設定一個陣列裡面預設值全部設為0,然後遍歷整個陣列把有出現過的index設定為1,接著再回傳該陣列最小值為1的index

function solution(A) {
    var max = Math.max(...A)
    if (max < 0){
        return 1
    }
    var sort = new Array(max).fill(0)
    // Implement your solution here
    for(var i=0;i<A.length;i++){
        if(sort[A[i]-1] == 0 && A[i] > 0){
            sort[A[i]-1] = 1
        }
    }
    for(var i=0;i<max;i++){
        if(sort[i-1] == 0){
            return i
        }
    }
    return max+1
}

結果如下,正確率為100%,複雜度為O(N)或O(N*log(N))

Posted on

時間複雜度BigO介紹

什麼是BigO

時間複雜度是指算法所需的運行時間與輸入數據規模之間的關係。在計算機科學中,我們使用 BigO 表示法來表示算法的時間複雜度。

BigO 表示法中的 O 代表 “on the order of”,即算法的運行時間隨著輸入規模的增長,按照某種數量級的速度增長。常見的時間複雜度從低到高分別是 O(1)、O(log n)、O(n)、O(n log n)、O(n^2)、O(2^n) 和 O(n!)。

在實際應用中,我們通常希望選擇時間複雜度盡可能低的算法。然而,不同的算法在不同的應用場景下可能表現不同,因此選擇合適的算法需要根據具體問題進行綜合考慮。

另外需要注意的是,時間複雜度只是對算法的一個粗略估計,實際運行時間還受到很多其他因素的影響,如輸入數據的特性、計算機硬件性能等。因此,在實際應用中需要進行充分的測試和優化,以獲得更準確的運行時間估計。

降低常數

在 BigO 表示法中,常數因子通常被省略,因為它們對算法的增長速度影響相對較小。在特定輸入下O(N)可能會比O(1)更小,一般來說,BIG O只是在描述增加的速率。

降低非優勢條件

在算法的時間複雜度分析中,我們通常會關注最壞情況下的時間複雜度,即算法在處理最壞情況下的輸入時所需的時間複雜度。但是,在實際應用中,算法所處理的輸入可能並不總是最壞情況,因此可能存在一些非優勢條件,即輸入規模較小、輸入具有特殊性質等情況。

刷題範例

題目連結: https://app.codility.com/programmers/lessons/3-time_complexity/tape_equilibrium/

我的解答 – https://app.codility.com/demo/results/training5HCTQQ-UW6/

function solution(A) {
    // Implement your solution here
    var total_a = 0;
    var total_b = A.reduce((partialSum, a) => partialSum + a, 0);   
    var min = Infinity;
    for(var i=0;i<A.length-1;i++){
        total_a = total_a+A[i];
        total_b = total_b-A[i];
        //console.log(total_a, total_b,Math.abs(total_a-total_b), i)
        if(Math.abs(total_a-total_b) < min){
            min = Math.abs(total_a-total_b)
        }
    }
    return min
}

結果如下

Posted on

[LeetCode] Maximum Value of K Coins From Piles

Maximum Value of K Coins From Piles

這是遞迴方式的寫法, 但我可能存太多垃圾資訊,當k值變大之後,會發生heap allocation錯誤的問題,也受限於遞迴的限制當k變大效率非常差…..
看來又要動態規劃(哭), 晚點再來補上改善版

/**
* @param {number[][]} piles
* @param {number} k
* @return {number}
*/
var maxValueOfCoins = function (piles, k) {
let rootNode = new node(piles.length, k);
let maxResult = addAllChildrenForThisNode(rootNode, piles, 0);
console.log(rootNode.toObject())
return maxResult
};
function addAllChildrenForThisNode(node, piles, maxResult) {
for (let i = 0; i &lt; node.pilesPointer.length; i++) {
if (node.pilesPointer[i] + 1 &lt; piles[i].length) {
let y = node.pilesPointer[i] + 1;
let newNode = node.addChild(piles[i][y], i, y);
console.log(newNode.deep)
if(newNode.deep &lt; newNode.maxSelectNum){
maxResult = addAllChildrenForThisNode(newNode, piles, maxResult)
}else{
maxResult = Math.max(newNode.currentValue, maxResult)
}
}
}
return maxResult
}
function node(pilesNum, maxSelectNum) {
this.currentValue = 0;
//樹的鏈結資料
this.deep = 0;
this.parent = undefined;
this.children = [];
//x代表在哪個piles, y代表在該piles的深度
this.indexX = undefined;
this.indexY = undefined;
//用來儲存這個node位置所有的piles的y座標
this.pilesPointer = new Array(pilesNum).fill(-1);
this.maxSelectNum = maxSelectNum;
this.addChild = function (num, indexX, indexY) {
let child = new node(this.pilesPointer.length, this.maxSelectNum);
child.deep = this.deep + 1;
child.parent = this;
child.currentValue = this.currentValue + num;
child.indexX = indexX;
child.indexY = indexY;
child.pilesPointer = this.pilesPointer.slice(0);
child.pilesPointer[child.indexX] = indexY;
this.children.push(child);
return child
}
this.toObject = function () {
let children = [];
for (let child of this.children) {
children.push(child.toObject())
}
return {deep:this.deep, value: this.currentValue , children: children}
}
}

console.log(maxValueOfCoins([[80,62,78,78,40,59,98,35],[79,19,100,15],[79,2,27,73,12,13,11,37,27,55,54,55,87,10,97,26,78,20,75,23,46,94,56,32,14,70,70,37,60,46,1,53]],5))
Posted on

[LeetCode] Coin Change 2

Coin Change2

這也是一題動態規劃的題目,但是比起上一題,上一題每一個暫時儲存的東西是有意義的,我們可以了解說每一個的暫時狀態是某個數字用所給的硬幣列表裡,能用最少數量達成答案的數目,但是這一題的中間狀態所儲存的真的就是『中間的狀態』,我是看著別人的答案去回推理論的,真的很難想像在遇到這樣的問題時,該用什麼角度去把複雜的問題拆分,並找尋出重複步驟並找到可暫存狀態來計算出最終結果。

這是最後的結果

var change = function(amount, coins) {
let dp = new Array(amount+1).fill(0);
dp[0]=1;
for(let coin of coins){
for(let i=coin;i<=amount;i++){ dp[i]+=dp[i-coin]; } } return dp[amount] }; [/code] 大概解釋一下解題的思緒,首先就是最重要的,先設定一個暫存動態編程的序列

dp

,這裡面的值只是儲存一個暫時的狀態,讓我們在計算下一個值時可以拿前一個運算到一半的值來繼續運算,長度為amount+1(這樣才存的到

dp[amount]

)。

接下來很重要的是把

dp[0]=1

, 這是所有計算的起點, 代表說在一個分解狀態下,若自己可以被自己整除,那就可以有一種解法

若題目是coins=[2,5,8], amount=16時:

我們可以先觀察在

coins:[2], amount:16

時,
這樣的半狀態會得到:

dp = [1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1]

這個的意思是,若是coins只有2時,可以用2組成0, 2, 4, 6, 8, 10, 12, 14, 16,皆只有一種組法。

接下來我們再把5加進去,但是因為以coins=[2,5]來說,我們可以把任務拆細,若是要組成16,我們要避免掉重覆的狀態,也就是說
我們可以思考,因為5,5,2,2,2和2,2,5,5,2其實是相同的答案,以2,2,2,5,5來說,其實是單純用2coin*3個=6和單純用5coin*2個=10,
那麼就可以開始計算,用5和2共同組成的狀況下,五的分佈是如何呢?

amount為16,則5有可能有分成:1個5(這時會需要單純用2組成11)和2個5(這時會需要用2組成6)和3個5(這時會需要用2組成1)三種可能性,這時參照上面coins=[2]的dp,會知道用2組成11的狀況和用2組成1狀況是0個,代表只會增加2個5的狀況。

但是動態編程當然沒這麼容易思考(真哭),因為這邊的dp存的是一個暫時半狀態的對照表,會需要算出從5~16所有可用[2,5]組成的,需要較完整的對照表。

首先很重要的,為什麼要從五開始,因為一定要大於五才有可能可以用五組成結果,並且如同dp[0]一樣,dp[5]因為coins有5,一定會多一種5的組合法。
從上面的推論我們可以觀查到要知道有沒有機會使用5這個coins來組成結果,主要要觀察『要組成總數』減掉現有coin的面額的數字,能不能用2組成

所以若要知道6能不能用[5,2]組成,就要查coins:[2]所暫存的表的

dp[6(要組數字)-5(現有硬幣)]=dp[1](能不能用2組成1) = 0

,可得知是0種可能,以此類推。
這邊非常重要的是一定要從小到大,因為才可以讓狀態累加,我們先得知5可以組成5,把dp[5]=1(本來只有coins:2時是0),而在算10時,才能得到dp[10-5]=1(得知可以使用5來組成10),外加單純用2組成10的可能性(當coins=[2]時dp[10]為1),可知10這個數字可單純用5組成也可用[2,5]組成,所以共有2種可能性,然後把dp[10]更新為2

接著在算dp[15]時才能夠利用dp[15-5]=dp[10]=2,再加上當coins=[2]時的dp[15]=0,得知dp[15]為2

以下為coins=[2,5]時的dp對照表

dp= [ 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 2, 2]

接下來就是再相同的步驟再把coin 8加進coins列表裡。以同樣的步驟和概念,就可以得到

dp=[1, 0, 1, 0, 1, 1, 1, 1, 2, 1, 3, 1, 3, 2, 3, 3, 4]

最後推得用[2,5,8要組成16有四種可能性]

Posted on

[LeetCode] Coin Change

https://leetcode.com/problems/coin-change

最近開始刷leetcode覺得思考這些邏輯問題還滿好玩的
第一個刷的是這個,他是一個動態規劃的題目

一般我們在思考的時候會去思考以錢幣為基準,例如1,3,5的錢幣要怎麼湊成11元,我們會拿1,3,5去隨機湊硬幣,但是當數值大了以後,要計算最小的錢幣組合就會變成很困難.

像這種極限求值就會需要將思考轉換過來, 我們可以一步步的拆解, 先了解1,3,5,若要組成1是用幾個(1個1),那組成2要用幾個(2個1),若要組成3要用幾個,到3時我們就會發現,3可以用”3個1″或”1個3″組成, 這時候就可以來比較誰用的硬幣比較少,然後把最小可組成的數字”1″存到可組成結果為3的陣列。
接下來在思考4的時候,我們就可以思考4減掉硬幣1,也就是3,的最小使用硬幣數量,那就代表我們使用3,再加上硬幣1,可以組成4。接下來算要組成6要幾個硬幣,6減掉硬幣3,可以拿到硬幣3要用幾個硬幣,所以就是硬幣3使用的硬幣數量+1。
var coinChange = function (coins, amount) {
if (amount == 0) return 0;
let amountRecord = []
amountRecord[0] = 0
lastExistResult = 0
minCoin = Math.min(…coins);
//紀錄這些硬幣能夠組合成的數值所使用的硬幣數, 並用這些數值來推算目標所需的硬幣
for (var currentAmount = 1; currentAmount <= amount; currentAmount++) { amountRecord[currentAmount] = Infinity if (currentAmount + minCoin >= lastExistResult) {
for (var coin of coins) {
if (coin <= currentAmount) {
//尋找這一個數字可不可以用之前可組成的硬幣加上其中一個新的硬幣來滿足(以確認是最小的硬幣數目)
amountRecord[currentAmount] = Math.min(amountRecord[currentAmount], amountRecord[currentAmount – coin] + 1);
}
}
}
if (amountRecord[currentAmount] != Infinity) {
lastExistResult = amountRecord[currentAmount];
}
}
console.log(amountRecord)
return amountRecord[amount] == Infinity ? -1 : amountRecord[amount]
};
這個JS的效能只贏了32.59%的人

覺得不太開心(?)
所以我把console.log拿掉,再把let amountRecord = []; amountRecord[0] = 0;改成一行let amountRecord = [0](是有沒有這麼無聊)

接著我突發奇想,想說若是這個數字可用硬幣湊齊, 下個數字如果小於最小的coins數字的話,可以省略(如硬幣是10,15,18, 但是要組成11,很明顯就不可能會有結果,因為11-10小於10)

PS: 但是測試後其實拿掉console.log影響最大XD
var coinChange = function (coins, amount) {
if (amount == 0) return 0;
let amountRecord = [0]
lastExistResult = 0
minCoin = Math.min(…coins);
//紀錄這些硬幣能夠組合成的數值所使用的硬幣數, 並用這些數值來推算目標所需的硬幣
for (var currentAmount = 1; currentAmount <= amount; currentAmount++) { amountRecord[currentAmount] = Infinity if (currentAmount + minCoin >= lastExistResult) {
for (var coin of coins) {
if (coin <= currentAmount) {
//尋找這一個數字可不可以用之前可組成的硬幣加上其中一個新的硬幣來滿足(以確認是最小的硬幣數目)
amountRecord[currentAmount] = Math.min(amountRecord[currentAmount], amountRecord[currentAmount – coin] + 1);
}
}
if (amountRecord[currentAmount] != Infinity) {
lastExistResult = amountRecord[currentAmount];
}
}
}
return amountRecord[amount] == Infinity ? -1 : amountRecord[amount]
};

結果至少擊敗了72.28%的人,累了…先這樣吧XDD