• 沒有找到結果。

質數 - 臺北市立建國高級中學

N/A
N/A
Protected

Academic year: 2023

Share "質數 - 臺北市立建國高級中學"

Copied!
1
0
0

加載中.... (立即查看全文)

全文

(1)

質數(Prime

如同大家所熟知的,質數就是一個正因數只有1和自己的正整數。

在寫程式時,我們要如何快速的找出質數呢?

最直觀的方法就是直接依照質數的定義去做。把所有大於一小於他的正整數全部拿來除除看,如 果都不能整除它那麼它就是個質數。

這方法在只問你某個數是不是質數的時候還可以用,但若要建立質數表的時候就非常緩慢了。

#想一想:真的需要把該數以下的所有數都拿來除,才能知道它是不是質數嗎?

Eratosthenes’ 篩法

兩千多年前,一個古希臘的數學家Eratosthenes就已經知道如何有效率的建立一個質數表了。

這個演算法的想法就是說,假如我們把正整數排成一列,由小到大去看,每看到一個質數就把它 的所有倍數刪掉,這樣就可以列出一張1~n的質數表了。

#想一想:要如何加快篩法的速度呢?

1. 對於質數p,可以從多少開始篩?

2. 需要篩到多大的質數才夠呢?

相關題目:TIOJ 1036

質因數分解

當建好了質數表後,因數分解就簡單了。從最小的質數開始一個一個試,如果是它的因數的話就 除掉後繼續往下試就好了。

#想一想:當你要求一個數的質因數分解時,質數表要建到多大才夠用?

※例題 — !!!!!!!!!!!!!!!!!!!!(TIOJ 1350)

給你k,請將k!因數分解。

相關題目:TIOJ 1351

遞迴&分而治之法(Recursion, Divide & Conquer

在數學上,遞迴指的是一個數列中的數和前面的幾項間存在著一種固定的關係,藉由找到這種關 係而解出原來的問題。

而在資訊上,概念和數學一樣是使用較小的問題來解出較大的問題,在程式實作時特別是指一個 函式自己呼叫自己。在解遞迴問題時,最重要的就是要找出問題和子問題之間的關聯,以及能夠 直接處理的小問題的答案。

分而治之法和遞迴頗為相似,是將問題分為數個規模較小但類似的問題,這樣不斷分解下去直到 可以直接解決的小問題。

※例題 — 約瑟夫問題

(2)

現在有n個人圍成一圈,從第一個人開始數,每數兩個人就殺掉一個,請問最後會剩下第幾個人?

※例題 — Savage Garden(ACM 10230)

在一個長為2n的正方形盤面上,挖去了一個空格。試用「L形」拼圖(總共3格所構成的)把整個 盤面拼滿。

相關題目:TIOJ 1055, 1060, 1108, 1364. ACM 374

排序(Sorting

給定一些數和他們之間的一種比較關係,請將他們由小到大排好。

排序是很重要的一個演算法,在許多的問題中,都需要先將給定的資料排序。

一些容易想到的O(n2)算法

1. 泡沫排序

當你走過一次陣列,並且對於相鄰的兩個數依照他們的關係交換,這樣就可以把最大的數換到 最後一個。這樣重複走過n次後就能排好整個陣列。

2. 選擇排序

每次都走過一次陣列並找出最大值,和陣列最後一個數交換,這樣重複n次後就可以排好整個 陣列。

3. 插入排序

和一般整理撲克牌的方法相像,就是每次拿起一個數,花O(n)的時間找到他應該要在的位置,

這樣重複n次後就可以排好整個陣列。

這些算法雖然時間複雜度比較高,但因為係數比較小,在規模較小時(如n<=16)速度是較快的。

O(nlgn)的排序法

這邊介紹兩種排序法:合併排序和快速排序,這兩種排序法都是基於分而治之的想法。

1. 合併排序(Merge sort)

#想一想:給你兩個已經排序好的序列,大小分別為m,n,要如何用O(m+n)的時間把兩個序列合 併成一個排序好的序列?

我們每次將序列切成兩個長度相同的序列,分別排序好後在用上面那個O(n)的合併法將兩序列合 併起來,而終止條件就是當序列長度只有一時。

Pseudocode:

MERGE (A, p, q, r) MERGE-SORT(A, p, r)

n1 p q + 1 if p < r

n2 r q then q ← ( p + q ) / 2

create arrays L[1..n1+1] and R[1..n2+1] MERGE-SORT(A, p, q) for i ← 1 to n1 MERGE-SORT(A, q + 1, r) do L[i] ← A[p+i-1] MERGE(A, p, q, r)

for j ← 1 to n2

(3)

do R[j] ← A[q+j]

L[n1+1] ← ∞ R[n2+1] ← ∞ i ← 1

j ← 1

for kp to r do if L[i] ≤ R[j]

then A[k] ← L[i]

ii + 1 else A[k] ← R[j]

jj + 1

2. 快速排序(Quick sort)

找數列中的一個數當作分界點,把所有比它大的數放它右邊,比它小的數放它左邊,然後再對兩 邊分別進行Quick sort,終止條件是陣列大小為0。

Pseudocode:

PARTITION (A, p, r) QUICKSORT(A, p, r)

x ← A[r] if p < r

ip – 1 then q ← PARTITION(A, p, r)

for jp to r – 1 QUICKSORT (A, p, q) do if A[j] ≤ x QUICKSORT (A, q + 1, r) then ii + 1

exchange A[i] ↔ A[j]

exchange A[i + 1] ↔ A[r]

return i + 1

雖然快速排列在最壞的情況下複雜度是O(n2)的,但如果選擇分界點時用隨機選擇的話,就可以達 到最壞複雜度O(nlgn),而Quick sort的實際執行效率也比Merge sort好。

#想一想:1. 如果一個排序法,對於兩筆值相同的元素在排序前後的相對位置不改變,則我們稱 該排序法為穩定(Stable)的,請問上述哪些排序法是穩定的?

2. 如何更改才能使得上述的演算法成為穩定的?

O(n)的特殊排序法

可以證明:在只用比較兩個數的排序法中,時間複雜度最好就是O(nlgn)。

但在要處理的資料比較特殊時,有一些複雜度O(n)(或是說接近O(n))的排序法。

1. Counting sort:當要處理的資料都是不大的整數時候,可以直接記錄每個數的出現次數後,再 依序輸出。

2. Bucket sort:將序列分為數個部分,再針對各部分做排序。

3. Radix Sort:從最小的位數開始,對每個位數做排序。

(4)

一些內建的排序函數 1. qsort

void qsort( void *buf, size_t num, size_t size, int (*compare)(const void *, const void *) );

C內建的Quick Sort,需要注意的是在compare函數裡要將void轉成所需的資料型態。

2. sort

void sort( iterator start, iterator end );

void sort( iterator start, iterator end, StrictWeakOrdering cmp );

STL內建,使用的是intro sort,最差與平均情況都是O(n lg n),intro sort其實就是多種排序演 算法的混合,根據不同算法在不同大小時候的表現選擇算法,使得它的速度比qsort() 快。

※例題 — 逆序數對(TIOJ 1080)

給你一個序列{ai},若存在i>j使得ai<aj,則稱(i,j)是一個逆序數對,請問這個數列中有多少個 逆序數對呢?請你找出一個O(nlgn)的演算法。

※例題 — 蛋糕內的信物(TIOJ 1364)

給你一個序列{ai}及k,請你找出數列中第k大的數。請你找出一個O(n)的演算法。

相關題目:TIOJ 1205, 1208, 1287

二分搜尋(Binary search

在一個已經排序好的數列中,找出一個給定的數的位置。

基本的想法就是利用數列已經排序好這個性質,每次都將想要找的數和正中央的那個數比較,並 依照比較結果可以知道要找的數是在左半還是在右半,這樣一直重複下去,可以在O(lgn)的時間 內找到要找的數。

Pseudocode:(假設數列是由小到大排好)

BINARY_SEARCH (A, p, r, d) while p < r

do m ← ( p + r ) / 2 if A[m] < d

then p m + 1 else rm

if A[p] = d

then return p else return not found

二元搜尋的運用可是非常廣泛的呢,例如這次校內初賽的第六題。如果能夠在題目中看出某種順 序的關係,會是個很強力的條件呢。

"Although the basic idea of binary search is comparatively straightforward, the details can be surprisingly tricky…" — Professor Donald Knuth,由這句話我們知道,其實二元搜尋並不是它看起來這麼容易

(5)

的呢,還有時候必須根據題目而更改一些細節,例如說當有很多個相同的元素時要找哪一個等等。

#想一想:當有很多個相同的元素,而題目要求要找出 (1)最前面的一個 (2)最後面的一個 的時候,

要怎麼樣處理比較簡單?而上面的程式碼找出來的會是這兩種之一嗎?

※例題 — Dark Rank(TIOJ 1360)

現在有一個m×n的表格,我們想找出每個橫排中最大的數在哪裡,我們已經知道這個表格有一個 性質,就是每橫排的最大值會在前一排最大值的右邊。你每次可以詢問任何一格的值,請你用越 少次越少的詢問來找出每個橫排最大值的位置。

樹(Tree

樹是一種很特殊的圖,它是一個連通但其中不含任何的圈的圖。

以下幾個敘述是等價的:

1. G是一個樹。

2. G為連通且沒有圈。

3. G為連通且| E | = | V | - 1。

4. G中任兩點恰好有唯一一條路徑連接。

5. G為連通,但是去掉任何一條邊後都會變成不連通。

6. G中沒有圈,但是加上任何一條邊後都會變成有圈。

而資訊中所談到的樹一般指的是「有根樹」。

有根樹就是將其中一點指定為根節點(Root),而其他點的深度定義就是他和根節點的距離。

這樣的有了以下的定義:

 母節點(parent node)、子節點(child node):如果a和b有邊相連,且b的深度比a的深 度多一,則我們稱a是b的母節點,b是a的子節點。一個節點可能有很多子節點,但只會有 一個母節點。

 兄弟節點(sibling):有相同母節點的兩個節點。

 度數(degree):一個節點的子節點數目。

 葉子(leaf):沒有子節點的節點。

 高度(height):我們稱一棵樹中節點的最大深度為該樹的高度。

k元樹(k-ary tree):如果一個樹每個節點的度數都不大於k,則稱此樹為一個k元樹。

 子樹(subtree):以某一個節點為根節點,並取所有他以下的節點和邊形成的樹。

 森林(forest):就是很多很多棵的樹(其實就是沒有圈的圖)

樹的表示法(Representation of Tree

要如何來儲存一棵樹呢?

我們先考慮最簡單的情況:二元樹,這時可以用兩個指標分別儲存左子節點和右子節點的位置。

Struct Node{

DataType data;

Struct Node* ParentNode;

Struct Node* LeftChild;

Struct Node* RightChild;

(6)

};

或是假如你知道了這棵樹是完全二元樹(complete binary tree),或是它的深度不深,沒有很歪斜,

那麼可以較簡單的用陣列來儲存一棵樹。令A[1]為根節點,對於一個節點A[n],它的子節點是 A[2n]和A[2n+1],母節點是A[n/2]。

用同樣的方法,假如我們知道了一個樹是k元樹,也可以用一個指標陣列來指向它的所有子節點。 Struct Node{

DataType data;

Struct Node* ParentNode;

Struct Node* ChildNode[k];

};

假如這棵樹沒有任何的限制的話,有一個叫做Left-child-right-sibling的方法可以來記錄它。就是對 於每個節點,記錄它最左邊的子節點,和他右邊相鄰的兄弟節點。雖然轉換會較為麻煩,但這個 是紀錄任意樹的比較合理的方法,尤其是它需要的記憶體用量會比上面那種k元樹紀錄法還少的 多。

Struct Node{

DataType data;

Struct Node* ParentNode;

Struct Node* LeftChild;

Struct Node* RightSibling;

};

至於若不是有根樹,只是一般的連通、無圈的圖,就只需要用普通的圖的表示法即可,而且通常 用的是adjacency list。

二元樹的走訪(Traversal of Tree

對於一個二元樹,我們有三種最常用的方法可以走過這棵樹所有的節點。

1. 前序表示法(pre-order)根節點 -> 左子樹 -> 右子樹 2. 中序表示法(in-order)左子樹 -> 根節點 -> 右子樹 3. 後序表示法(post-order)左子樹 -> 右子樹 -> 根節點

這三種方法都可以用遞迴來實現,而有趣的是,只要知道任意兩種表示法,就可以確定這棵樹進 而得到第三種表示法。

#想一想:1. 給你前序和中序表示法,如何求出後序表示法?

2. 給你前序和後序表示法,如何求出中序表示法?

3. 給你中序和後序表示法,如何求出前序表示法?

※例題 — 樹狀的堆積結構(TIOJ 1204)

我們把滿足「任何非根節點的值都比其母節點的數值還要大」的二元樹稱為一個「樹狀的堆積結構」,

現在給你某棵「樹狀堆積結構」的中序表示法,請求出它的前序表示法。

(7)

二元搜尋樹(Binary Search Tree - BST

二元搜尋樹是一種二元樹,並且滿足對於任何一個節點,它的左子樹的節點的值都比它小,它的 右子樹的節點的值都比它大,對二元搜尋樹,我們可以進行幾種操作。

1. 尋找元素:

和二元搜尋的概念一模一樣,從根節點開始,現在的數值比要找的數值大就往左走,如果比較 小就往右走,相等的話就找到了。

2. 增加元素:

和尋找元素的時候一樣,從根節點開始找,直到走到一個葉子為止,然後把它加在該葉子的子 節點。

3. 刪除元素:

刪除元素時比較麻煩,如果要刪除的節點是x,有三種情況:

* 如果x是葉節點,就直接刪除就好。

* 如果x只有一個子節點,則把該子節點連到x的母節點,並且刪除x。

* 如果x有兩個子節點,我們選取左子樹最右邊的一個元素y替代x,並且刪除y。

如果二元搜尋樹的高度是h,則上述三個操作的時間複雜度都是O(h)。

我們知道,如果一個二元樹是完全的,那麼它的高度為O(lgn),所以上述演算法的複雜度就是很

快的O(lgn)。但是這樣建立出來的二元搜尋樹往往是不完全,甚至於很不平衡的,在最糟糕的情況

下,全部元素排成一個直線時,h就變成O(n)了。

為了預防這種情形,我們可以在建立時將增加點的順序用成隨機的,可以證明隨機後的BST的深

度會是O(lgn)的,但是假如你的輸入不是一次給完的話(也就是要求要是在線算法),那麼這招

便不可行了。

而其實還有各種的平衡樹(balanced tree),例如AVL-tree、red black tree、Treap等等,但是因為 這些的方法都很複雜,所以在這裡就不介紹了。(其實是因為我也不會寫…)

其實樹還有很多的應用,像後面會講到的Heap就也是一種二元樹,Disjoint Set是一種森林等等。

所以樹是相當相當基本且重要的一種資料結構呢!

※例題 — 約瑟問題(TIOJ 1382)

有 n 個人圍成一圈,從第一個人開始數,每個人的椅子都被裝上"強制脫出裝置",現在給你n個 數,代表每一次要數幾個人之後彈出,請問你人被彈出的順序。

相關題目:TIOJ 1106, 1108, 1213, 1214

參考文獻

相關文件

壹、 前言 近年來注入式攻擊成為程式安全的一大隱憂。這樣的安全性問題可以說是無所不 在。這問題的發生原因,是對於使用者輸入的字串,在使用如組成 SQL 指令,或是送 到 eval 函數前沒有經過適當的過濾,導致意外執行了攻擊者所送入的程式碼。如果我 們能有一個方法,對於程式所有可能的字串輸入,都做適當的檢測,過濾危險的輸入

一個c−1,我們會希望他滿足c−1c≡ 1 mod m。可惜的是根據 Bezout’s Theorem,這要 求gcdc, m = 1。所以如果c, m互質,這樣的c−1 一定存在,稱之為模逆元。 模逆元可以直接由擴展歐幾里德求到,複雜度是Ologminc, m.不過如果 m是質數, 由費馬小定理知道cp−2