書名:代碼本色:用編程模擬自然系統
作者:Daniel Shiffman
譯者:周晗彬
ISBN:978-7-115-36947-5
第7章目錄
先掌握用Processing Sketch創建和可視化Wolfram CA模型的方法。
7.3 如何編寫初等細胞自動機
1、數組表示CA
- 你也許會想:“我知道模擬細胞的思路,它有一些屬性(狀態、迭代次數、鄰居細胞和在屏幕上的像素位置)。除此之外,它可能還有一些功能(顯示自身、產生新狀態)……”這樣的思路是正確的
- 但我們不想采用這種方法。在本章的后面,我們會討論面向對象方法在CA模擬上的重要性;但在最開始,本例可以使用更初級的數據結構。畢竟,這個初等CA只是由“0和1”構成的狀態列表,我們可以用一個數組表示CA的一次迭代。
int[] cells = {1,0,1,0,0,0,0,1,0,1,1,1,0,0,0,1,1,1,0,0};
為了繪制這個數組,我們可以根據元素的狀態填充對應的顏色。
for (int i = 0; i < cells.length; i++) { 遍歷每個細胞
if (cell[i] == 0) fill(255);
else fill(0); 根據狀態(0或1)填充細胞顏色
stroke(0);
rect(i*50,0,50,50);
}
2、當前要做的事
?? 現在,我們用數組描述一次迭代(只考慮“當前”的迭代)的細胞狀態,下面還要引入計算下一次迭代的機制。我們先用偽代碼表示當前要做的事。
??對數組中的每個細胞:
- 獲取鄰居的狀態——左右兩邊的細胞和自身的狀態;
- 根據先前設定的規則查詢新的狀態;
- 把細胞的狀態設為新值。
for (int i = 0; i < cells.length; i++) { 對數組中的每個細胞 ......
int left = cell[i-1]; 獲取鄰居的狀態
int middle = cell[i];
int right = cell[i+1];
int newstate = rules(left,middle,right); 根據先前設定的規則查詢新的狀態
cell[i] = newstate; 把細胞的狀態設為新值
}
3、如何處理沒有左右鄰居的邊界細胞?
現在有3種解決方案可供選擇。
- 1.讓邊界細胞的狀態是常量。
這也許是最簡單的方案,我們不需要管任何邊界細胞,只需讓它們保持常量狀態(0或1)。 - 2.邊界環繞。
把CA想象成一張紙條,把紙條兩端相連,變成一個環。如此一來,最左邊的細胞和最右邊細胞就成了鄰居,反過來也是如此。用這種方法我們可創建出無限網格的外形,這或許也是最常用的解決方案。 - 3.邊界細胞有特殊的鄰居和計算規則。
我們可以將邊界細胞區別對待,為其創建特殊的規則:讓它們只有兩個鄰居。在某些情況下,你可能會這么做,但是在本例中,這么做會引入很多額外代碼,收益卻很少
??為了讓代碼盡可能地易讀易理解,我們采用第一種方案:直接略過邊界細胞,讓它們的狀態保持常量。這種方案的實現很簡單,只需要讓循環從下標1開始,并提前一個元素結束:
for (int i = 1; i < cells.length-1; i++) { 忽略第一個和最后一個細胞
int left = cell[i-1];
int middle = cell[i];
int right = cell[i+1];
int newstate = rules(left,middle,right);
cell[i] = newstate;
}
4、使用兩個數組
??上面代碼看起來并沒有錯誤:一旦我們得到新狀態,確實需要將它賦給當前細胞。但在下一次迭代中,你會發現一個嚴重的漏洞。會覆蓋當前代的狀態。
??這個問題的解決方案是:使用兩個數組,一個數組用于保存當前代的狀態,另一個數組用于保存下一代的狀態。
int [] newcells = new int[cells.length]; 用另一個數組保存下一代狀態
for (int i = 1; i < cells.length-1; i++) {
int left = cell[i-1]; 從當前數組獲取細胞狀態
int middle = cell[i];
int right = cell[i+1];
int newstate = rules(left,middle,right);
newcells[i] = newstate; 在新數組中保存新狀態
}
處理完數組的每個元素后,我們就可以丟棄舊的數組,讓它的值等于新數組。
cells = newcells; 新一代狀態變成了當前代狀態
5、rules()函數
- 這個函數的功能是根據鄰居(左邊、自身和右邊的細胞)計算當前細胞的新狀態。它的返回值是一個整數(0或1),有3個參數(3個鄰居)。
- 首先,我們需要建立規則的存儲方式。
我們可以用數組存儲這些規則。
int[] ruleset = {0,1,0,1,1,0,1,0};
然后:
if (a == 1 && b == 1 && c == 1) return ruleset[0];
- 如果左邊、自身和右邊細胞的狀態都為1,函數就返回組合“111”對應的結果,也就是規則數組中的第一個元素。下面,我們用這種方法實現所有可能的組合:
int rules (int a, int b, int c) {
if (a==1 && b == 1 && c == 1) return ruleset[0];
else if (a == 1 && b == 1 && c == 0) return ruleset[1];
else if (a == 1 && b == 0 && c == 1) return ruleset[2];
else if (a == 1 && b == 0 && c == 0) return ruleset[3];
else if (a == 0 && b == 1 && c == 1) return ruleset[4];
else if (a == 0 && b == 1 && c == 0) return ruleset[5];
else if (a == 0 && b == 0 && c == 1) return ruleset[6];
else if (a == 0 && b == 0 && c == 0) return ruleset[7];
return 0; 為了讓函數的定義合法,必須加上這個返回值,雖然我們知道不可能出現不符合這8種情況}
- 我喜歡這種實現方式,因為它把每種鄰居組合都描述清楚了。但這不是一個好方案。
如果CA中的細胞有4種可能的狀態(0~3),這樣就會有64種可能的鄰居狀態組合;
如果有10種可能的狀態,鄰居細胞的狀態組合將達到1000種。我們肯定不想輸入1000行這樣的代碼!
6、另一種解決方案
另一種解決方案有點難以理解,就是把鄰居狀態的組合(3位二進制數)轉換成一個普通整數,并把該值作為規則數組的下標。實現方式如下:
int rules (int a, int b, int c) {
String s = "" + a + b + c; 將3位轉化為字符串
int index = Integer.parseInt(s,2); 第二個參數“2”告訴parseInt()函數要把s當成二進制數
return ruleset[index];
}
7、CA類
class CA {
int[] cells; 我們需要兩個數組,一個用來存放細胞,另一個用來存放規則
int[] ruleset;
CA() {
cells = new int[width];
ruleset = {0,1,0,1,1,0,1,0}; 隨意選取規則90
for (int i = 0; i < cells.length; i++) {
cells[i] = 0;
}
cells[cells.length/2] = 1; 除了中間的細胞以狀態1開始,其余所有細胞都從狀態0開始
}
void generate() {
int[] nextgen = new int[cells.length]; 計算下一代狀態
for (int i = 1; i < cells.length-1; i++) {
int left = cells[i-1];
int me = cells[i];
int right = cells[i+1];
nextgen[i] = rules(left, me, right);
}
cells = nextgen;
}
int rules (int a, int b, int c) { 在規則集中查詢新狀態
String s = "" + a + b + c;
int index = Integer.parseInt(s,2);
return ruleset[index];
}
}