上一篇:http://m.shnenglu.com/tanky-woo/archive/2011/04/09/143794.html
前三章基本沒什么內容,所以合在一起總結。
第一章:
講了算法(algorithm)的基本概念,以及算法的作用。(這些可以看書)
用個人的話來講,你可以把算法當做一個解決問題的方法,就像數學里的各種公式一樣,你也可以把他們認為是一種算法。算法無處不在,而且算法必須存在,否則我們的生活都將變得緩慢,遲鈍。
舉個例子:我們平時出去游玩時,要事先查好路線,這時就可以用百度地圖搜索從A地到B地的路線,地圖上會給出最快的乘車路線,這些路線是怎么給出來的,就是用了最短路的算法,關于最短路的算法有很多,比如Dijkstra, Bellman, Floyd, SPFA等等,當然還有好多我不知道,但是通過這可以看出,算法可以讓我們的生活變得更有效率。
當然,第一章也可以認為是給大家鼓氣的一章,讓大家發現算法的魅力,算法的強悍。大家都來愛上算法吧!
第二章:
本書的算法都是用偽代碼寫的,偽代碼讀起來很簡單,它省去了無關的細節,著重考慮算法的整體。
2.1節講的是插入排序(Insertion Sort),這個很簡單,也可以認為是最基本的排序算法。
(P11)需要好好的記住,一般一本書中都會寫一些事先的約定,以方便大家閱讀。本書也不例外,這些約定都是關于偽代碼的,為了更好的閱讀并理解偽代碼,所以這些約定要記住了!
2.2節講的是算法的分析。算法分析是指對一個算法所需要的資源進行預測。在(P13)講到了"運行時間"和"輸入規模"的概念。一個程序的運行時間可以表示為一個輸入規模的函數。一般算法所需的時間與輸入規模是同步增長的,而且對于不同的輸入序列,其運行時間也可能不同。(P14~15的算法運行時間分析要好好看看)。
2.3節講的是分治法。
分治策略:將原問題劃分成n個規模較小而結構與原問題相似的子問題;遞歸的解決這些子問題,然后再合并其結果,就得到原問題的解。
分治策略的三步驟(P17):分解(Divide),解決(Conquer),合并(Combine)。
合并排序算法就是利用了分治策略,將n個元素分成各含n/2個元素的子序列。
這個是分治法的精髓:
其實理解起來很簡單,有沒有發現和二叉樹的后序遍歷類似。
第三章:
一般而言,我們研究的是算法的漸進意義。我在這里把漸進確界,漸進上界,漸進下界的三個符號的定義放在了一起:
書上的圖3-1也非常給力:
這一章全部很重要。可以先記住,然后在后面的章節通過實踐來掌握
Tanky Woo 標簽:
算法導論,
算法分析,
漸進效率
posted @
2011-04-10 09:53 Tanky Woo 閱讀(2875) |
評論 (4) |
編輯 收藏
09年買的這本書,不過先開始一直沒怎么用,直到去年6月份左右開始搞ACM,才偶爾翻翻這本書。
這本書給我這樣的感覺:有時遇到一個算法,在網上找了很多相關資料,但是看完后還是有點迷茫,然后才想起《算法導論》,遇到翻開目錄,發現有相關的章節,于是去認真閱讀,頓時發現自己的很多問題都可以解決了。它就是這么一本書,也許你會把它當一本圣經來供養,但是當你認真閱讀后,你會發現你受益頗多。
于是,自從幾次問題通過《算法導論》解決后,我開始意識到,這是一個多么大的寶庫啊。它容納的目前常用的諸多算法,并且都給予了詳細解釋,圖文并茂,易于理解。
到目前為止,中間零散的看過一些章節。我有這么一個習慣,就是每學到一個算法,我都會把這個算法總結一下,我覺得雖然當時寫這個總結時有點占用時間,但是當你再溫故時,你只需要短短的5分鐘,就可以再次記住這個算法,并且通過以前總結的,你也許還可以發現不足,并改正。就像我之前有兩次都中斷了算法的學習,并且一斷就是幾個月,但是當我再次拾起算法時,我只需要看看我以前總結的筆記,就可以很快的拾起。在這里推薦下我的算法專題:http://www.wutianqi.com/sfzt.html
所以,這次我也準備把《算法導論》這本書好好總結一下,這樣當我總結時,我就可以知道哪些我徹底掌握了,因為如果我只掌握其表面內容,我是沒法用自己的話去總結。二是這樣大家通過各種搜索引擎搜到我的文章時,可以互相探討,并且發現哪些地方我理解錯了,因為我是非計科生,從大一到現在都是我自己一個人自學過來的,中間肯定會走一些彎路,對一些知識產生曲解,所以這樣也可以通過大家來改正我的錯誤,大家互相學習,互相探討,交一個可以討論學術的朋友,何樂而不為呢?
網上有些朋友推薦再遇到算法問題時可以把《算法導論》當字典來查,但是個人覺得,這本書讀多少遍都值得,所以完完整整的把這本書看一遍是必須的,以后再可以當一個工具書去查。所以推薦大家一起好好把《算法導論》學習下。
另外,我推薦每學一個算法,就去各個OJ(Online Judge)找一些相關題目做做,有時理論讓人很無語,分析代碼也是一個不錯的選擇。
對于接下來要寫的一些總結文章,我想做一些約定:
1.寫這個總結,我不能確定時間,也許一天總結一章,也許幾天總結一章,但是我不會斷的,鑒于書上有35章,我估計最快得兩個月的時間,最慢應該會花3個月在暑期之前搞定。(因為我還得準備考研,悲催啊。。。)
2.對于后序總結,我不會照搬書上去寫,我會盡量少寫書上已有的一些原話,我要寫的會是強調一些重點,以及書上講解過少的地方,我會找資料補充起來。
3.當然事情不是絕對的,對一些很重要的概念,算法等,我會根據書上及其他資料再寫一遍。
4.對于一些書上內容,當我借鑒時,我可能會直接從英文版的chm書上直接copy或者截圖(比如有特殊符號)。
5.因為我只看過部分章節,還有一些章節我也是小白,所以肯定會有一些地方自己也迷糊,大家發現我哪些地方講的不足,如果發現網上有講的詳細的,可以把網址通過留言告訴我,謝謝。
6.如果我文章里的哪些文字傷害了你的心靈,你可以留言告訴我,我會盡力去安慰你。(呵呵,開個玩笑),大家和諧討論,和諧學習,和諧共處
7.以后有需要補充的約定我還會再添加。。。
接下來,就讓我們大家一起來學習《算法導論》吧!
再廢話一句:
推薦論壇:C++奮斗樂園(C/C++/算法):http://www.cppleyuan.com/
推薦QQ群:119154131(里面有一些高校的牛。。。加群請填寫ACM驗證)
http://www.wutianqi.com/?p=2298
posted @
2011-04-09 12:13 Tanky Woo 閱讀(3227) |
評論 (9) |
編輯 收藏
接上一篇:
最短路徑算法—Bellman-Ford(貝爾曼-福特)算法分析與實現(C/C++) Dijkstra(迪杰斯特拉)算法是典型的最短路徑路由算法,用于計算一個節點到其他所有節點的最短路徑。主要特點是以起始點為中心向外層層擴展,直到擴展到終點為止。
Dijkstra算法能得出最短路徑的最優解,但由于它遍歷計算的節點很多,所以效率低。
Dijkstra算法是很有代表性的最短路算法,在很多專業課程中都作為基本內容有詳細的介紹,如數據結構,圖論,運籌學等等。
其基本思想是,設置頂點集合S并不斷地作貪心選擇來擴充這個集合。一個頂點屬于集合S當且僅當從源到該頂點的最短路徑長度已知。
初始時,S中僅含有源。設u是G的某一個頂點,把從源到u且中間只經過S中頂點的路稱為從源到u的特殊路徑,并用數組dist記錄當前每個頂點所對應的最短特殊路徑長度。Dijkstra算法每次從V-S中取出具有最短特殊路長度的頂點u,將u添加到S中,同時對數組dist作必要的修改。一旦S包含了所有V中頂點,dist就記錄了從源到所有其它頂點之間的最短路徑長度。
例如,對下圖中的有向圖,應用Dijkstra算法計算從源頂點1到其它頂點間最短路徑的過程列在下表中。

Dijkstra算法的迭代過程:

主題好好理解上圖!
以下是具體的實現(C/C++):
/***************************************
* About: 有向圖的Dijkstra算法實現
* Author: Tanky Woo
* Blog: www.WuTianQi.com
***************************************/
#include <iostream>
using namespace std;
const int maxnum = 100;
const int maxint = 999999;
void Dijkstra(int n, int v, int *dist, int *prev, int c[maxnum][maxnum])
{
bool s[maxnum]; // 判斷是否已存入該點到S集合中
for(int i=1; i<=n; ++i)
{
dist[i] = c[v][i];
s[i] = 0; // 初始都未用過該點
if(dist[i] == maxint)
prev[i] = 0;
else
prev[i] = v;
}
dist[v] = 0;
s[v] = 1;
// 依次將未放入S集合的結點中,取dist[]最小值的結點,放入結合S中
// 一旦S包含了所有V中頂點,dist就記錄了從源點到所有其他頂點之間的最短路徑長度
for(int i=2; i<=n; ++i)
{
int tmp = maxint;
int u = v;
// 找出當前未使用的點j的dist[j]最小值
for(int j=1; j<=n; ++j)
if((!s[j]) && dist[j]<tmp)
{
u = j; // u保存當前鄰接點中距離最小的點的號碼
tmp = dist[j];
}
s[u] = 1; // 表示u點已存入S集合中
// 更新dist
for(int j=1; j<=n; ++j)
if((!s[j]) && c[u][j]<maxint)
{
int newdist = dist[u] + c[u][j];
if(newdist < dist[j])
{
dist[j] = newdist;
prev[j] = u;
}
}
}
}
void searchPath(int *prev,int v, int u)
{
int que[maxnum];
int tot = 1;
que[tot] = u;
tot++;
int tmp = prev[u];
while(tmp != v)
{
que[tot] = tmp;
tot++;
tmp = prev[tmp];
}
que[tot] = v;
for(int i=tot; i>=1; --i)
if(i != 1)
cout << que[i] << " -> ";
else
cout << que[i] << endl;
}
int main()
{
freopen("input.txt", "r", stdin);
// 各數組都從下標1開始
int dist[maxnum]; // 表示當前點到源點的最短路徑長度
int prev[maxnum]; // 記錄當前點的前一個結點
int c[maxnum][maxnum]; // 記錄圖的兩點間路徑長度
int n, line; // 圖的結點數和路徑數
// 輸入結點數
cin >> n;
// 輸入路徑數
cin >> line;
int p, q, len; // 輸入p, q兩點及其路徑長度
// 初始化c[][]為maxint
for(int i=1; i<=n; ++i)
for(int j=1; j<=n; ++j)
c[i][j] = maxint;
for(int i=1; i<=line; ++i)
{
cin >> p >> q >> len;
if(len < c[p][q]) // 有重邊
{
c[p][q] = len; // p指向q
c[q][p] = len; // q指向p,這樣表示無向圖
}
}
for(int i=1; i<=n; ++i)
dist[i] = maxint;
for(int i=1; i<=n; ++i)
{
for(int j=1; j<=n; ++j)
printf("%8d", c[i][j]);
printf("\n");
}
Dijkstra(n, 1, dist, prev, c);
// 最短路徑長度
cout << "源點到最后一個頂點的最短路徑長度: " << dist[n] << endl;
// 路徑
cout << "源點到最后一個頂點的路徑為: ";
searchPath(prev, 1, n);
}
輸入數據:
5
7
1 2 10
1 4 30
1 5 100
2 3 50
3 5 10
4 3 20
4 5 60
輸出數據:
999999 10 999999 30 100
10 999999 50 999999 999999
999999 50 999999 20 10
30 999999 20 999999 60
100 999999 10 60 999999
源點到最后一個頂點的最短路徑長度: 60
源點到最后一個頂點的路徑為: 1 -> 4 -> 3 -> 5
最后給出兩道題目練手,都是直接套用模版就OK的:
1.HDOJ 1874 暢通工程續
http://www.wutianqi.com/?p=1894
2.HDOJ 2544 最短路
http://www.wutianqi.com/?p=1892
posted @
2011-01-19 13:06 Tanky Woo 閱讀(22666) |
評論 (7) |
編輯 收藏
相關文章:
1.Dijkstra算法:
http://www.wutianqi.com/?p=1890
2.Floyd算法:
http://www.wutianqi.com/?p=1903
Dijkstra算法是處理單源最短路徑的有效算法,但它局限于邊的權值非負的情況,若圖中出現權值為負的邊,Dijkstra算法就會失效,求出的最短路徑就可能是錯的。這時候,就需要使用其他的算法來求解最短路徑,Bellman-Ford算法就是其中最常用的一個。該算法由美國數學家理查德•貝爾曼(Richard Bellman, 動態規劃的提出者)和小萊斯特•福特(Lester Ford)發明。Bellman-Ford算法的流程如下:
給定圖G(V, E)(其中V、E分別為圖G的頂點集與邊集),源點s,
- 數組Distant[i]記錄從源點s到頂點i的路徑長度,初始化數組Distant[n]為, Distant[s]為0;
-
以下操作循環執行至多n-1次,n為頂點數:
對于每一條邊e(u, v),如果Distant[u] + w(u, v) < Distant[v],則另Distant[v] = Distant[u]+w(u, v)。w(u, v)為邊e(u,v)的權值;
若上述操作沒有對Distant進行更新,說明最短路徑已經查找完畢,或者部分點不可達,跳出循環。否則執行下次循環;
- 為了檢測圖中是否存在負環路,即權值之和小于0的環路。對于每一條邊e(u, v),如果存在Distant[u] + w(u, v) < Distant[v]的邊,則圖中存在負環路,即是說改圖無法求出單源最短路徑。否則數組Distant[n]中記錄的就是源點s到各頂點的最短路徑長度。
可知,Bellman-Ford算法尋找單源最短路徑的時間復雜度為O(V*E).
首先介紹一下松弛計算。如下圖:

松弛計算之前,點B的值是8,但是點A的值加上邊上的權重2,得到5,比點B的值(8)小,所以,點B的值減小為5。這個過程的意義是,找到了一條通向B點更短的路線,且該路線是先經過點A,然后通過權重為2的邊,到達點B。
當然,如果出現一下情況

則不會修改點B的值,因為3+4>6。
Bellman-Ford算法可以大致分為三個部分
第一,初始化所有點。每一個點保存一個值,表示從原點到達這個點的距離,將原點的值設為0,其它的點的值設為無窮大(表示不可達)。
第二,進行循環,循環下標為從1到n-1(n等于圖中點的個數)。在循環內部,遍歷所有的邊,進行松弛計算。
第三,遍歷途中所有的邊(edge(u,v)),判斷是否存在這樣情況:
d(v) > d (u) + w(u,v)
則返回false,表示途中存在從源點可達的權為負的回路。
之所以需要第三部分的原因,是因為,如果存在從源點可達的權為負的回路。則 應為無法收斂而導致不能求出最短路徑。
考慮如下的圖:

經過第一次遍歷后,點B的值變為5,點C的值變為8,這時,注意權重為-10的邊,這條邊的存在,導致點A的值變為-2。(8+ -10=-2)

第二次遍歷后,點B的值變為3,點C變為6,點A變為-4。正是因為有一條負邊在回路中,導致每次遍歷后,各個點的值不斷變小。
在回過來看一下bellman-ford算法的第三部分,遍歷所有邊,檢查是否存在d(v) > d (u) + w(u,v)。因為第二部分循環的次數是定長的,所以如果存在無法收斂的情況,則肯定能夠在第三部分中檢查出來。比如

此時,點A的值為-2,點B的值為5,邊AB的權重為5,5 > -2 + 5. 檢查出來這條邊沒有收斂。
所以,Bellman-Ford算法可以解決圖中有權為負數的邊的單源最短路徑問。
個人感覺算法導論講解很不錯,把這一章貼出來和大家分享:
24.1 The Bellman-Ford algorithm
The Bellman-Ford algorithm solves the single-source shortest-paths problem in the general case in which edge weights may be negative. Given a weighted, directed graph G = (V, E) with source s and weight function w : E → R, the Bellman-Ford algorithm returns a boolean value indicating whether or not there is a negative-weight cycle that is reachable from the source. If there is such a cycle, the algorithm indicates that no solution exists. If there is no such cycle, the algorithm produces the shortest paths and their weights.
The algorithm uses relaxation, progressively decreasing an estimate d[v] on the weight of a shortest path from the source s to each vertex v ∈ V until it achieves the actual shortest-path weight δ(s, v). The algorithm returns TRUE if and only if the graph contains no negative-weight cycles that are reachable from the source.
BELLMAN-FORD(G, w, s)
1 INITIALIZE-SINGLE-SOURCE(G, s)
2 for i ← 1 to |V[G]| - 1
3 do for each edge (u, v) ∈ E[G]
4 do RELAX(u, v, w)
5 for each edge (u, v) ∈ E[G]
6 do if d[v] > d[u] + w(u, v)
7 then return FALSE
8 return TRUE
Figure 24.4 shows the execution of the Bellman-Ford algorithm on a graph with 5 vertices. After initializing the dand π values of all vertices in line 1, the algorithm makes |V| – 1 passes over the edges of the graph. Each pass is one iteration of the for loop of lines 2-4 and consists of relaxing each edge of the graph once. Figures 24.4(b)-(e) show the state of the algorithm after each of the four passes over the edges. After making |V|- 1 passes, lines 5-8 check for a negative-weight cycle and return the appropriate boolean value. (We’ll see a little later why this check works.)
(單擊圖片可以放大)

Figure 24.4: The execution of the Bellman-Ford algorithm. The source is vertex s. The d values are shown within the vertices, and shaded edges indicate predecessor values: if edge (u, v) is shaded, then π[v] = u. In this particular example, each pass relaxes the edges in the order (t, x), (t, y), (t, z), (x, t), (y, x), (y, z), (z, x), (z, s), (s, t), (s, y). (a) The situation just before the first pass over the edges. (b)-(e) The situation after each successive pass over the edges. The d and π values in part (e) are the final values. The Bellman-Ford algorithm returns TRUE in this example.
The Bellman-Ford algorithm runs in time O(V E), since the initialization in line 1 takes Θ(V) time, each of the |V| – 1 passes over the edges in lines 2-4 takes Θ(E) time, and the for loop of lines 5-7 takes O(E) time.
以下是Bellman-Ford代碼:
/*
* About: Bellman-Ford算法
* Author: Tanky Woo
* Blog: www.WuTianqi.com
*/
#include <iostream>
using namespace std;
const int maxnum = 100;
const int maxint = 99999;
// 邊,
typedef struct Edge{
int u, v; // 起點,重點
int weight; // 邊的權值
}Edge;
Edge edge[maxnum]; // 保存邊的值
int dist[maxnum]; // 結點到源點最小距離
int nodenum, edgenum, source; // 結點數,邊數,源點
// 初始化圖
void init()
{
// 輸入結點數,邊數,源點
cin >> nodenum >> edgenum >> source;
for(int i=1; i<=nodenum; ++i)
dist[i] = maxint;
dist[source] = 0;
for(int i=1; i<=edgenum; ++i)
{
cin >> edge[i].u >> edge[i].v >> edge[i].weight;
if(edge[i].u == source) //注意這里設置初始情況
dist[edge[i].v] = edge[i].weight;
}
}
// 松弛計算
void relax(int u, int v, int weight)
{
if(dist[v] > dist[u] + weight)
dist[v] = dist[u] + weight;
}
bool Bellman_Ford()
{
for(int i=1; i<=nodenum-1; ++i)
for(int j=1; j<=edgenum; ++j)
relax(edge[j].u, edge[j].v, edge[j].weight);
bool flag = 1;
// 判斷是否有負環路
for(int i=1; i<=edgenum; ++i)
if(dist[edge[i].v] > dist[edge[i].u] + edge[i].weight)
{
flag = 0;
break;
}
return flag;
}
int main()
{
//freopen("input3.txt", "r", stdin);
init();
if(Bellman_Ford())
for(int i = 1 ;i <= nodenum; i++)
cout << dist[i] << endl;
return 0;
}
posted @
2011-01-17 20:48 Tanky Woo 閱讀(4579) |
評論 (2) |
編輯 收藏
已出連載:
1.《隨機化算法(1) — 隨機數》
2.《隨機化算法(2) — 數值概率算法》
3.《隨機化算法(3) — 舍伍德(Sherwood)算法》
正文:
悟性不夠,這一章看代碼看了快一個上午,才理解。
上一章講過《概率算法(3) — 舍伍德(Sherwood)算法》,但是他的有點是計算時間復雜性對所有實例而言相對均勻,而其平均時間復雜性沒有改變。而拉斯維加斯算法怎么顯著改進了算法的有效性。
拉斯維加斯算法的一個顯著特征是它所作的隨機性決策有可能導致算法找不到所需的解。因此通常用一個bool型函數表示拉斯維加斯算法。
void Obstinate(InputType x, OutputType &y)
{
// 反復調用拉斯維加斯算法LV(x, y),直到找到問題的一個解
bool success= false;
while (!success)
success = LV(x,y);
}
考慮用拉斯維加斯算法解決N皇后問題:
對于n后問題的任何一個解而言,每一個皇后在棋盤上的位置無任何規律,不具有系統性,而更象是隨機放置的。由此容易想到下面的拉斯維加斯算法。
在棋盤上相繼的各行中隨機地放置皇后,并注意使新放置的皇后與已放置的皇后互不攻擊,直至n個皇后已相容地放置好,或已沒有下一個皇后的可放置位置時為止。注意這里解決的是找到其中一個方法,求不是求出N皇后的全部解。
這里提前說明一下,否則不好理解。
接下來的這個用Las Vegas算法解決N皇后問題,我們采用的是隨機放置位置策略和回溯法相結合,具體就是比如八皇后中,前幾行選擇用隨機法放置皇后,剩下的選擇用回溯法解決。
這個程序不是很好理解,有的地方我特別說明了是理解程序的關鍵,大家看時一定要認真了,另外,王曉東的書上先是用單純的隨機法解決,大家可以先去理解書上這個例子。然后再來分析我這個程序。不過那本書上關于這一塊錯誤比較多,大家看時要注意哪些地方他寫錯了。
拉斯維加斯算法解決N皇后代碼:
依然用到了RandomNumber.h頭文件,大家可以去看下第一章,我就不貼出來了。
剩下部分的代碼:
注意QueensLV()函數是這個程序的精髓所在。
Queen.h頭文件
#ifndef QUEEN_H
#define QUEEN_H
class Queen
{
friend bool nQueen(int);
private:
bool Place(int k); // 測試皇后k置于第x[k]列的合法性
bool Backtrack(int t); // 解n后問題的回溯法
bool QueensLV(int stopVegas); // 隨機放置n個皇后拉斯維加斯算法
int n, *x, *y;
};
#endif
Queen.cpp文件
#include <iostream>
#include "Queen.h"
#include "RandomNumber.h"
using namespace std;
bool Queen::Place(int k)
{
// 測試皇后k置于第x[k]列的合法性
for(int j = 1; j < k; ++ j)
if((abs(k-j) == abs(x[j]-x[k])) || (x[j]==x[k]))
return false;
return true;
}
bool Queen::Backtrack(int t)
{
// 解n后問題的回溯法
if(t > n)
{
for(int i=1; i<=n; ++i)
y[i] = x[i];
return true;
}
else
for(int i=1; i<=n; ++i)
{
x[t] = i;
if(Place(t) && Backtrack(t+1))
return true;
}
return false;
}
/*
* QueensLV是整個Las Vegas解n皇后的精髓。而且這個函數不好理解,我在這里具體分析下。
* k是第k行,x[k]表示第k行的皇后放在x[k]列
* 下面這兩點解析請認真看了,我個人就是卡在這里半天了,但是理解后很簡單。
* 標號①處:這里是遍歷第k行所有可以放置的列號,用y保存下來,并用count記錄有多少個位置可以放置
* 標號②處:這里利用上面保存的可以放置的列,然后隨機取其中一列來放置第k行的皇后。這就是Las Vegas的精髓!
*/
// Author: Tanky Woo
// Blog: www.WuTianQi.com
bool Queen::QueensLV(int stopVegas)
{
// 隨機放置n個皇后的拉斯維加斯算法
RandomNumber rnd; // 隨機數產生器
int k = 1; // 下一個放置的皇后編號
int count = 1;
// 1 <= stopVegas <= n 表示允許隨機放置的皇后數
while((k <= stopVegas) && (count > 0))
{
count = 0;
for(int i = 1; i<=n; ++i) // ----------- ①
{
x[k] = i;
if(Place(k))
y[count++] = i;
}
if(count > 0) // -------------②
x[k++] = y[rnd.Random(count)]; // 隨機位置
}
return (count > 0); // count > 0表示放置位置成功
}
Main.cpp主文件:
/*
* Author: Tanky woo
* Blog: www.WuTianQi.com
* Date: 2010.12.9
* 拉斯維加斯(Las Vegas)算法解決N皇后問題
* 代碼來至王曉東《計算機算法設計與分析》
*/
#include "Queen.h"
#include "RandomNumber.h"
#include <iostream>
using namespace std;
bool nQueen(int n)
{
// 與回溯法結合的解n后問題的拉斯維加斯算法
Queen X;
// 初始化X
X.n = n;
int *p = new int[n+1];
int *q = new int[n+1];
for(int i=0; i<=n; ++i)
{
p[i] = 0;
q[i] = 0;
}
X.y = q;
X.x = p;
// 設置隨機放置皇后的個數
int stop = 8;
if(n > 15)
stop = n-15;
bool found = false;
while(! X.QueensLV(stop));
// 算法的回溯搜索部分
if(X.Backtrack(stop+1))
{
for(int i=1; i<=n; ++i)
cout << p[i] << " ";
found = true;
}
cout << endl;
delete [] p;
delete [] q;
return found;
}
int main()
{
nQueen(8);
}
在8皇后問題中:
1.stopVegas=0表示完全使用回溯法解決問題。因此一定可以得到一組解。
2.stopVegas=8表示完全實用隨機法解決問題。因此一定可以得到一組解。
注意書上說解決8皇后問題時,列出了一個不同stopVegas值時,所對應的算法成功率,在stopVegas=8時,他寫著是0.1293,我個人認為是錯的。接下來我說下自己這么理解的原因:
首先,這個程序為何會造成有失敗的情況,那就是因為在隨機求出前stopVegas行成立時,不代表后面N-stopVegas行用回溯就可以成立,因為這不是一個徹底的回溯。它是從stopVegas+1行開始計算,回溯不成立最后返回時,也只停留在stopVegas。
而我們在完全隨機時,那么它就是反復調用隨機位置放置n個皇后的Las Vegas算法,直至放置成功。所以當stopVegas=8時,他的成功率也應該是100%。
另外在stopVegas=3時,成功率等于0.49,趨近于0.5,大家可以試試,基本上每運行兩次會有一次回來結果。
如果上面我的理解有錯,歡迎大家指出,我的博客(http://www.wutianqi.com/)。
下一篇我會寫《隨機化算法(5) — 蒙特卡羅(Monte Carlo)算法》。
Tanky Woo原創,歡迎轉載,轉載請附上鏈接,請不要私自刪除文章內任何關于本博客的鏈接。
posted @
2010-12-15 14:28 Tanky Woo 閱讀(3254) |
評論 (0) |
編輯 收藏
關于編程的學習,大家肯定都知道,也是大家都說來說去的,就幾句話:
1.多看書。
2.多看代碼。
3.多敲代碼。
這些我不想多說,也覺得沒有多說的必要。
經常在CSDN上看到有人問“我學習C++一段時間了,該如何進階?”,然后接著就是一大堆的人,重復這上面的三句話或者更多,我不是說這些方法是錯的,我只是認為,這樣沒有點到本質,初學者喜歡依賴于書籍,他們看書了,他們也照著書敲了代碼,但是他們就是感覺一直在基礎的層面上打轉,這是為何呢?
在C++里定義復制構造函數時,大家知道,一般對于類中含有指針的,要進行深復制,而不是淺復制。而我在這里也要講一個類似的方法,那就是關于編程的淺學習與深學習的問題。
大家在這里可以先試著想想自己平時是怎么學習編程的?遇到一個新函數、新概念,大家是看書?記住概念?看看代碼?抑或是其他?
我根據個人的理解和經驗,在沒遇到一個新知識時,我把學習這個知識點的深度分為三個層次,依次深入:
①.看了書,看了代碼。
②.在①的基礎上,照著書把代碼敲在電腦里運行了。
③.在②的基礎上,自己根據自己的理解和腦海里的記憶,不看書,把代碼敲在電腦上,并運行。
對于第①個層次,一般會發生在以下情況下:平時沒學習,考前瘋狂的看書,但是沒時間敲代碼,于是把書和代碼都用學習概念的方法—->死記,這樣,直接導致了考時忘光光,考后欲哭無淚。
對于第②個層次,大部分人應該都處于這種情況。大家平時學習時,是一種機械化的學習,也就是第②種層次所說的,照著書敲代碼,這樣雖然當時把程序運行出來了,很高興,但是,如果我接著讓你不看書,自己動手再敲一遍,有幾個人可以敲出來?抑或是,我把題目要求改一改,讓你們用這個新學到的方法做,有幾個人可以做出來?
這就是第②種層次的弊病,網上很多人都建議,自己動手把代碼敲在電腦上,但是我相信,他們的本意是讓大家不看書,把代碼敲上去,而不是只是簡單的照著書敲代碼。
對于第①種層次,根本談不上是學習;而第②種層次和第③種層次,就是我在文章標題里所說的淺學習和深學習的區別。
我說了很多,可能有些人覺得是廢話,只需要一兩句就可以說清楚的。本文的目的,只是為了分析淺層次與深層次學習的區別,進而能自己去區別學習層次,雖然一兩句話也可以說清楚,但是卻無法印刻在讀者的腦海里,更無法自己去形成這個概念,也就無法判斷自己的學習是否到位。
最后,我像把文章用幾句話總結一下:
1.學習編程,要完成三個步驟:
①:看書,看代碼;
②:對照著書敲代碼;
③:拋開書本,自己根據自己理解,去敲代碼,或者自己給個題目,然后用新學到的知識去解決;
2.學習編程,如果只做到上面兩個層次,不如不學,把時間留著去打會球,因為這樣根本沒學到知識,當然,不排除有些人記憶力超強。
3.以上學習方法可以運用到其他學習上去。大家自行去理解,尋找一套適合自己的學習方法。
Tanky Woo原創,歡迎轉載,轉載請注明作者信息以及本博客:http://m.shnenglu.com/tanky-woo/
posted @
2010-12-13 16:11 Tanky Woo 閱讀(2596) |
評論 (11) |
編輯 收藏
摘要: 已出連載:
1.《隨機化算法(1) — 隨機數》
2.《隨機化算法(2) — 數值概率算法》
正文:
這一章怎么說呢,我個人感覺不好理解,在網上查了一些資料,沒發現有具體對舍伍德算法的介紹。
迄今為止看的最全面的就是王曉東的《計算機算法設計與分析》里講的了。在網上查的一些資料也基本全和這本書上講的一樣,至于是這本書先寫的,還是其他位置先寫的,我就不做評論了。
(有時間我把那本書上講舍伍...
閱讀全文
posted @
2010-12-13 10:04 Tanky Woo 閱讀(2847) |
評論 (0) |
編輯 收藏
接著上一篇: 隨機化算法(1) — 隨機數
在這章開篇推薦下chinazhangjie總結的隨機算法,因為咱兩看的是同一本書,所以大家也可以去參考下他的,總結的很不錯。
http://www.cnblogs.com/chinazhangjie/archive/2010/11/11/1874924.html
(順便再PS一下,小杰也是我論壇的C/C++問題求助板塊的版主,C/C++小牛)
這一章我就把書中的一個例子舉出來了,感覺雖然很簡單,但是很有意思。
用隨機投點法計算Pi值
設有一半徑為r的圓及其外切四邊形。向該正方形隨機地投擲n個點。設落入圓內的點數為k。由于所投入的點在正方形上均勻分布,因而所投入的點落入圓內的概率為(Pi*r*r)/(4*r*r)= Pi/4 。所以當n足夠大時,k與n之比就逼近這一概率。從而,PI 約等于 (4*k)/n.
如下圖:
因為代碼里用到了上一章《概率算法(1) — 隨機數》里的RandomNumber類,所以大家可以先把前一章看看。
我把這個偽隨機類再貼一遍:
const unsigned long maxshort = 65535L;
const unsigned long multiplier = 1194211693L;
const unsigned long adder = 12345L;
class RandomNumber{
private:
// 當前種子
unsigned long randSeed;
public:
// 構造函數,默認值0表示由系統自動產生種子
RandomNumber(unsigned long s = 0);
// 產生0 ~ n-1之間的隨機整數
unsigned short Random(unsigned long n);
// 產生[0, 1) 之間的隨機實數
double fRandom();
};
// 產生種子
RandomNumber::RandomNumber(unsigned long s)
{
if(s == 0)
randSeed = time(0); //用系統時間產生種子
else
randSeed = s;
}
// 產生0 ~ n-1 之間的隨機整數
unsigned short RandomNumber::Random(unsigned long n)
{
randSeed = multiplier * randSeed + adder;
return (unsigned short)((randSeed >> 16) % n);
}
// 產生[0, 1)之間的隨機實數
double RandomNumber::fRandom()
{
return Random(maxshort) / double(maxshort);
}
主文件Main:
/*
* Author: Tanky woo
* Blog: www.WuTianQi.com
* Date: 2010.12.8
* 用隨機投點法計算Pi值
* 代碼來至王曉東《計算機算法設計與分析》
*/
#include "RandomNumber.h"
#include <iostream>
#include <iomanip>
#include <time.h>
using namespace std;
double Darts(long n)
{
// 用隨機投點法計算Pi值
static RandomNumber dart;
long k = 0;
for(long i=1; i<=n; ++i)
{
double x = dart.fRandom();
double y = dart.fRandom();
// 在圓內
if((x*x+y*y) <= 1)
++k;
}
return 4 * k / double(n);
}
int main()
{
// 當進行1,000次投點時
cout << Darts(1000) << endl;
// 當進行10,000次投點時
cout << Darts(10000) << endl;
// 當進行100,000次投點時
cout << Darts(100000) << endl;
// 當進行1,000,000次投點時
cout << Darts(1000000) << endl;
// 當進行10,000,000次投點時
cout << Darts(10000000) << endl;
// 當進行100,000,000次投點時
cout << Darts(100000000) << endl;
return 0;
}
通過代碼可以看出,隨機投點越多,則越接近與3.1415926…
這個和拋硬幣時求硬幣正反面概率類似,實驗次數越多,則越接近于理論值。
下一章是《隨機化算法(3) — 舍伍德(Sherwood)算法》
Tanky Woo原創,歡迎轉載,轉載請附上鏈接,請不要私自刪除文章內任何關于本博客的鏈接。
posted @
2010-12-12 11:51 Tanky Woo 閱讀(2476) |
評論 (1) |
編輯 收藏
最近在看王曉東的《計算機算法設計與分析(第3版) 》,感覺講的挺不錯的。這里先推薦下。
接下來的幾章(包括本章),我準備以連載的方式講出來,主要用到的資料是上面推薦的那本書以及《算法導論》和網上的資源,內容是概率分析與隨機算法。文章內大部分內容出自書中,我僅以匯總形式以及個人理解加以補充。如有紕漏,歡迎指出。
概率算法的一個基本特征是對所求解問題的同一實例用同一概率算法求解兩次可能得到完全不同的效果。這兩次求解問題所需的時間甚至所得到的結果可能會有相當大的差別。一般情況下,可將概率算法大致分為四類:數值概率算法,蒙特卡羅(Monte Carlo)算法,拉斯維加斯(Las Vegas)算法和舍伍德(Sherwood)算法。
隨機數在概率算法設計中扮演著十分重要的角色。在現實計算機上無法產生真正的隨機數,因此在概率算法中使用的隨機數都是一定程度上隨機的,即偽隨機數。
產生隨機數最常用的方法是線性同余法。由線性同余法產生的隨機序列a1,a2,…,an滿足
1.a0=d
2.an=(b*an-1+c)mod m (n=1,2…….)
其中,b>0, c>=0, d>=m。d稱為該隨機序列的種子。
一般情況下,取gcd(m, b)=1,因此可取b為一素數。
這是一個隨機數類:
const unsigned long maxshort = 65535L;
const unsigned long multiplier = 1194211693L;
const unsigned long adder = 12345L;
class RandomNumber{
private:
// 當前種子
unsigned long randSeed;
public:
// 構造函數,默認值0表示由系統自動產生種子
RandomNumber(unsigned long s = 0);
// 產生0 ~ n-1之間的隨機整數
unsigned short Random(unsigned long n);
// 產生[0, 1) 之間的隨機實數
double fRandom();
};
// 產生種子
RandomNumber::RandomNumber(unsigned long s)
{
if(s == 0)
randSeed = time(0); //用系統時間產生種子
else
randSeed = s;
}
// 產生0 ~ n-1 之間的隨機整數
unsigned short RandomNumber::Random(unsigned long n)
{
randSeed = multiplier * randSeed + adder;
return (unsigned short)((randSeed >> 16) % n);
}
// 產生[0, 1)之間的隨機實數
double RandomNumber::fRandom()
{
return Random(maxshort) / double(maxshort);
}
利用這個隨機數類,寫一個程序,模擬拋硬幣的實驗。
拋10次硬幣構成一個事件,每次事件記錄得到正面的個數。反復模擬這個事件50,000次,然后對這50,000L次進行輸出頻率圖,比較每次事件得到正面次數的比例。
以下是總的代碼:
頭文件 RandomNumber.h:
// RandomNumber.h
const unsigned long maxshort = 65535L;
const unsigned long multiplier = 1194211693L;
const unsigned long adder = 12345L;
#ifndef RANDOMNUMBER_H
#define RANDOMNUMBER_H
class RandomNumber{
private:
// 當前種子
unsigned long randSeed;
public:
// 構造函數,默認值0表示由系統自動產生種子
RandomNumber(unsigned long s = 0);
// 產生0 ~ n-1之間的隨機整數
unsigned short Random(unsigned long n);
// 產生[0, 1) 之間的隨機實數
double fRandom();
};
#endif
類實現文件RandomNumber.cpp :
// RandomNumber.cpp
#include "RandomNumber.h"
#include <iostream>
#include <stdlib.h>
#include <time.h>
using namespace std;
// 產生種子
RandomNumber::RandomNumber(unsigned long s)
{
if(s == 0)
randSeed = time(0); //用系統時間產生種子
else
randSeed = s;
}
// 產生0 ~ n-1 之間的隨機整數
unsigned short RandomNumber::Random(unsigned long n)
{
randSeed = multiplier * randSeed + adder;
return (unsigned short)((randSeed >> 16) % n);
}
// 產生[0, 1)之間的隨機實數
double RandomNumber::fRandom()
{
return Random(maxshort) / double(maxshort);
}
主文件Main :
// 主文件main
/*
* Author: Tanky woo
* Blog: www.WuTianQi.com
* Date: 2010.12.7
* 代碼來至王曉東《計算機算法設計與分析》
*/
#include "RandomNumber.h"
#include <iostream>
#include <iomanip>
#include <time.h>
using namespace std;
int TossCoins(int numberCoins)
{
// 隨機拋硬幣
static RandomNumber coinToss;
int i, tosses = 0;
for(i = 0; i < numberCoins; ++i)
tosses += coinToss.Random(2);
return tosses;
}
int main()
{
// 模擬隨機拋硬幣事件
const int NCOINS = 10;
const long NTOSSES = 50000L;
// heads[i]得到的i次正面的次數
long i, heads[NCOINS+1];
int j, position;
// 初始化數組heads
for(j = 0; j < NCOINS+1; ++j)
heads[j] = 0;
// 重復50,000次模擬事件
for(i = 0; i < NTOSSES; ++i)
heads[TossCoins(NCOINS)] ++;
// 輸出頻率圖
for(i = 0; i <= NCOINS; ++i)
{
position = int (float(heads[i]) / NTOSSES*72);
cout << setw(6) << i << " ";
for(j = 0; j<position-1; ++j)
cout << " ";
cout << "*" << endl;
}
return 0;
}
輸出頻率圖:

具體可以看看王曉東的《計算機算法設計與分析》第七章。
下一篇我會寫《隨機化算法(2) — 數值概率算法》。
Tanky Woo原創,歡迎轉載,轉載請附上鏈接,請不要私自刪除文章內任何關于本博客的鏈接。
posted @
2010-12-10 08:01 Tanky Woo 閱讀(9463) |
評論 (2) |
編輯 收藏
雖然這個問題已經在網上被討論遍了,但是最近從新拾起算法,感覺有必要夯實一下基礎。
棋盤覆蓋問題:
首先大致描述一下題目:
在一個2^k×2^k個方格組成的棋盤中,若有一個方格與其他方格不同,則稱該方格為一特殊方格,且稱該棋盤為一個特殊棋盤.顯然特殊方格在棋盤上出現的位置有4^k種情形.因而對任何
k≥0,有4^k種不同的特殊棋盤.
下圖–圖(1)中的特殊棋盤是當k=2時16個特殊棋盤中的一個:

圖(1)
題目要求在棋盤覆蓋問題中,要用下圖—圖(2)所示的4種不同形態的L型骨牌覆蓋一個給定的特殊棋盤上除特殊方格以外的所有方格,且任何2個L型骨牌不得重疊覆蓋.
圖(2)
思路分析:
當k>0時,將2^k×2^k棋盤分割為4個2^k-1×2^k-1子棋盤,如下圖–圖(3)所示:

圖(3)
特殊方格必位于4個較小子棋盤之一中,其余3個子棋盤中無特殊方格.為了將這3個無特殊方格的子棋盤轉化為特殊棋盤,可以用一個L型骨牌覆蓋這3個較小棋盤的會合處。
如下圖–圖(4)所示,這3個子棋盤上被L型骨牌覆蓋的方格就成為該棋盤上的特殊方格,從而原問題轉化為4個較小規模的棋盤覆蓋問題.遞歸地使用這種分割,直至棋盤簡化為1×1棋盤。

以下是代碼:
/*
Author: Tanky Woo
Blog: www.WuTianQi.com
棋盤覆蓋問題
分治法
2010-12-3
*/
#include <iostream>
using namespace std;
const int N = 11;
int Board[N][N];
int tile = 0;
/*
tr:棋盤左上角方格的行號
tc:棋盤左上角方格的列號
dr:特殊方格所在的行號
dc:特殊方格所在的列號
size:方形棋盤的邊長
*/
void ChessBoard(int tr, int tc, int dr, int dc, int size)
{
if(size == 1)
return;
int t = ++tile, s = size/2;
//覆蓋左上角子棋盤
if(dr<tr+s && dc<tc+s)
//特殊方格在此棋盤中
ChessBoard(tr, tc, dr, dc, s);
else // 此棋盤無特殊方格
{
// 用t號L型骨型牌覆蓋右下角
Board[tr+s-1][tc+s-1] = t;
// 覆蓋其余方格
ChessBoard(tr, tc, tr+s-1, tc+s-1, s);
}
//覆蓋右上角子棋盤
if(dr<tr+s && dc>=tc+s)
ChessBoard(tr, tc+s, dr, dc, s);
else
{
Board[tr+s-1][tc+s] = t;
ChessBoard(tr, tc+s, tr+s-1, tc+s, s);
}
//覆蓋左下角子棋盤
if(dr>=tr+s && dc<tc+s)
ChessBoard(tr+s, tc, dr, dc, s);
else
{
Board[tr+s][tc+s-1] = t;
ChessBoard(tr+s, tc, tr+s, tc+s-1, s);
}
//覆蓋右下角子棋盤
if(dr>=tr+s && dc>=tc+s)
ChessBoard(tr+s, tc+s, dr, dc, s);
else
{
Board[tr+s][tc+s] = t;
ChessBoard(tr+s, tc+s, tr+s, tc+s, s);
}
}
void DisplayBoard(int size)
{
for(int i=1; i<=size; ++i)
{
for(int j=1; j<=size; ++j)
printf("%2d ", Board[i][j]);
printf("\n");
}
}
int main()
{
ChessBoard(1, 1, 1, 2, 4);
DisplayBoard(4);
return 0;
}
posted @
2010-12-08 10:23 Tanky Woo 閱讀(3927) |
評論 (7) |
編輯 收藏