这是一个理论问题。在编程任务中,我们被告知要实施John Conway的生命游戏。作为一项额外的任务,我们已被要求修改程序,以便它可以检测多达四代的模式重复。例如,程序应该表现得像这样,给予游戏特定的“种子”:
--------
| |
| OOO |
| |
| |
| |
--------
--------
| 0 |
| 1 |
| 0 |
| |
| |
--------
--------
| |
| O2O |
| |
| |
| |
--------
Repetition detected (2): exiting
表示程序重复,并且该时间段长达2代。
我的问题是这个。是否有可能真正知道程序何时只是一遍又一遍地重复相同的模式?我听说过“停机问题”。这与此有关吗?
现在,如果确实有可能,那么教师似乎在运行的程序似乎能够在重复一次后才能检测到它?其次,期望基础编程课程的学生编写一个能够检测生命游戏中重复模式的程序是否合理?我有一种感觉,他们的意思是“修改你的程序,当在4代窗口中达到两次相同的状态时退出”,这在我看来完全不同于检测模式是否会真正重复自己
修改
以下是规范说明的内容: 您将修改程序以检测先前模式的重复。您的程序应该能够检测周期最多为四代的重复模式。当发现这样的重复时,程序应该以不同的消息退出:
Period detected (4): exiting
替换“已完成”消息,并用括号中的数字表示的句点长度。退出前应打印重复的图案。
答案 0 :(得分:3)
是否有可能真正知道程序何时只是一遍又一遍地重复相同的模式?
康威的生命游戏是100%确定性的,这意味着无论何时遇到模式,你总能确切地知道模式的下一个演变是什么。除此之外,一代中的给定输入将始终为下一代产生一个特定输出,无论何时接收到该输入。
因此,要找到状态演变的时期,您所要做的就是检测何时/是否出现重复状态;那一刻,你知道你已经找到了一个循环。我将用C ++编写我的示例代码,但任何具有“哈希表”或类似数据结构的语言都可以使用相同的基本算法。
//We're expressly defining a grid as a 50x50 grid.
typedef std::array<std::array<bool, 50>, 50> Conway_Grid;
struct Conway_Hash {
size_t operator()(Conway_Grid const& grid) const {
size_t hash = 0;
for(int i = 0; i < grid.size(); i++) {for(int j = 0; j < grid[i].size(); j++) {
if(grid[i][j])
hash += (i * 50 + j);
//I make no guarantees as to the quality of this hash function...
}}
return hash;
}
};
struct Conway_Equal {
bool operator()(Conway_Grid const& g1, Conway_Grid const& g2) const {
for(int i = 0; i < grid.size(); i++) {for(int j = 0; j < grid[i].size(); j++) {
if(g1[i][j] != g2[i][j])
return false;
}}
return true;
}
};
typedef int Generation;
std::unordered_map<Conway_Grid, Generation, Conway_Hash, Conway_Equal> cache;
Conway_Grid get_next_gen(Conway_Grid const& grid) {
Conway_Grid next{};
for(int i = 1; i < grid.size() - 1; i++) {for(int j = 1; j < grid[i].size() - 1; j++) {
int neighbors = 0;
for(int x = i - 1; x <= i + 1; x++) { for(int y = j - 1; y <= j + 1; y++) {
if(x == i && y == j) continue;
if(grid[x][y]) neighbors++;
}}
if(grid[i][j] && (neighbors == 2 || neighbors == 3))
next[i][j] = true;
else if(!grid[i][j] && (neighbors == 3))
next[i][j] = true;
}}
return next;
}
int main() {
Conway_Grid grid{};//Initialized all to false
grid[20][20] = true;
grid[21][20] = true;
grid[22][20] = true;//Blinker
for(Generation gen = 0; gen < 1'000; gen++) { //We'll search a thousand generations
auto it = cache.find(grid);
if(it != cache.end()) {//"Is the grid already in the cache?"
std::cout << "Period found at generation " << gen;
std::cout << ", which was started on generation " << it->second;
std::cout << ", which means the period length is " << gen - it->second << '.' << std::endl;
break;
}
cache[grid] = gen; //"Inserts the current grid into the cache"
grid = get_next_gen(grid); //"Updates the grid to its next generation"
}
return 0;
}
请注意,此代码实际上适用于任何周期长度,而不仅仅是小于4的长度。在上面的代码中,对于一个方向指示灯(连续三个单元格),我们得到以下结果:
Period found at generation 2, which was started on generation 0, which means the period length is 2.
作为一项完整性检查,我决定进口一台Gosper Glider Gun,以确保它同样有效。
grid[31][21] = true;
grid[29][22] = true;
grid[31][22] = true;
grid[19][23] = true;
grid[20][23] = true;
grid[27][23] = true;
grid[28][23] = true;
grid[41][23] = true;
grid[42][23] = true;
grid[18][24] = true;
grid[22][24] = true;
grid[27][24] = true;
grid[28][24] = true;
grid[41][24] = true;
grid[42][24] = true;
grid[7][25] = true;
grid[8][25] = true;
grid[17][25] = true;
grid[23][25] = true;
grid[27][25] = true;
grid[28][25] = true;
grid[7][26] = true;
grid[8][26] = true;
grid[17][26] = true;
grid[21][26] = true;
grid[23][26] = true;
grid[24][26] = true;
grid[29][26] = true;
grid[31][26] = true;
grid[17][27] = true;
grid[23][27] = true;
grid[31][27] = true;
grid[18][28] = true;
grid[22][28] = true;
grid[19][29] = true;
grid[20][29] = true;
Gosper的滑翔机通常没有一段时间,因为随着时间的推移它会产生无限数量的滑翔机,并且模式永远不会重复。但由于网格是有界的,我们只是擦除网格边框上的单元格,这种模式最终会创建一个重复模式,果然,这个程序找到了它:
Period found at generation 119, which was started on generation 59, which means the period length is 60.
(这是非常好的,因为只有枪的时期应该是60)
请注意,这几乎肯定不是解决此问题的最佳解决方案,因为此解决方案将每个生成的网格保存在内存中,对于较大的网格,这将占用RAM和CPU周期。但这是最简单的解决方案,您可能会找到适合您正在使用的编程语言的类似解决方案。