Previous | 回到目錄 |本文來源: 演算法筆記 | Next


Memoization

惟事事,乃其有備,有備無患。《書經》


Memoization

「記憶法」是符合電腦運作特性的方法。電腦擁有大量儲存空間。只要將計算過的數值,儲存於記憶體,往後就能直接使用記憶體儲存的資料,不必再浪費時間重複計算一遍。

Memoization(Tabulation)
演算法執行過程之中,即時更新數值,儲存於記憶體。
例如堆疊的大小。

Preprocessing(Precalculation)
演算法開始之時,預先計算數值,儲存於記憶體。
例如圓周率、字串的長度、質數的表格。
如果要儲存大量的、同性質的數值,我們可以將這些數值整理成一個表格(通常是陣列),以方便查閱──稱作「查詢表 
lookup table 」。例如質數表便是一個「查詢表」。

範例:陣列大小

使用一個變數,紀錄資料數量,以便迅速地增加資料。

//Swift的Array設計上與C/C++不太相同, 演算法操作上要特別留意與學習

var array = [Int]()
func array_size(array: [Int]) -> Int {
    var n = 0
    var a = array
    func addElement(i: Int) {
        a.append(i)
        n += 1
    }
    addElement(1)
    addElement(6)
    addElement(9)
    return n
}

extension Array {
    mutating func appendNew(element: Element) {
        self += [element]
    }
}
var anArray = [0, 1, 2, 3]
anArray.count
anArray.appendNew(4)
anArray.count

array_size(array)

C++ 程式語言的標準函式庫的 stack ,事實上也額外隱含了一個變數,紀錄資料數量。當堆疊塞入資料、彈出資料的時候,也就是呼叫 push 函式、呼叫 pop 函式的時候,就默默更新資料數量。

public struct Stack<T> {
    private var array = [T]()

    public var count: Int {
        return array.count
    }

    public var isEmpty: Bool {
        return array.isEmpty
    }

    public mutating func push(element: T) {
        array.append(element)
    }

    public mutating func pop() -> T? {
        if count == 0 {
            return nil
        } else {
            return array.removeLast()
        }
    }

    public func peek() -> T? {
        return array.last
    }
}
var stackOfNames = Stack(array: ["Carl", "Lisa", "Stephanie", "Jeff", "Wade"])
stackOfNames.push("Sean")
stackOfNames.count
stackOfNames.peek()
stackOfNames.pop()
stackOfNames.pop()
stackOfNames.count

範例:加總數字


利用一個變數,累計數字的總和。

import Foundation

func summation() -> Int {
    let a = [3, 6, 9, -8, 1]
    var sum = 0
    for element in a {
        sum += element
    }
    return sum
}
summation()
// 使用標準函式庫
print("sum = \([3, 6, 9, -8, 1].reduce(0) { $0 + $1})")


func summation(array: [Int]) -> Int {
    var sum = 0
    for element in array {
        sum += element
    }
    return sum
}
summation([3, 6, 9, -8, 1])

範例:統計字母數量

建立 26 格的陣列,讓字母 a 到 z 依序對應陣列的每一格,作為 lookup table 。一邊讀取字串,一邊累計字母出現次數。

// unicodeScalars很重要

var hello = "Hello, World"   // A -> 65, a -> 97
func countCharacter(str: String) -> [Int] {
    var array = [Int](count: 27, repeatedValue: 0)
    for scalar in str.unicodeScalars {
        var i: Int = (scalar.value < 65 || scalar.value > 122) ? 91 : Int(scalar.value)  // not a~z | A~Z -> 91 -> 26
        i = (i >= 97) ? (i - 97) : (i - 65)
        array[i] += 1
    }
    return array
}
let charCount = countCharacter(hello)
var countDic: [String: Int] = [:]
for i in 0..<charCount.count - 1 {
    let characters = "abcdefghijklmnopqrstuvwxyz".characters
    countDic[String(characters[characters.startIndex.advancedBy(i)])] = charCount[i]
}
countDic.description

UVa [10260], UVa [10082], UVa [10222], UVa [12626]


範例:統計數字數量

當數字範圍太大,無法建立那麼大的陣列,可以改用 hash table 、 binary search tree 等等資料結構作為 lookup table 。

let array_1 = [ 1, 3, 4, 10, 11, 100000000, 23, 99, 123, 514, 4]
func count_number(array: [Int]) -> [Int] {
    var c = [Int](count: 100000000, repeatedValue: 0)
    for element in array {
        c[element] += 1
    }
    return c
}
//let c = count_number(array_1)  //==> 資料量太大

func count_number_improve(array: [Int]) -> [Int: Int] {
    var dic = [Int: Int]()
    for element in array {
        if let _ = dic[element] {
            dic[element]! += 1
        } else {
            dic[element] = 1
        }
    }
    return dic
}
count_number_improve(array_1).description

UVa [11572], UVa [141]


範例:計數排序法( Counting Sort )

建立足夠長的陣列,讓數字對應陣列的每一格,作為 lookup table 。統計每個數字的出現次數。由小到大讀取 lookup table ,順便排序數字。

let array_2 = [ 3, 6, 9, 9, 1, 8, 2, 6]
func sort_number(array: [Int]) -> [Int] {
    var c = [Int](count: 10, repeatedValue: 0)
    for element in array {
        c[element] += 1
    }
    var a = [Int]()
    for i in 0..<c.count {
        while c[i] != 0 {
            c[i] -= 1
            a.append(i)
        }
    }
    return a    
}
let sorted = sort_number(array_2)

func sort_number_dic(array: [Int]) -> [Int] {
    var dic = [Int: Int]()
    for element in array {
        if let _ = dic[element] {
            dic[element]! += 1
        } else {
            dic[element] = 1
        }
    }
    dic.description
    var a = [Int]()
    for i in 0..<10 {
        if let _ = dic[i] {
            for _ in 0..<dic[i]! {
                a.append(i)
            }
        }
    }
    return a
}
let sortedAgain = sort_number_dic(array_2)

範例:求 1 到 n 的全部整數的立方和, n 的範圍由 1 到 10 。

以直接的方式,累加每個立方數。(儘管這個問題有公式解,但是為了方便舉例,所以這裡不採用公式解。)

//求 1 到 n 的全部整數的立方和, n 的範圍由 1 到 10 。

func sumOfCubic(n: Int) -> Int {
    var sum = 0
    for i in 0...n {
        sum += i * i * i
    }
    return sum
}
sumOfCubic(10)

//使用標準函式庫
let array_int = Array(1...10)
let array_cubic = array_int.map { $0 * $0 * $0 }
let sum_cubic = array_cubic.reduce(0) { $0 + $1 }
sum_cubic

使用 Memoization 。建立 11 格的陣列,每一格依序對應 0 到 10 的立方數,作為 lookup table 。一旦計算完畢,就儲存至表格;往後就直接讀取表格,不需重複計算。

// 本例使用static var在swift中只可使用於形態(class, struct, enum)中
struct SOC {
    static var answer: [Int] = Array(count: 11, repeatedValue: 0)
    static func getSoC(n: Int) -> Int {
        guard answer[n] == 0 else { return answer[n] }
        var sum = 0
        for i in 0...n {
            sum += i * i * i
            answer[i] = sum
        }
        return answer[n]
    }

}
SOC.getSoC(5)
SOC.answer[10]
SOC.getSoC(10)
SOC.answer[10]

func makeSumOfCubicTable(n: Int) -> [Int] {
    var array = [Int]()
    for i in 0...n {
        array.append(sumOfCubic(i))
    }
    return array
}
makeSumOfCubicTable(10)[10]

使用 Preprocessing 。

func sumOfCubicBy_precubicarray(n: Int) -> Int {
    var array = Array(count: 11, repeatedValue: 0)
    for i in 0...n {
        array[i] = i * i * i
    }
    var sum = 0
    for i in 0...n {
        sum += array[i]
    }
    return sum
}
sumOfCubicBy_precubicarray(10)

Preprocessing 當然也可以直接算答案啦。

// C++範例
int sum_of_cubes(int n) {
    int sum = 0;
    for (int i=1; i<=n; i++)
        sum += i * i * i;
    return sum;
}

void print_sum_of_cubes() {
    // 預先計算所有答案
    int answer[10 + 1];
    for (int i=1; i<=10; ++i)
        answer[i] = sum_of_cubes(i);

    // 直接讀取表格的答案
    int n;
    while (cin >> n && n > 0)
        cout << answer[n];
}

最後是 Preprocessing 的極致。

// C++範例
void print_sum_of_cubes() {
    // 預先計算答案,寫死在程式碼裡面。
    int answer[10 + 1] ={
            0, 1, 9, 36, 100, 225,
            441, 784, 1296, 2025, 3025
    };
    // 直接讀取表格的答案
    int n;
    while (cin >> n && n > 0)
        cout << answer[n];
}

UVa [10738], UVa [10894]


範例:印出方框

建立二維陣列:陣列的格子,依序對應視窗的文字。

不直接印出方框,而是間接填至陣列。不必數空白鍵,只需兩條水平線和兩條垂直線。

func printSQ() {
    var array = Array2D(columns: 5, rows: 5, initialValue: "")
    for i in 0..<5 {
        for j in 0..<5 {
            array[i, j] = (i == 0 || i == 4 || j == 0 || j == 4) ? "*" : " "
            array[i, 4] += "\n"
            print(array[i, j], separator: "", terminator: "")
        }
    }
}
printSQ()

UVa [105], UVa [706]


範例:拆開迴圈( Loop Unrolling )

迴圈語法的功能是:一段指令,重覆實施數次,但是每次都稍微變動一點點。

事實上,我們可以反璞歸真,拆開迴圈,還原成數行指令。如此一來,就節省了迴圈每次累加變數的時間,也節省了迴圈每次判斷結束條件的時間。

拆開迴圈是一種 Preprocessing ,預先計算迴圈變量、預先計算迴圈結束條件。

拆開迴圈之後,雖然提高了程式的執行速度,但是降低了程式可讀性。程式員必須自行取捨。


Previous | 回到目錄 |本文來源: 演算法筆記 | Next

results matching ""

    No results matching ""