結合律演算法

這裡記錄一些有關結合律的演算法。

目錄

前言

基本代數定義

這裡用比較偏集合論的語言來描述。

對於集合

S,稱
S
上的二元運算,代表它是二元函數
:S×SS

這時就說
S
配備上
組成了代數結構
S,

我們常會要求二元運算

滿足封閉性:對任意
a,bS
都有
abS
,這時我們也會說
S
對二元運算
封閉。
而這裡把
定為
S
上的二元運算直接蘊含了封閉性,所以我們不必再額外設定這個條件。

我們可以用

SS×S 代表搜集
S
上二元運算的集合,其中
BA
代表搜集所有函數
AB
的集合。
在 Haskell 中習慣表示為 S → S → S,這是因為
SS×S
(SS)S
同構,而後者更方便於 Haskell 中使用。

對於代數結構

S,,如果任意
x,y,zS
都滿足
(xy)z=x(yz),
我們就說
滿足結合律,同時也稱
S,
為半群(semigroup)。

結合律是常見於各種代數結構中的性質,這代表這篇筆記提到的演算法也可以適用於很多情境。
有了結合律,我們也不必使用括號表明運算順序了。

還有一個常見的性質稱為交換律,也就是對任意

x,yS 都有
xy=yx

並不是所有代數結構都滿足交換律,但如果滿足就會稱是可交換的(commutative)。

對於代數結構

S,,如果
eS
對任意
xS
都滿足
xe=x=ex,
我們就稱
e
S,
的單位元素。
對於
xS
,如果
yS
滿足
xy=e=yx,
我們就說
x
可逆,同時也稱
y
x
的反元素。

當半群

S, 有單位元素
e
,我們就稱代數結構
S,,e
為么半群(monoid)。
若每個元素都可逆,我們就稱
S,,e
為群(group)。

Monoid 在國教院網站被翻譯成相當白話的「具單位元素半群」。
在這邊我們選用另一個常見翻譯「么半群」,其中「么」對應「mono-」,大概是單位元素的意思。

例子

我們可以從既有的半群得到其他半群:

  • 對於半群
    S,
    與代數結構
    A,
    ,若有同態
    φ:SA
    ,也就是對任意
    a,bS
    都滿足
    φ(ab)=φ(a)φ(b)
    ,則
    img(φ),
    也是半群。
    • S
      有單位元素
      e
      ,則
      img(φ),,φ(e)
      是么半群。
  • 對於么半群
    M1,1,e1
    M2,2,e2
    ,若有同態
    φ:M1M2
    ,則
    ker(φ),1,e1
    也是么半群。
    • 其中
      ker(φ):={aM1φ(a)=e2}
  • 對於半群
    S,
    ,若子集
    XS
    封閉,則
    X,
    也是半群。
    • 若有單位元素
      e
      並且也屬於
      X
      ,則
      X,,e
      是么半群。
  • 對於半群
    S1,1
    S2,2
    ,它們的積
    S1×S2,
    也是半群,其中
    (a1,a2)(b1,b2):=(a11b1,a22b2).
    • 若各有單位元素
      e1
      e2
      ,則
      S1×S2,,(e1,e2)
      是么半群。
  • 對於集合
    X
    與半群
    S,
    ,代數結構
    SX,
    是半群,其中
    (fg)(x):=f(x)g(x).
    • 若有單位元素
      e
      ,則
      SX,,ce
      是么半群,其中令
      ce:xe

下面列舉的大多數例子其實是更複雜的代數結構,在這裡不把詳細定義寫出。

合成

合成其實就是串接。
為了描述抽象又通用的概念,這裡講的合成(composition)使用了範疇論的語言。
範疇(category)包含了兩類事物:物件(object)與態射(morphism)。
任意的態射

f:XY
g:YZ
都存在它們的合成
gf:XZ
,其中代表合成的二元運算
被要求滿足結合律。
而每個物件
X
也都有單位態射
idX
,使得對任意態射
f:XY
都有
idYf=f=fidX.

於是我們有以下的例子:

  • 對於範疇
    C
    與物件
    X
    ,代數結構
    HomC(X,X),,idX
    是么半群。
    • 事實上任何的么半群都可以轉化成單物件範疇。
  • 對於集合
    S
    ,代數結構
    SS,,idS
    是么半群。
  • S
    搜集所有字串,則代數結構
    S,,ε
    是么半群。
    • 二元運算
      代表字串串接、
      ε
      代表空字串。

環(ring)是配備了加法(

+)與乘法(
)的代數結構。
R,+,,0,1
包含了可交換的加法群
R,+,0
與乘法么半群
R,,1

下面列舉的各個例子都是環,也就是說都包含有加法群與乘法半群:

  • 整數
    Z
    、模
    n
    整數
    Z/nZ
    、有理數
    Q
    、實數
    R
    、複數
    C
    都是環。
  • 任意環
    R
    都有多項式環
    R[x],+,,0,1
    • 其中
      R[x]
      搜集係數屬於環
      R
      的一元多項式。
  • 任意環
    R
    都有矩陣環
    Mn(R),+,,On,In
    • 其中
      Mn(R)
      搜集元素屬於環
      R
      n
      階方陣。
  • 布林環
    B,,,0,1
    • 其中
      代表 XOR。
    • 要注意與下面的布林代數不同。

格(lattice)是配備了交(

,meet)與接(
,join)的代數結構。
L,,
包含了兩個可交換的半群
L,
L,

若只包含其中一個二元運算,那麼就稱為半格(semilattice)。

對於偏序

P,,如果任兩個元素都有最大下界或最小上界,那麼
P,inf
P,sup
也會形成半格。
另一方面,半格都能定義出對應的偏序結構:
abab=a.

如果對應的偏序結構有最大元素
或最小元素
,那麼
L,,
L,,
就是么半群,而
L,,,,
也稱為有界格。

下面列舉的各個例子都是格,也就是說都包含有兩種半群:

  • 布林代數
    B,,,0,1
    是有界格。
    • 要注意與上面的布林環不同。
  • 整除關係
    Z,gcd,lcm,1,0
    是有界格。
  • 任意集合
    S
    都有有界格
    2S,,,,S
    • 其中
      2S
      搜集所有
      S
      的子集。
  • 任意全序
    T,
    都有格
    T,min,max

對於半群

S, 以及任意
xS
,我們令「次方」為
xn:=i=1nx=xxxn.
如果是在么半群
S,,e
內,我們定義
x0:=e

我們會發現因為

滿足結合律,所以「次方」也會滿足指數律:
xm+n=xmxn,xmn=(xm)n.

快速冪

這個演算法也被稱為平方求冪法(exponentiating by squaring)。
而對於把

運算稱為「加法」的代數結構,這類算法也稱為 double-and-add。

快速冪是一個快速算「次方」的演算法,核心概念就是利用指數律來降低

的運算次數。
比如說把
x14
變成
(x7)2
(x2)7
,像這樣一直遞迴拆解下去,我們就能用
O(logn)
次的
運算計算出
xn

我們通常會選用
2
來除,這多半是因為對現實中機器而言,除以
2
與判斷奇偶數是非常容易的操作。
而依照拆解的方式又可分為以下兩類。

第一類算法:
x2k=(xk)2

例如

x14=((x2x)2x)2
這裡使用以下的規則:
xn={x,n=1;xkxk,n=2k;xkxkx,n=2k+1.

當我們是在么半群
S,,e
中計算時,將第一條式子改為
x0=e

第二類算法:
x2k=(x2)k

例如

x14=x8x4x2,其中
x2n
都是從已經算過的
xn
「平方」之後算出來的。
這裡使用以下的規則:
xn={x,n=1;(xx)k,n=2k;(xx)kx,n=2k+1.

當我們是在么半群
S,,e
中計算時,將第一條式子改為
x0=e

區間查詢

對於半群

S, 以及
S
中的序列
(xi)i=1n
,我們令
x(a,b):=i=abxi=xaxa+1xb.
下面的各個小節主要關注在查詢多個
x(a,b)
時,如何有效率地求出各個結果而不總是從頭計算。
更進一步地,我們也關注在查詢之間更改
xi
的情形,這被稱為「動態區間查詢」問題。







下面的小節會用到一種結構稱為樹。我們會將樹

T 上的資料結構考慮為節點到值的函數
τ:TS

需要注意的是,這裡所說的樹是無窮大的樹,而資料結構
τ
的定義域則往往是有限的。
如果這個樹是二元樹,我們會特別記為
T2

其他符號定義如下:

對於樹

T 與其上的節點
vT
,我們定義

  • root(T)
    T
    的根結點。
  • parent(v)
    v
    的父結點。
  • childi(v)
    v
    的第
    i
    個子結點。
  • 特別地當在
    vT2
    時,
    • left(v):=child1(v)
      v
      的左子結點。
    • right(v):=child2(v)
      v
      的右子結點。

部分和

在某些地方也被稱為前綴和(prefix sum)。

Work In Progress

與 scan 的關聯

函數型程式語言裡可能設計有高階函式

scan:AA×B×A×BnAn+1 使得
scan(,a,(bi))=(a,ab1,(ab1)b2,,((ab1))bn).

在 Haskell 裡面寫作 scanl,後面的 l 是左結合的意思,另外還有 scanl1: (a → a → a) → [a] → [a] 更符合我們說的部分和。

其中作為參數之一的二元運算

:A×BA 並不必是半群運算,所以
scan
看似與半群上的部分和不太一樣。
然而這只是程式語言的設計選擇而已,我們可以考慮以下的定義:

  • σb:aab
    ,也就是說
    σb
    代表在右邊乘上
    b
    的運算。
  • S:={σbbB}{idA}

能驗證

S,,idA 是么半群,於是
scan
可以視為
S,,idA
上的部分和:
scan(,a,(bi))=σb(0,i)(a).

二分區間樹

據說是演算法競賽選手發明的資料結構,競賽圈通常稱其為線段樹(segment tree)。
然而這個名稱似乎有歧義,在這裡我們姑且叫它二分區間樹。

二分區間樹

τx:T2S 是由以下兩個規則定義的二元樹資料結構:

  1. τx(root(T2))=x(1,n)
  2. 如果
    τx(v)=x(a,b)
    a<b
    ,令
    c=(a+b)/2
    ,那麼
    τx(left(v))=x(a,c),τx(right(v))=x(c+1,b).

我們會發現

τx(v)=τx(left(v))τx(right(v)),讓我們能自下而上建構
τx

每一個節點都代表對應一個區間值,我們就寫作
τx(v)=x(v,rv)

因為每個子節點都是二分區間之後的結果,這個樹狀資料結構會是非常平衡的。
當序列長度是

n 時,定義域的節點數會是
2n1
個,最大深度則是
log2n

例如當
n=7
時,我們可以用以下的圖示表示二分區間樹
τx







%0



x(1,7)

x(1,7)



x(1,4)

x(1,4)



x(1,7)--x(1,4)




x(5,7)

x(5,7)



x(1,7)--x(5,7)





x(1,2)

x(1,2)



x(1,4)--x(1,2)




x(3,4)

x(3,4)



x(1,4)--x(3,4)




x(5,6)

x(5,6)



x(5,7)--x(5,6)




x(7,7)

x(7,7)



x(5,7)--x(7,7)





x(1,1)

x(1,1)



x(1,2)--x(1,1)




x(2,2)

x(2,2)



x(1,2)--x(2,2)





x(3,3)

x(3,3)



x(3,4)--x(3,3)




x(4,4)

x(4,4)



x(3,4)--x(4,4)





x(5,5)

x(5,5)



x(5,6)--x(5,5)




x(6,6)

x(6,6)



x(5,6)--x(6,6)









演算法競賽圈常有人用序列模擬二元樹,也就是令

root(T2)=1
left(i)=2i
right(i)=2i+1

然而二分區間樹的定義會使最深的一層在序列上變得分散,例如當
n=2m+2
時,序列最後一項有定義的是
τx(2m3+1)=x2m1+3

這就是競賽圈實作時序列長度常會開到
4n
的原因……

不過這並不代表建構

τx 都必須自下而上把
2n1
個節點通通跑過。
如果序列
(xi)
在么半群
S,,e
中,我們可以先令二分區間樹為常數函數
τe:ve

如果序列中只有
k
項不是
e
,那麼對
τe
施以下一小節的單項更改來一步步建構出我們需要的
τx
只需要更改不超過
k(log2n+1)
個節點。
也就是說如果非
e
項占約全部的
2/(log2n+1)
以下,這樣做都會比較省步驟。

在演算法競賽圈,這種建立方式稱為「動態開點」。

單項更改

我們可以透過以下算法做單項更改:

τy=update(i,yi,τx)update(i,yi,τx)(v)={yi,v=i=rv;τy(left(v))τx(right(v)),irleft(v);τx(left(v))τy(right(v)),right(v)i;τx(v),otherwise.

n=7 時,若要更改
x3
,那麼
x(3,3)
x(3,4)
x(1,4)
x(1,7)
對應的值都需要重新計算。







%0



x(1,7)

x(1,7)



x(1,4)

x(1,4)



x(1,7)--x(1,4)




x(5,7)

x(5,7)



x(1,7)--x(5,7)





x(1,2)

x(1,2)



x(1,4)--x(1,2)




x(3,4)

x(3,4)



x(1,4)--x(3,4)




x(5,6)

x(5,6)



x(5,7)--x(5,6)




x(7,7)

x(7,7)



x(5,7)--x(7,7)





x(1,1)

x(1,1)



x(1,2)--x(1,1)




x(2,2)

x(2,2)



x(1,2)--x(2,2)





x(3,3)

x(3,3)



x(3,4)--x(3,3)




x(4,4)

x(4,4)



x(3,4)--x(4,4)





x(5,5)

x(5,5)



x(5,6)--x(5,5)




x(6,6)

x(6,6)



x(5,6)--x(6,6)









區間查詢

我們可以透過以下算法得出特定的區間值

x(a,b)
x(a,b)=query(a,b,τx,root(T2))query(a,b,τx,v)={τx(v),avrvb;L,brleft(v);R,right(v)a;LR,otherwise.L=query(a,b,τx,left(v))R=query(a,b,τx,right(v))

n=7 時,若要求
x(2,6)
,就要計算
x(2,2)x(3,4)x(5,6)







%0



x(1,7)

x(1,7)



x(1,4)

x(1,4)



x(1,7)--x(1,4)




x(5,7)

x(5,7)



x(1,7)--x(5,7)





x(1,2)

x(1,2)



x(1,4)--x(1,2)




x(3,4)

x(3,4)



x(1,4)--x(3,4)




x(5,6)

x(5,6)



x(5,7)--x(5,6)




x(7,7)

x(7,7)



x(5,7)--x(7,7)





x(1,1)

x(1,1)



x(1,2)--x(1,1)




x(2,2)

x(2,2)



x(1,2)--x(2,2)





x(3,3)

x(3,3)



x(3,4)--x(3,3)




x(4,4)

x(4,4)



x(3,4)--x(4,4)





x(5,5)

x(5,5)



x(5,6)--x(5,5)




x(6,6)

x(6,6)



x(5,6)--x(6,6)









惰性區間更改

顯而易見,如果我們要同時更改一個區間的值,不斷使用單點更改的方法將會耗費大量的步驟重新建構

τx
不過如果我們所要處理的更改操作有足夠好的性質,那我們可以就利用惰性運算解決這個問題。

對於更改序列的操作

f:(xi)(yi),我們希望能直接算出區間值
y(a,b)
,不必自下而上完全重建新的
τy
,並且只有在有需要求子區間值的時候才往下更改。
也就是說,假設
F
是我們要處理的一類更改操作,我們就要找出
Φ:S×ΛS
ϕ0:FΛ
ϕL,ϕR:ΛΛ
,滿足以下兩個性質:

  1. τy(root(T2))=Φ(τx(root(T2)),ϕ0(f))
  2. 如果
    τy(v)=Φ(τx(v),t)
    ,就有
    τy(left(v))=Φ(τx(left(v)),ϕL(t)),τy(right(v))=Φ(τx(right(v)),ϕR(t)).

在演算法競賽圈中稱

tΛ 為懶惰標記,而
ϕL
ϕR
可能被稱為 push,表示把標記往下一層推進。

二元索引樹

由計算機科學家 P. M. Fenwick 提出,也被稱為 Fenwick 樹。
演算法競賽圈通常稱它為二元索引樹(binary indexed tree,縮寫為 BIT)。

二元索引樹

τx:TS 是啥?

n=13 時,我們可以用以下的圖示表示二元索引樹
τx







%0



0

0



1

1



0--1




2

2



0--2




4

4



0--4




8

8



0--8




3

3



2--3




5

5



4--5




6

6



4--6




9

9



8--9




10

10



8--10




12

12



8--12




7

7



6--7




11

11



10--11




13

13



12--13










%0



8

8



4

4



8--4




6

6



8--6




7

7



8--7




12

12



10

10



12--10




11

11



12--11




16

16



16--8




16--12




14

14



16--14




13

13



14--13




2

2



4--2




3

3



4--3




5

5



6--5




1

1



2--1




9

9



10--9










%0



10

10



9

9



10--9




11

11



10--11




13

13



14

14



14--13




8

8



4

4



8--4




12

12



8--12




2

2



4--2




6

6



4--6




12--10




12--14




1

1



2--1




3

3



2--3




5

5



6--5




7

7



6--7




Work In Progress