這次的軟件架構作業要求是分別使用管道/過濾器風格,調用/返回風格,回溯法,和黑板風格四種方法實現N皇后問題,並比較這四種實現方案的性能
那麼話不多說,就開始我們的介紹吧
N皇后問題描述
N皇后問題描述的是如何將n個皇后放置在n*n的棋盤上,並使n個皇后不會互相攻擊,即他們不能同行,不能同列,也不能位於同一條對角線上。我們要做的,就是在給定N的情況下算出有多少種滿足情況的棋局。這道題是一個非常經典的問題了,但是我們在這裏並不着重於解釋N皇后問題的思路,而是用這個例子去深入研究不用的架構風格
分別使用不同的架構風格實現N皇后問題
管道/過濾器風格
首先讓我們來看看什麼是管道過濾器風格
管道過濾器風格管道/過濾器風格是指在程序中的一個處理流程中,過程元素的輸出是下一個過程元素的輸入,連續的過程元素中使用字節或者位的緩衝量作爲連接兩個過程元素的中間數據流。
要注意的是,每個過濾器(也就是上面提到的過程元素)是獨立的,不能與其他的過濾器共享數據,同時這個過濾器也不知道其他過濾器的存在,也就是他們之間是解耦的。一個管道過濾器網絡輸出的正確性並不依賴於過濾器進行增量計算過程的順序。
總結一下就是管道過濾器風格有以下幾個特點:
- 它是由數據控制計算的
- 系統結構由數據在處理之間有序移動
- 數據流結構的系統有明顯的結構
- 進程之間除了數據交換,沒有任何其他的交互
分析與設計
講完了管道/過濾器風格,接下來讓我們結合N皇后問題進行分析:
通過N皇后的問題描述中我們可以知道,對於一個棋局棋局,要滿足N皇后問題的要求,那麼這個棋盤需要同時滿足如下5個條件:
a. 恰好有N個皇后
b. 對於該棋盤中的每一列,有且僅有一個皇后
c. 對於該棋盤中的每一行,有且僅有一個皇后
d. 對於該棋盤中的每一條主對角線,有且僅有一個皇后
e. 對於該棋盤中的每一條副對角線,有且僅有一個皇后
如果有其中一個條件不符合,那麼這個棋局就不滿足問題的要求。
根據上面對問題的分析,我們可以知道,N皇后問題實際上可以認爲是5個條件的判斷,5個條件的判斷是互相不干擾的,只有同時滿足了5個條件才能滿足N皇后問題的要求
那麼現在,按照管道/過濾器的術語來描述需求:
給定四個條件的過濾器,遍歷一個含有N個皇后的棋局,將同時符合5個條件(也就是5個過濾器)的所有棋局過濾出來
這幅圖描述了管道/過濾器使用,其中箭頭代表數據流的流向,圓圈代表不同的進程/方法。
在實現的時候我們需要對於N*N的棋盤生成所有的情況(相當於全排列),然後對於每一個棋局進行逐一判斷
實現與優化
類圖如下,使用startuml進行繪製
其中四種Filter(AmountFilter,ColumnFilter,LineFilter,MainDagonalFilter,ParaDiagonalFilter)繼承父類Filter,代表一個判斷的條件。
過濾器的代碼如下,使用java實現:
public class Filter {
boolean judge(boolean[][] c) {
return false;
}
}
class AmountFilter extends Filter{
public boolean judge(boolean[][] c) {
int len = c.length;
int amount = 0;
for(int i = 0;i < len; i++) //計算皇后總數
for(int j = 0;j < len ;j++)
if(c[i][j] == true)
amount++;
if(amount == len) return true;
else return false;
}
}
class ColumnFilter extends Filter{
public boolean judge(boolean[][] c) {
int len = c.length;
boolean flag = false;//用來記錄第i行是否已經有一個皇后了
for(int i = 0;i < len; i++) {
flag = false;
for(int j = 0; j < len;j++) {
if(flag == false && c[j][i] == true)
flag = true;
else if(flag == true && c[j][i] == true)
return false;//說明一列有兩個皇后
}
}
return true;
}
}
class LineFilter extends Filter{
public boolean judge(boolean[][] c) {
int len = c.length;
boolean flag = false;//用來記錄第i行是否已經有一個皇后了
for(int i = 0;i < len; i++) {
flag = false;
for(int j = 0; j < len;j++) {
if(flag == false && c[i][j] == true)
flag = true;
else if(flag == true && c[i][j] == true)
return false;//說明一行有兩個皇后
}
}
return true;
}
}
class MainDagonalFilter extends Filter{
public boolean judge(boolean[][] c) {
int len = c.length;
boolean flag = false;//用來記錄第i行是否已經有一個皇后了
for(int i = 0;i < len;i++) {
flag = false;
for(int j = 0;j + i < len; j++) {//以第0行第i列爲起點的對角線··
if(c[j][j + i]== true) {
if(flag == false)
flag = true;
else
return false;
}
}
flag = false;
for(int j = 0;j + i < len; j++) {//以第0列第i行爲起點的對角線··
if(c[j + i][j]== true) {
if(flag == false)
flag = true;
else
return false;
}
}
}
return true;
}
}
class ParaDiagonalFilter extends Filter{
public boolean judge(boolean[][] c) {//不一定對...
int len = c.length;
boolean flag = false;//用來記錄第i行是否已經有一個皇后了
for(int i = 0;i < len;i++) {
flag = false;
for(int j = 0;i - j >= 0; j++) {//以第0行第i列爲起點的對角線
if(c[j][i - j]== true) {
if(flag == false)
flag = true;
else
return false;
}
}
flag = false;
for(int j = i; (i + len - 1) - j >= 0 && j < len; j++) {//以第len - 1列第i行爲起點的對角線
if(c[(i + len - 1) - j][j]== true) {
if(flag == false)
flag = true;
else
return false;
}
}
}
return true;
}
}
調用/返回風格
用一張圖解釋調用/返回風格
調用/返回風格,其實就是一種分而治之的思想,它有以下幾個特點
- 它將問題逐步分解成子問題/子模塊,用過程調用作爲交互的機制
- 它是基於單線程控制的
- 子程序的正確性取決於他調用的子程序的正確性。換而言之,如果一個子程序調用的程序是錯誤的,那麼他自己也不能獲得正確的答案
分析與設計
可以參照網上的很多題解,這裏就不深入闡述了
實現與優化
這裏參照的是網上的位運算版本,使用java實現
public class CallReturnSolution extends Solutions{
private int upperlimit;
public CallReturnSolution(int n) {
super(n);
upperlimit = (1 << n) - 1;
cal(0,0,0);
}
private void cal(int r,int ld,int rd) {
//System.out.println(amount);
if (r != upperlimit)
{
int pos = upperlimit & (~(r | ld | rd) );
while (pos != 0) {
int p = pos & (~pos + 1);
pos -= p;
cal(r | p, (ld | p) << 1, (rd | p) >> 1);
}
}
else//找到答案了
{
amount++;
}
}
}
回溯法
分析與設計
前面的管道/過濾器風格相當於是把所有的棋局情況列舉出來,然後再使用不同的過濾器過濾出符合情況的棋局。
這種方法雖然可以解決問題,但是在過濾器過濾的過程中實際上重複計算了很多次,效率很低,因此我們需要考慮用其他的方法來高效進行判斷。使用回溯法,在生成棋局的過程中就同時進行約束條件的判斷,能大大加速程序的速度
在回溯法中我們不需要生成所有的棋局,而是每次給棋局加一些限制最終的到合格的棋局。
對於一個棋局來說,我們每次在某個位置生成一個皇后,重複N次這樣的過程。在每次生成皇后之後,我們都需要增加限制,即當在棋盤上放置了一個皇后之後,立刻排除這個皇后所在的行,列,主對角線和副對角線。
實現與優化
public class BackTracSolution extends Solutions{
boolean[] column;
boolean[] posdiagonal;
boolean[] negdiagonal;
int[] queen;//用來記錄當前棧中 皇后的位置
public BackTracSolution(int n) {
super(n);
cal();
}
private void cal() {
column= new boolean[n];
posdiagonal = new boolean[2 * n];//前n個爲正對角線 後n個爲反對角線;
negdiagonal= new boolean[2 * n];
queen = new int[n];//用來記錄當前棧中皇后的位置
find(0);//現在進行回溯
}
void find(int row) {//一行一行的放置皇后
for(int i = 0;i < n;i++) {//遍歷每一列 看看能不能放皇后
if(this.column[i] == false
&& this.posdiagonal[row + i] == false
&& this.negdiagonal[row - i + n - 1] == false) {//列 對角線不衝突
//記錄一些信息
put(row,i);
if(row == this.n - 1)//判斷是否爲放置的最後一個
{
manageOutput();//增加方案
this.amount++;
}
else
find(row + 1);
move(row,i);//把當前的皇后移走! 回溯的重點
}
else
continue;
}
}
void move(int row,int column) {
//設置 column 和diagnal
this.queen[row] = -1;//重置
this.column[column] = false;
this.posdiagonal[row + column] = false;//設置爲false就是代表沒有限制
this.negdiagonal[row - column + n - 1] = false;
}
void put(int row,int column) {
this.queen[row] = column;//row行column列放置着皇后
this.column[column] = true;
this.posdiagonal[row + column] = true;//設置爲false就是代表沒有限制
this.negdiagonal[row - column + n - 1] = true;
}
}
黑板風格
關於黑板風格的闡述:
黑板風格是一種共享數據風格的架構,最典型的例子就是數據庫。這裏理解的話可以聯想現實中的黑板。黑板是面向所有人的,然後在某些時刻老師或學生會在黑板上寫東西,這個時候黑板下面的所有人都能看到更新的內容(也就是他對於所有人是可讀的)
分析與設計
黑板風格的關鍵就是其中有一個公共的黑板,黑板中是我們關心的數據結構,所有線程都可以讀或者寫黑板,不過爲了保證數據的一致性,在實現的時候應該保證同一時刻只有一個線程在寫黑板,對讀黑板沒有限制。使用黑板風格實現N皇后的問題,關鍵在於如何定義黑板。
實現與優化
我們定義了黑板結構的類NQueenSpace,其中定義了write(),read(),take()方法。分別代表寫黑板,讀黑板,擦黑板.要注意的是這裏一次只能由一個線程寫或者擦黑板
import java.util.ArrayList;
public class NQueenSpace {
ArrayList<int[]> queens;//黑板包括了所有可行的方案以及當前正在討論的方案
public NQueenSpace(int n) {//初始化 將一個空的棋局寫入黑板
queens = new ArrayList<int[]>();
int [] temp = new int[n];
for (int i = 0; i < n; i++) // 初始化數組
temp[i] = 0;
write(temp);
}
public void write(int[] a) {
queens.add(a);
}
public int[] read() {
return queens.get(queens.size() - 1);//返回黑板的最後一個元素
}
public int[] take() {//返回最後一個元素,並將他從黑板中擦除
int[] temp = queens.get(queens.size() - 1);
queens.remove(temp);
return temp;
}
}
黑板解決方案的代碼:
public class BackTracSolution extends Solutions{
boolean[] column;
boolean[] posdiagonal;
boolean[] negdiagonal;
int[] queen;//用來記錄當前棧中 皇后的位置
public BackTracSolution(int n) {
super(n);
cal();
}
private void cal() {
column= new boolean[n];
posdiagonal = new boolean[2 * n];//前n個爲正對角線 後n個爲反對角線;
negdiagonal= new boolean[2 * n];
queen = new int[n];//用來記錄當前棧中皇后的位置
find(0);//現在進行回溯
}
void find(int row) {//一行一行的放置皇后
for(int i = 0;i < n;i++) {//遍歷每一列 看看能不能放皇后
if(this.column[i] == false
&& this.posdiagonal[row + i] == false
&& this.negdiagonal[row - i + n - 1] == false) {//列 對角線不衝突
//記錄一些信息
put(row,i);
if(row == this.n - 1)//判斷是否爲放置的最後一個
{
manageOutput();//增加方案
this.amount++;
}
else
find(row + 1);
move(row,i);//把當前的皇后移走! 回溯的重點
}
else
continue;
}
}
void move(int row,int column) {
//設置 column 和diagnal
this.queen[row] = -1;//重置
this.column[column] = false;
this.posdiagonal[row + column] = false;//設置爲false就是代表沒有限制
this.negdiagonal[row - column + n - 1] = false;
}
void put(int row,int column) {
this.queen[row] = column;//row行column列放置着皇后
this.column[column] = true;
this.posdiagonal[row + column] = true;//設置爲false就是代表沒有限制
this.negdiagonal[row - column + n - 1] = true;
}
}
四種風格的綜合類圖
實現方案性能對比
說了這四種風格,接下來讓我們看看四種風格的性能對比,單位爲毫秒
其中可以看到回溯法和調用/返回方案的時間隨着皇后數目變大,計算所花的時間增長較緩慢,耗費的時間比較少。
而黑板風格計算所花費的時間隨着皇后數目變大而變大得比較明顯,主要原因是黑板風格中線程頻繁的切換造成了比較大的時間開銷。
過濾器風格中時間開銷的變化最大,這主要是因爲過濾器風格的實質是全排列,對於全排列中的每一種棋盤進行判斷,消耗時間非常大。