前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家:点击跳转
目录
一,正常扫雷
1,开局
经典开局是30*16的格子,所有格子都是未知的,里面有99个雷
2,操作
正常的扫雷,是串行的,即一个回合只能对一个格子进行操作。
扫雷有4种操作:左键单击打开、右键单击标记,右键单击取消标记、左键双击。
具体来说,左键双击的效果就是,把该格子周围未标记的所有格子都打开。
四种操作对操作位置都有要求,不满足要求的格子是无效操作。
而一个回合有多少个格子的状态发生改变,取决于雷的分布,后台会自动进行递归。
3,自动递归
如果点开一个格子,周围没有雷,则自动执行左键双击。
我这个描述可能和读者在其他地方看到的不太一样,但是是等价的。
4,胜负
只要所有非雷的格子点开即可获胜。
不看标记。
点开任意一个雷所在的格子立刻判负。
二,串行扫雷
1,规则微调
(1)取消右键单击取消标记,改成只要标记错误立刻判负。
(2)取消左键双击功能的操作位置校验,改成只要双击就把该格子以及周围未标记的所有格子都打开。
(3)即使标记了雷的格子,左键单击也会判负。
显然,微调之后只是不允许误操作而已,实际上规则是等价的,玩的难度也是一样的。
微调的好处就是代码实现的难度降低了很多。
2,扫雷代码V1
#include <iostream>
#include <string>
#include <vector>
#include <time.h>
#include <functional>
#include <algorithm>
#include <vector>
#include <queue>
#include <numeric>
#include <map>
#include <set>
#include <stack>
#include <unordered_map>
#include <unordered_set>
#include <array>
#include <functional>
#include <string.h>
#include <math.h>
#include <fstream>
#include <streambuf>
#include <stdio.h>
#include <mutex>
#include <unordered_map>
#include <iostream>
#include <vector>
#include <iomanip>
#include <string>
#include <algorithm>
#include <cmath>
#include <stack>
#include <set>
#include <queue>
#include <math.h>
#include <functional>
#include <limits>
#include <climits>
#include <stdint.h>
#include <windows.h>
using namespace std;
#define LOCAL
int ROW = 16;
int COL = 30;
int BOMB = 99;
enum Nod {
EmptyGrid,
BombGrid
};
enum Status {
Unknow,
Open,
Flag
};
vector<vector<int>>v;//雷分布
vector<vector<int>>vsum;//雷计数
vector<vector<int>>vs;//实时界面
bool isWin;
int hseed;//棋谱中的种子
vector<int>hr, hc, ho;//棋谱中的行、列、操作数
void init(int seed)
{
srand(seed);
v.resize(ROW);
vsum.resize(ROW);
vs.resize(ROW);
for (auto& vi : v) {
vi.resize(COL);
for (auto& x : vi)x = EmptyGrid;
}
for (auto& vi : vsum) {
vi.resize(COL);
for (auto& x : vi)x = 0;
}
for (auto& vi : vs) {
vi.resize(COL);
for (auto& x : vi)x = Unknow;
}
int b = BOMB;
while (b) {
int r = rand() % ROW;
int c = rand() % COL;
if (v[r][c] == BombGrid)continue;
v[r][c] = BombGrid;
b--;
for (int i = max(0,r - 1); i <= min(r + 1,ROW-1); i++) {
for (int j = max(0, c - 1); j <= min(c + 1, COL - 1); j++){
vsum[i][j]++;
}
}
}
isWin = false;
}
//鼠标左键单击,是否阵亡
bool clickLeft(int r, int c)
{
if (v[r][c] == BombGrid) {
return true;
}
if (vs[r][c] == Open)return false;
vs[r][c] = Open;
if (vsum[r][c])return false;
for (int i = max(0, r - 1); i <= min(r + 1, ROW - 1); i++) {
for (int j = max(0, c - 1); j <= min(c + 1, COL - 1); j++) {
clickLeft(i, j);
}
}
return false;
}
//鼠标右键单击,是否阵亡
bool clickRight(int r, int c)
{
vs[r][c] = Flag;
return v[r][c] != BombGrid;
}
//鼠标左键双击,是否阵亡
bool clickDouble(int r, int c)
{
for (int i = max(0, r - 1); i <= min(r + 1, ROW - 1); i++) {
for (int j = max(0, c - 1); j <= min(c + 1, COL - 1); j++) {
if (vs[i][j] != Flag && clickLeft(i, j))return true;
}
}
return false;
}
//1左键单击,2右键单击,3左键双击,4显示棋谱,返回是否结束
bool opt(int r, int c, int optNum)
{
if (optNum >= 1 && optNum <= 3) {
hr.push_back(r);
hc.push_back(c);
ho.push_back(optNum);
}
if (optNum == 1 && clickLeft(r, c))return true;
if (optNum == 2 && clickRight(r, c))return true;
if (optNum == 3 && clickDouble(r, c))return true;
if(optNum==4){
cout << hseed << endl;
for (int i = 0; i < hr.size(); i++) {
cout << hr[i] << " " << hc[i] << " " << ho[i] << " ";
if (i % 5 == 4)cout << endl;
}
Sleep(5000);
return false;
}
for (int i = 0; i < ROW; i++) {
for (int j = 0; j < COL; j++) {
if (vs[i][j] == Unknow && v[i][j]== EmptyGrid) {
return false;
}
}
}
return isWin = true;
}
void show()
{
system("cls");
cout << " 0 1 2 3 4 5 6 7 8 9 10 12 14 16 18 20 ";
cout << "22 24 26 28" << endl;
int flagNum = 0;
for (int i = 0; i < ROW; i++) {
cout << i << " ";
if (i < 10)cout << " ";
for (int j = 0; j < COL; j++) {
if (vs[i][j] == Unknow)cout << "■";
else if (vs[i][j] == Open) {
if (vsum[i][j])cout << vsum[i][j]<<"_";
else cout << "□";
}
else {
cout << "●";
flagNum++;
}
}
cout << endl;
}
cout << "还剩" << BOMB - flagNum << "雷未标记";
cout << endl;
}
void showEnd()
{
cout << " 0 1 2 3 4 5 6 7 8 9 10 12 14 16 18 20 ";
cout << "22 24 26 28" << endl;
for (int i = 0; i < ROW; i++) {
cout << i << " ";
if (i < 10)cout << " ";
for (int j = 0; j < COL; j++) {
if (v[i][j] == BombGrid)cout << "●";
else cout << "□";
}
cout << endl;
}
cout << endl;
opt(0, 0, 4);
}
bool run()
{
show();
cout << "输入行 列 操作数" << endl;
cout << "1左键单击,2右键单击,3左键双击,4显示棋谱" << endl;
int r, c, optNum;
cin >> r >> c >> optNum;
return opt(r, c, optNum);
}
int main()
{
cout << "输入随机数种子" << endl;
cin >> hseed;
init(hseed);
while(!run());
if (isWin)cout << "Win!!!" << endl;
else cout << "Lose..." << endl;
showEnd();
return 0;
}
3,运行效果
4,通关棋谱
我玩的第一个种子就是666777888,因为有棋谱功能,所以可以随时手动添加回溯点,不小心挂了可以恢复,这样很容易就得到一个通关的棋谱。
666777888
15 3 1 13 0 1 15 0 1 6 0 1 6 10 1
6 20 1 6 29 1 15 15 1 8 17 1 2 2 1
1 1 1 1 2 1 1 3 1 6 4 2 7 4 2
8 3 3 2 5 1 5 5 2 5 4 3 5 6 2
6 5 3 7 7 2 6 7 3 7 9 2 6 9 3
4 10 2 3 9 3 5 10 3 4 11 3 2 11 2
2 10 3 1 11 3 6 15 2 5 16 3 3 17 2
4 18 2 4 17 3 2 17 2 2 16 3 0 16 2
0 17 2 1 17 3 2 19 1 5 19 3 3 20 2
3 19 2 2 18 3 0 19 2 0 20 1 3 25 2
3 24 3 7 20 2 6 21 3 7 23 2 7 24 2
6 24 3 7 26 2 7 25 3 8 24 3 8 22 2
7 21 3 9 19 2 8 19 3 9 21 2 10 22 3
9 18 3 10 20 2 10 21 3 11 19 2 12 19 2
12 22 1 11 18 3 12 18 3 13 18 3 12 16 2
13 17 3 13 16 3 12 14 2 11 14 3 11 13 3
12 13 3 13 14 3 14 12 2 15 13 3 15 12 3
14 11 3 12 11 2 13 10 3 11 12 3 8 14 2
9 13 2 9 12 2 8 11 3 7 10 3 7 8 3
9 9 2 8 9 3 9 7 2 10 10 2 9 10 3
10 11 3 11 9 2 15 9 2 11 8 2 10 9 3
10 7 2 10 8 3 11 7 3 13 8 2 12 8 3
13 6 2 13 7 3 14 8 3 13 5 2 14 5 2
15 5 2 10 5 1 10 4 1 11 4 1 10 3 2
9 3 3 9 1 2 9 0 2 9 2 3 11 5 1
12 5 2 11 5 3 13 3 1 13 4 1 1 8 2
0 8 2 0 4 2 2 3 2 1 4 3 0 2 2
2 2 3 0 1 2 2 0 2 1 0 2 1 1 3
3 13 2 4 13 2 4 14 2 5 13 3 7 14 2
15 16 2 13 21 1 14 21 1 15 21 2 14 21 3
13 23 2 14 23 1 15 23 2 12 25 1 13 24 1
13 25 1 13 26 1 9 26 1 14 26 1 14 24 2
15 24 1 12 20 2 11 21 3 12 23 2 11 24 3
12 26 2 13 27 3 13 29 2 12 25 3 10 26 2
7 27 2 5 26 2 6 26 3 5 27 3 4 27 3
6 27 3 10 27 1 11 27 2 12 27 3 10 28 1
10 29 1 2 28 1 3 29 1 2 27 1 2 26 2
2 25 3 1 25 3 1 23 2 0 23 3 0 22 3
1 20 2 2 22 2 2 21 2 2 20 1 2 27 3
0 27 2 1 27 3 2 29 1 1 29 2 0 29 1
4 29 2 5 28 3 7 29 2 9 28 1 9 27 2
8 26 3 8 28 2 9 28 3 11 29 2 12 28 3
14 29 2 15 28 3 15 1 2 11 2 2 12 2 2
13 2 2 13 1 3 10 0 1 10 2 3 11 0 2
8 5 1 9 6 1 14 1 3 14 4 1
这个棋谱和真实的推理过程有略微的差异,我把不确定的操作都提到了最前面。
这样的微调在逻辑上是通畅的,可以看做一开始刚好点了这几个格子,后面就是纯推理了。
三,纯推理开局
1,定义
对于任意局面,只要开局的时候多点开几个格子,点到合适的格子,然后再开始推理,那么就一定可以做到,在推理过程中全程都是确定的推理结果,不需要概率运算。
即,纯推理,无猜测。
2,实例
对于扫雷代码V1+666777888这个种子来说,开局点开9个格子:
15 3 1 13 0 1 15 0 1 6 0 1 6 10 1
6 20 1 6 29 1 15 15 1 8 17 1
就能得到一个纯推理开局。
上面的棋谱中,对于这个纯推理开局,花了215次操作才获胜。
3,意义
纯推理开局适合用于研究基于扫雷提出的各种优化问题,例如“最快并行扫雷问题”
四,最快并行扫雷
1,规则
允许的操作和串行扫雷相同,但是一个回合可以输入多个操作。
单次操作不刷新界面,只在回合结束时刷新界面。
挑战目标是回合数越少越好
2,扫雷代码V2
#include <iostream>
#include <string>
#include <vector>
#include <time.h>
#include <functional>
#include <algorithm>
#include <vector>
#include <queue>
#include <numeric>
#include <map>
#include <set>
#include <stack>
#include <unordered_map>
#include <unordered_set>
#include <array>
#include <functional>
#include <string.h>
#include <math.h>
#include <fstream>
#include <streambuf>
#include <stdio.h>
#include <mutex>
#include <unordered_map>
#include <iostream>
#include <vector>
#include <iomanip>
#include <string>
#include <algorithm>
#include <cmath>
#include <stack>
#include <set>
#include <queue>
#include <math.h>
#include <functional>
#include <limits>
#include <climits>
#include <stdint.h>
#include <windows.h>
using namespace std;
#define LOCAL
int ROW = 16;
int COL = 30;
int BOMB = 99;
enum Nod {
EmptyGrid,
BombGrid
};
enum Status {
Unknow,
Open,
Flag
};
vector<vector<int>>v;//雷分布
vector<vector<int>>vsum;//雷计数
vector<vector<int>>vs;//实时界面
bool isWin;
int hseed;//棋谱中的种子
vector<int>hr, hc, ho;//棋谱中的行、列、操作数
int turnNum;
void init(int seed)
{
srand(seed);
v.resize(ROW);
vsum.resize(ROW);
vs.resize(ROW);
for (auto& vi : v) {
vi.resize(COL);
for (auto& x : vi)x = EmptyGrid;
}
for (auto& vi : vsum) {
vi.resize(COL);
for (auto& x : vi)x = 0;
}
for (auto& vi : vs) {
vi.resize(COL);
for (auto& x : vi)x = Unknow;
}
int b = BOMB;
while (b) {
int r = rand() % ROW;
int c = rand() % COL;
if (v[r][c] == BombGrid)continue;
v[r][c] = BombGrid;
b--;
for (int i = max(0,r - 1); i <= min(r + 1,ROW-1); i++) {
for (int j = max(0, c - 1); j <= min(c + 1, COL - 1); j++){
vsum[i][j]++;
}
}
}
isWin = false;
turnNum = 0;
}
//鼠标左键单击,是否阵亡
bool clickLeft(int r, int c)
{
if (v[r][c] == BombGrid) {
return true;
}
if (vs[r][c] == Open)return false;
vs[r][c] = Open;
if (vsum[r][c])return false;
for (int i = max(0, r - 1); i <= min(r + 1, ROW - 1); i++) {
for (int j = max(0, c - 1); j <= min(c + 1, COL - 1); j++) {
clickLeft(i, j);
}
}
return false;
}
//鼠标右键单击,是否阵亡
bool clickRight(int r, int c)
{
vs[r][c] = Flag;
return v[r][c] != BombGrid;
}
//鼠标左键双击,是否阵亡
bool clickDouble(int r, int c)
{
for (int i = max(0, r - 1); i <= min(r + 1, ROW - 1); i++) {
for (int j = max(0, c - 1); j <= min(c + 1, COL - 1); j++) {
if (vs[i][j] != Flag && clickLeft(i, j))return true;
}
}
return false;
}
//1左键单击,2右键单击,3左键双击,4显示棋谱,5结束回合,返回是否结束
bool opt(int r, int c, int optNum)
{
if (optNum !=4) {
hr.push_back(r);
hc.push_back(c);
ho.push_back(optNum);
}
if (optNum == 1 && clickLeft(r, c))return true;
if (optNum == 2 && clickRight(r, c))return true;
if (optNum == 3 && clickDouble(r, c))return true;
if (optNum == 4) {
cout << hseed << endl;
for (int i = 0; i < hr.size(); i++) {
cout << hr[i] << " " << hc[i] << " " << ho[i] << " ";
if (i % 5 == 4)cout << endl;
}
Sleep(5000);
return false;
}
if (optNum == 5)return false;
for (int i = 0; i < ROW; i++) {
for (int j = 0; j < COL; j++) {
if (vs[i][j] == Unknow && v[i][j]== EmptyGrid) {
return false;
}
}
}
return isWin = true;
}
void show()
{
system("cls");
cout << " 0 1 2 3 4 5 6 7 8 9 10 12 14 16 18 20 ";
cout << "22 24 26 28" << endl;
int flagNum = 0;
for (int i = 0; i < ROW; i++) {
cout << i << " ";
if (i < 10)cout << " ";
for (int j = 0; j < COL; j++) {
if (vs[i][j] == Unknow)cout << "■";
else if (vs[i][j] == Open) {
if (vsum[i][j])cout << vsum[i][j]<<"_";
else cout << "□";
}
else {
cout << "●";
flagNum++;
}
}
cout << endl;
}
cout << "还剩" << BOMB - flagNum << "雷未标记";
cout << endl;
}
void showEnd()
{
cout << " 0 1 2 3 4 5 6 7 8 9 10 12 14 16 18 20 ";
cout << "22 24 26 28" << endl;
for (int i = 0; i < ROW; i++) {
cout << i << " ";
if (i < 10)cout << " ";
for (int j = 0; j < COL; j++) {
if (v[i][j] == BombGrid)cout << "●";
else cout << "□";
}
cout << endl;
}
cout << endl;
opt(0, 0, 4);
}
bool run()
{
show();
cout << "输入若干组(行 列 操作数)" << endl;
cout << "1左键单击,2右键单击,3左键双击,4显示棋谱,5结束回合" << endl;
int r, c, optNum;
while (cin >> r)
{
cin >> c >> optNum;
if (opt(r, c, optNum))return true;
if (optNum == 5) {
turnNum++;
return false;
}
}
return false;
}
int main()
{
cout << "输入随机数种子" << endl;
cin >> hseed;
init(hseed);
while (!run());
if (isWin)cout << "Win!!! 总回合数=" << turnNum + 1 << endl;
else cout << "Lose..." << endl;
showEnd();
return 0;
}
3,实战
代码V1和V2的随机子系统是相同的,所以我还是用666777888这个种子,开局点开9个格子:
15 3 1 13 0 1 15 0 1 6 0 1 6 10 1
6 20 1 6 29 1 15 15 1 8 17 1
得到纯推理开局之后挑战最快并行扫雷。
我的成绩是回合数=12,去掉第一个回合是用于构造纯推理开局,我的成绩是11个回合。
即对于这个开局,我只用了11个回合(中间只刷新了10次局面)就完成了完整的扫雷。
棋谱:
2 2 1 2 5 1 3 5 1 4 5 1 5 5 2
6 4 2 6 5 1 7 4 2 8 3 3 9 0 2
9 1 2 11 2 2 12 2 2 13 2 2 14 2 1
15 1 2 14 1 3 6 15 2 5 16 3 4 18 2
5 19 3 7 20 2 6 20 3 8 20 1 9 19 2
9 20 1 10 19 1 11 19 2 12 14 2 12 17 1
11 14 3 9 13 2 8 14 2 7 14 2 6 14 1
8 13 1 5 5 5 2 3 2 0 4 2 1 4 3
2 2 3 2 0 2 1 8 2 0 8 2 2 9 3
4 10 2 5 10 1 7 7 2 7 10 1 5 6 2
6 7 3 7 5 1 14 3 2 14 2 3 3 25 2
7 22 1 7 23 2 7 24 2 7 25 1 7 26 2
4 26 1 12 16 2 12 17 3 12 19 2 12 15 3
12 13 3 11 13 3 9 12 2 8 13 3 6 14 3
4 14 2 4 15 3 3 17 2 4 17 3 3 19 2
3 20 2 3 24 3 2 21 2 2 22 2 2 20 1
5 5 5 1 0 2 0 1 2 0 2 2 1 9 3
5 11 3 1 11 1 4 13 2 3 15 3 2 17 2
3 18 3 7 25 3 5 26 2 4 26 3 8 23 1
13 18 3 13 16 3 13 14 3 14 12 2 8 11 3
10 12 3 12 11 2 13 12 3 7 9 2 9 9 2
8 10 3 7 8 3 8 7 1 5 5 5 0 0 1
2 11 2 3 13 2 14 11 3 10 10 2 11 11 3
10 9 1 0 16 2 2 16 3 0 17 2 2 18 3
1 20 2 1 21 1 2 28 1 4 27 3 5 27 3
7 27 2 8 24 3 8 25 3 12 20 2 13 20 3
15 21 2 5 5 5 4 11 3 13 10 3 11 9 2
15 9 2 9 8 1 9 7 2 12 21 3 14 21 3
12 23 2 11 24 3 10 26 2 11 26 1 12 26 2
10 27 1 7 28 1 3 29 1 1 21 3 1 25 1
5 5 5 0 19 2 1 18 3 8 22 2 7 21 3
0 24 1 0 25 1 0 26 1 12 25 3 13 23 2
13 22 3 15 23 2 15 16 2 11 8 2 14 8 1
10 7 2 9 8 3 5 5 5 1 23 2 0 23 1
2 26 2 2 27 1 10 21 1 13 26 3 11 27 2
14 24 2 13 24 3 13 7 1 14 7 1 15 7 1
11 7 1 5 5 5 9 21 2 10 20 2 15 25 3
2 27 3 0 27 2 12 27 3 13 29 2 13 8 2
13 6 2 11 7 3 13 5 2 14 5 2 15 5 2
5 5 5 0 28 1 0 29 1 10 28 1 10 29 1
10 5 1 11 5 1 12 5 2 10 2 1 5 5 5
1 29 2 4 29 2 7 29 2 9 27 2 8 27 1
11 5 3 10 3 2 10 2 3 5 5 5 3 29 3
6 29 3 10 0 1 12 3 2 13 3 3 8 5 1
9 6 1 8 29 1 11 29 2 10 29 3 12 29 1
15 29 1
4,操作数分析
对于这个纯推理开局,三种操作(左键单击、右键单击、左键双击)我一共使用了216次,刚好只比串行扫雷多1次。