這裡記錄一些有關結合律的演算法。
這裡用比較偏集合論的語言來描述。
對於集合 ,稱 是 上的二元運算,代表它是二元函數 。
這時就說 配備上 組成了代數結構 。
我們常會要求二元運算 滿足封閉性:對任意 都有 ,這時我們也會說 對二元運算 封閉。
而這裡把 定為 上的二元運算直接蘊含了封閉性,所以我們不必再額外設定這個條件。
我們可以用 代表搜集 上二元運算的集合,其中 代表搜集所有函數 的集合。
在 Haskell 中習慣表示為S → S → S
,這是因為 與 同構,而後者更方便於 Haskell 中使用。
對於代數結構 ,如果任意 都滿足 我們就說 滿足結合律,同時也稱 為半群(semigroup)。
結合律是常見於各種代數結構中的性質,這代表這篇筆記提到的演算法也可以適用於很多情境。
有了結合律,我們也不必使用括號表明運算順序了。
還有一個常見的性質稱為交換律,也就是對任意 都有 。
並不是所有代數結構都滿足交換律,但如果滿足就會稱是可交換的(commutative)。
對於代數結構 ,如果 對任意 都滿足 我們就稱 為 的單位元素。
對於 ,如果 滿足 我們就說 可逆,同時也稱 為 的反元素。
當半群 有單位元素 ,我們就稱代數結構 為么半群(monoid)。
若每個元素都可逆,我們就稱 為群(group)。
Monoid 在國教院網站被翻譯成相當白話的「具單位元素半群」。
在這邊我們選用另一個常見翻譯「么半群」,其中「么」對應「mono-」,大概是單位元素的意思。
我們可以從既有的半群得到其他半群:
下面列舉的大多數例子其實是更複雜的代數結構,在這裡不把詳細定義寫出。
合成其實就是串接。
為了描述抽象又通用的概念,這裡講的合成(composition)使用了範疇論的語言。
範疇(category)包含了兩類事物:物件(object)與態射(morphism)。
任意的態射 與 都存在它們的合成 ,其中代表合成的二元運算 被要求滿足結合律。
而每個物件 也都有單位態射 ,使得對任意態射 都有
於是我們有以下的例子:
環(ring)是配備了加法()與乘法()的代數結構。
環 包含了可交換的加法群 與乘法么半群 。
下面列舉的各個例子都是環,也就是說都包含有加法群與乘法半群:
格(lattice)是配備了交(,meet)與接(,join)的代數結構。
格 包含了兩個可交換的半群 與 。
若只包含其中一個二元運算,那麼就稱為半格(semilattice)。
對於偏序 ,如果任兩個元素都有最大下界或最小上界,那麼 或 也會形成半格。
另一方面,半格都能定義出對應的偏序結構:
如果對應的偏序結構有最大元素 或最小元素 ,那麼 或 就是么半群,而 也稱為有界格。
下面列舉的各個例子都是格,也就是說都包含有兩種半群:
對於半群 以及任意 ,我們令「次方」為 如果是在么半群 內,我們定義 。
我們會發現因為 滿足結合律,所以「次方」也會滿足指數律:
這個演算法也被稱為平方求冪法(exponentiating by squaring)。
而對於把 運算稱為「加法」的代數結構,這類算法也稱為 double-and-add。
快速冪是一個快速算「次方」的演算法,核心概念就是利用指數律來降低 的運算次數。
比如說把 變成 或 ,像這樣一直遞迴拆解下去,我們就能用 次的 運算計算出 。
我們通常會選用 來除,這多半是因為對現實中機器而言,除以 與判斷奇偶數是非常容易的操作。
而依照拆解的方式又可分為以下兩類。
例如 。
這裡使用以下的規則:
當我們是在么半群 中計算時,將第一條式子改為 。
例如 ,其中 都是從已經算過的 「平方」之後算出來的。
這裡使用以下的規則:
當我們是在么半群 中計算時,將第一條式子改為 。
對於半群 以及 中的序列 ,我們令 下面的各個小節主要關注在查詢多個 時,如何有效率地求出各個結果而不總是從頭計算。
更進一步地,我們也關注在查詢之間更改 的情形,這被稱為「動態區間查詢」問題。
下面的小節會用到一種結構稱為樹。我們會將樹 上的資料結構考慮為節點到值的函數 。
需要注意的是,這裡所說的樹是無窮大的樹,而資料結構 的定義域則往往是有限的。
如果這個樹是二元樹,我們會特別記為 。
其他符號定義如下:
對於樹 與其上的節點 ,我們定義
在某些地方也被稱為前綴和(prefix sum)。
Work In Progress…
函數型程式語言裡可能設計有高階函式 使得
在 Haskell 裡面寫作
scanl
,後面的l
是左結合的意思,另外還有scanl1: (a → a → a) → [a] → [a]
更符合我們說的部分和。
其中作為參數之一的二元運算 並不必是半群運算,所以 看似與半群上的部分和不太一樣。
然而這只是程式語言的設計選擇而已,我們可以考慮以下的定義:
能驗證 是么半群,於是 可以視為 上的部分和:
據說是演算法競賽選手發明的資料結構,競賽圈通常稱其為線段樹(segment tree)。
然而這個名稱似乎有歧義,在這裡我們姑且叫它二分區間樹。
二分區間樹 是由以下兩個規則定義的二元樹資料結構:
我們會發現 ,讓我們能自下而上建構 。
每一個節點都代表對應一個區間值,我們就寫作 。
因為每個子節點都是二分區間之後的結果,這個樹狀資料結構會是非常平衡的。
當序列長度是 時,定義域的節點數會是 個,最大深度則是 。
例如當 時,我們可以用以下的圖示表示二分區間樹 :
演算法競賽圈常有人用序列模擬二元樹,也就是令 、、。
然而二分區間樹的定義會使最深的一層在序列上變得分散,例如當 時,序列最後一項有定義的是 。
這就是競賽圈實作時序列長度常會開到 的原因……
不過這並不代表建構 都必須自下而上把 個節點通通跑過。
如果序列 在么半群 中,我們可以先令二分區間樹為常數函數 。
如果序列中只有 項不是 ,那麼對 施以下一小節的單項更改來一步步建構出我們需要的 只需要更改不超過 個節點。
也就是說如果非 項占約全部的 以下,這樣做都會比較省步驟。
在演算法競賽圈,這種建立方式稱為「動態開點」。
我們可以透過以下算法做單項更改:
當 時,若要更改 ,那麼 、、、 對應的值都需要重新計算。
我們可以透過以下算法得出特定的區間值 :
當 時,若要求 ,就要計算 。
顯而易見,如果我們要同時更改一個區間的值,不斷使用單點更改的方法將會耗費大量的步驟重新建構 。
不過如果我們所要處理的更改操作有足夠好的性質,那我們可以就利用惰性運算解決這個問題。
對於更改序列的操作 ,我們希望能直接算出區間值 ,不必自下而上完全重建新的 ,並且只有在有需要求子區間值的時候才往下更改。
也就是說,假設 是我們要處理的一類更改操作,我們就要找出 、、,滿足以下兩個性質:
在演算法競賽圈中稱 為懶惰標記,而 與 可能被稱為
push
,表示把標記往下一層推進。
由計算機科學家 P. M. Fenwick 提出,也被稱為 Fenwick 樹。
演算法競賽圈通常稱它為二元索引樹(binary indexed tree,縮寫為 BIT)。
二元索引樹 是啥?
當 時,我們可以用以下的圖示表示二元索引樹 :
Work In Progress…