發佈日期:

Sorting – Triangle解題紀錄

題目內容

題目頁面: https://app.codility.com/programmers/lessons/6-sorting/triangle/

題意解析


除了任兩邊的長度必須大於第三邊之外,組成三角形的線段還有一些其他的限制:

  1. 三條邊的長度必須為正數。
  2. 任兩邊的長度之差必須小於第三邊的長度,也就是說,最長的邊的長度必須小於其他兩邊的長度之和。
  3. 任何一邊的長度都不能超過其他兩邊長度之和的兩倍。

如果以上任何一個條件不滿足,則無法組成三角形。

0 ≤ P < Q < R < N

解題過程

先寫一個一定會被打槍的暴力解,複雜度為O(N^3)

function solution(A) {
    for (let i = 0; i < A.length; i++) {
        if (A[i] > 0) {
            for (let j = i + 1; j < A.length; j++) {
                if (A[j] > 0) {
                    for (let k = j + 1; k < A.length; k++) {
                        if (A[k] > 0) {
                            if (A[i] + A[j] > A[k] &&
                                A[j] + A[k] > A[i] &&
                                A[k] + A[i] > A[j]) {
                                return 1;
                            }
                        }
                    }
                }
            }
        }
        return 0;
    }
}

結果0%,,沒一個過的了,不是答案錯誤就是runtime error

直覺猜測,這一定和三角形的定義有關,查了一下谷歌大神,找到這段

當一個陣列已經排好序時,如果能夠構成三角形,那麼任意三個相鄰的元素都能構成三角形。

假設排序後的陣列為arr,元素個數為n。我們使用一個for循環遍歷arr中的元素arr[i]。由於arr已經排好序,因此arr[i]一定是arr[i]、arr[i+1]和arr[i+2]中最小的一個,arr[i+1]是中間的一個,arr[i+2]是最大的一個。如果這三個元素能夠構成三角形,那麼其他任意三個相鄰的元素也能構成三角形。

如果arr[i]+arr[i+1]>arr[i+2],那麼一定存在一組三個相鄰的元素可以構成三角形。因此,在檢查完arr[i]、arr[i+1]和arr[i+2]之後,我們可以繼續檢查arr[i+1]、arr[i+2]和arr[i+3],arr[i+2]、arr[i+3]和arr[i+4],以此類推,直到遍歷完整個陣列。

總之,當一個陣列已經排好序時,只需要檢查相鄰的三個元素是否能夠構成三角形,即可判斷是否存在三個元素能夠構成三角形。這比遍歷所有可能的三元組要更高效。

所以就是要知道這個定理就可以了

function solution(A) {
    A.sort((a, b) => a - b);

    for (let i = 0; i < A.length - 2; i++) {
        if (A[i] + A[i + 1] > A[i + 2]) {
            return 1;
        }
    }

    return 0;
}
發佈日期:

Sorting – MaxProductOfThree解題紀錄

題目內容

題目頁面: https://app.codility.com/programmers/lessons/6-sorting/max_product_of_three/

題目理解

總之就是取得陣列A裡面,由N個數字所挑選隨意三個相乘的最大值,這些數字可能是-1000~1000,然後陣列長度最少3個最多10萬個

解題紀錄

首先我想到,形成最大值的三個值,當該值為正數時,只有可能由3個正數或2個負數*1個正數組成。當該值為負數時,則有可能由3個負數或2個正數*1個負數組成。所以,現在所需的就是先遍歷整個陣列,然後確認正數和負數的數量。當有可能有正數乘積時,則以正數乘積為優先,否則則需要考慮負數的乘積。

因此,我決定先遍歷A陣列,找尋最大的前三個正數數字以及最小的前三個負數數字(當正值時使用),以及最小的前三個正數數字以及最大的負數數字(當負值時使用),然後統計正值的數量和負值的數量,以決定最後輸出的會是正值還是負值

function solution(A) {
    // Implement your solution here
    var max_positive = new triplet(true)
    var min_positive = new triplet(false)
    var positive_count = 0
    var max_negative = new triplet(true)
    var min_negative = new triplet(false)
    var negative_count = 0
    var has_zero = false
    for (var i = 0; i < A.length; i++) {
        if (A[i] == 0) has_zero = true
        else if (A[i] > 0) {
            max_positive.compareMax(A[i])
            min_positive.compareMin(A[i])
            positive_count++
        } else {
            max_negative.compareMax(A[i])
            min_negative.compareMin(A[i])
            negative_count++
        }
    }
    if (positive_count >= 3 || (negative_count >= 2 && positive_count > 0)) {
        var pos_3 = 0
        var pos_2_neg_1 = 0
        if (positive_count >= 3) {
            pos_3 = max_positive.max * max_positive.middle * max_positive.min
        }
        if (negative_count >= 2 && positive_count > 0) {
            pos_2_neg_1 = min_negative.middle * min_negative.min * max_positive.max
        }
        return Math.max(pos_3, pos_2_neg_1)
    } else if (has_zero) {
        return 0
    } else {
        //確認該使用2個正數*1個負數還是3個負數
        var max_2_pos_1_neg = -Infinity
        var max_3_neg = -Infinity
        if (negative_count > 0 && positive_count >= 2) {
            max_2_pos_1_neg = min_positive.min * min_positive.middle * max_negative.max
        }
        if (negative_count >= 3) {
            max_3_neg = max_negative.max * max_negative.middle * max_negative.min
        }
        return Math.max(max_2_pos_1_neg, max_3_neg)
    }
}
class triplet {
    constructor(isMaxCompare) {
        var data = isMaxCompare ? -Infinity : Infinity
        this.max = data;
        this.middle = data;
        this.min = data;
    }
    getValue() {
        console.log(this.max, this.middle, this.min)
    }
    compareMax(value) {
        if (value > this.max) {
            this.min = this.middle
            this.middle = this.max
            this.max = value
        } else if (value > this.middle) {
            this.min = this.middle
            this.middle = value
        } else if (value > this.min) {
            this.min = value
        }
    }
    compareMin(value) {
        if (value < this.min) {
            this.max = this.middle
            this.middle = this.min
            this.min = value
        } else if (value < this.middle) {
            this.max = this.middle
            this.middle = value
        } else if (value < this.max) {
            this.max = value
        }
    }
}

100%!!!一次成功~感動(https://app.codility.com/demo/results/trainingR9PVFF-P78/)

發佈日期:

Sorting – Distinct解題紀錄

題目內容

題目頁面: https://app.codility.com/programmers/lessons/6-sorting/distinct/

題目解析

這一題之前其實出現過,但是因為這一題A的範圍是負一百萬到正一百萬,實在太大了,沒辦法直接用查表的方式儲存,所以會需要使用到二分搜尋法去建立這個不重複陣列

我的解題記錄

第一次的解法

function solution(A) {
    var M = []
    for (var i = 0; i < A.length; i++) {
        var idx = binarySearch(M, A[i]);
        if(M[idx] != A[i]){
            M.splice(idx, 0, A[i])
        }
    }
    //console.log(M)
    return M.length
}

//在一個已排序的陣列裡,取得newValue應插入的index
function binarySearch(arr, target) {
    let left = 0;
    let right = arr.length - 1;
    while (left <= right) {
        let mid = Math.floor((left + right) / 2);
        if (arr[mid] === target) {
            return mid;
        } else if (arr[mid] < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    return left
}

對於這結果,我真是滿腹委屈,明明二分搜尋法就是O(logN)阿!到底是誰把他變成O(N^2)的…我覺得應該是原本遍歷陣列為N,又乘上搜尋的LogN,因此變得很耗時

經過了一陣思考後,我覺得我根本被這一個章節的篇名所騙了,我幹嘛要建立有序陣列呢?用物件不就好了嗎?甚麼二分搜尋法根本自討苦吃!!

function solution(A) {
    var M = {}
    for (var i = 0; i < A.length; i++) {
        M[A[i]] = true;
    }
    //console.log(Object.keys(M))
    return Object.keys(M).length
}

結果根本沒在鳥甚麼Sorting不Sorting的反而過關,100%PASS!(https://app.codility.com/demo/results/trainingTR8QQS-TDU/)

發佈日期:

Prefix Sums – MinAvgTwoSlice解題紀錄

題目內容

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

題目理解

尋找 (P, Q),其中0 ≤ P < Q < N,為陣列A的切片,N介於2~100000之間,並尋找最小平均切片的起始位置,要注意的是,陣列的內容為-10000~10000

感覺起來就是要先儲存從0~N的加總,然後再來尋找最小平均切片,但是思考起來這樣子複雜度應該也會有O(N**2),但是先這樣試看看

解題思路

第一次的嘗試

function solution(A) {
    var sum = []
    var current_sum = 0
    sum.push(0)
    for(var i=0;i<A.length;i++){
        current_sum += A[i];
        sum.push(current_sum)
    }

    var min = Infinity;
    var min_start = -1
    for(var i = 0;i<A.length;i++){
        for(var j = i+1;j<A.length;j++){
            var avg = (sum[j+1]-sum[i])/(j+1-i);
            if(min > avg) {
                min = avg;
                min_start = i
            }
        }
    }
    return min_start
}

我就知道….60%,timeout errors。複雜度還是太高,現在的解法是很直接的思考平均算法,先嘗試思考看看”平均”有沒有其他可能的算法

本來想嘗試必定包含最小值得所有可能,但是發現有邏輯錯誤,因為一個最小值不代表平均起來就一定最小,因為若該最小值的左右兩邊比較大,旁邊有兩個比較小的值,平均值會比包含最小值的還要小。所以即便是最小的值也不一定在最小平均之內。

//這會有錯誤的結果
function solution(A) {
    var sum = []
    var current_sum = 0
    sum.push(0)
    var mustIncludeIndex = -1
    var mustIncludeValue = Infinity
    for(var i=0;i<A.length;i++){
        current_sum += A[i];
        sum.push(current_sum)
        if(mustIncludeValue > A[i]) {
            mustIncludeValue = A[i];
            mustIncludeIndex = i
        }
    }
    var min = Infinity;
    var min_start = -1
    for(var i = 0;i<mustIncludeIndex;i++){
        for(var j = i+1;j<A.length;j++){
            var avg = (sum[j+1]-sum[i])/(j+1-i);
            if(min > avg) {
                min = avg;
                min_start = i
            }
        }
    }
    return min_start
}

萬般無助之下,詢問了ChatGPT,沒想到他都比我聰明(哭),好吧這就來改寫!!

以下為改寫後的程式碼

function solution(A) {
    var sum = []
    var current_sum = 0
    sum.push(0)
    for(var i=0;i<A.length;i++){
        current_sum += A[i];
        sum.push(current_sum)
    }

    var min = Infinity;
    var min_start = -1
    for(var i = 0;i<A.length;i++){
        var max_possible = Math.min(A.length, i+3)
        for(var j = i+1;j<max_possible;j++){
            var avg = (sum[j+1]-sum[i])/(j+1-i);
            if(min > avg) {
                min = avg;
                min_start = i
            }
        }
    }
    return min_start
}

好,100%!謝謝ChatGPT大神!!!

發佈日期:

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

發佈日期:

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/

發佈日期:

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程度的我玩這麼多次,真的很哭…但滿好玩的(有嗎?)

發佈日期:

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 &lt; 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))

發佈日期:

時間複雜度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
}

結果如下

發佈日期:

Chunked Encoding介紹

甚麼是Chunked Encoding

Chunked encoding(分塊編碼)是一種HTTP/1.1協議中的傳輸編碼方式,用於將HTTP消息主體分成多個塊(chunks),以便在網絡上進行有效傳輸。分塊編碼主要用於動態生成的內容,以及在事先不知道內容大小的情況下傳輸數據。

分塊編碼的工作原理如下:

  1. 服務器將HTTP消息主體分成多個大小可變的塊。每個塊由兩部分組成:塊大小(十六進制表示)和實際數據。
  2. 每個塊都以塊大小開頭,然後是一個回車換行符(CRLF),接著是實際數據。在每個塊的數據之後,還有另一個回車換行符(CRLF)。
  3. 數據傳輸完成後,服務器會發送一個大小為0的塊,表示數據已經全部傳輸完畢。接著,服務器可以選擇性地傳輸附加的HTTP頭部,以提供更多關於已傳輸數據的信息。
  4. 客戶端接收到分塊編碼的數據後,將各個塊重新組合成完整的HTTP消息主體。

要使用分塊編碼,服務器需要在HTTP響應頭中設置Transfer-Encoding字段為chunked。這告訴客戶端,接收到的數據將使用分塊編碼格式。

分塊編碼的主要優點是允許服務器在不知道最終內容大小的情況下開始傳輸數據。這對於動態生成的內容、實時數據流和大文件傳輸非常有用。此外,分塊編碼還可以實現數據的即時壓縮和傳輸,從而提高傳輸效率。

設定nginx以支持Chunked Encoding

以下是一個簡單的nginx.conf範例,用於支持在http://127.0.0.1/live下的文件開啟chunked encoding。此配置檔案會將請求代理到後端應用伺服器(例如:Node.js、Python或其他後端應用)進行處理。請注意,這裡假設後端應用伺服器已經正確配置並支持分塊編碼。

http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    keepalive_timeout  65;

    gzip  on;
    gzip_min_length 1024;
    gzip_proxied any;
    gzip_types text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml application/xml+rss;

    server {
        listen       80;
        server_name  127.0.0.1;

        location / {
            root   /usr/share/nginx/html;
            index  index.html index.htm;
        }

        location /live {
            proxy_pass http://backend:3000; # 請將 "backend" 替換為您的後端應用伺服器地址(IP 或 域名),並將 "3000" 替換為您的後端應用伺服器的端口

            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;

            proxy_http_version 1.1;
            proxy_set_header Connection "";
            chunked_transfer_encoding on;
        }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   /usr/share/nginx/html;
        }
    }
}
events {
    worker_connections  1024;
}

這個配置檔案將Nginx設置為代理位於http://127.0.0.1/live的請求,並將請求轉發到後端應用伺服器。在location /live部分,使用chunked_transfer_encoding on;指令開啟分塊編碼。

觀察你的檔案是否有啟用Chunked Encoding

我們可以從伺服器的回應看到這個伺服器的檔案傳輸是否有支持Chunked Encoding,如下圖