题目描述
N皇后问题即为在一个n×n的棋盘上放置n个彼此不受攻击的皇后。按照国际象棋的规则,皇后可以攻击同行、同列或同一斜线上的棋子。等价于在n×n的棋盘放置n个皇后,使得任意两个皇后不在同一行、同一列以及同一斜线上。
Input:n(棋盘规格与皇后数量)
Output:n皇后问题的解向量
回溯法
N皇后问题的解空间可以用一棵完全n叉树表示,这意味着有nn种可能的解空间。
如果不进行剪枝那么时间复杂度会达到夸张的O(nn)。因此对于回溯法而言,难点在于确定适当的剪枝函数。
事实上我们能做到的就是控制其快速判断是否满足约定条件。
那么我们可以一行一行的放入皇后,这样就始终满足每一行只有一个皇后。这样我们的解空间规模就大致为n!。
那么该如何判断放入皇后的当前列和两条斜线有无皇后呢?
如果选择每次都循环判断的方式,那么每次插入都要经过时间复杂度为O(n)的判断,这显然是相当浪费时间的。
可以考虑用哈希的思想直接用一个bool数组记录每一列和每一对角线的是否放入皇后。下面以4皇后为例:
用Col记录列状态后,后续插入皇后只需用O(1)的时间复杂度即可判断该列有无皇后。但是对于对角线问题我们还没有解决,继而我们考虑一个n×n的棋盘有多少条副对角线,且满足什么状态:
如上图我们不难发现,一个n×n的棋盘应当有2n-1条副对角线,且这些对角线都是同一斜率-1,这意味着他们都满足x+y=b,且b的取值范围为{0,1,2,3,4,5,6}.因此我们只需创建一个bool Dig2数组来记录每个皇后(x,y)对应的x+y是否为true,即可知道该皇后所在副对角线上有无其他皇后。
对于主对角线也是同理,不过需要注意的是主对角线上对角线满足的是x-y=b,且b的取值范围为{-3,-2,-1,0,1,2,3}因此boolDig1需要记录的是x-y+n-1位置的值。
有了上述三个数组我们就能做到O(1)的时间复杂度判断新插入的皇后是否满足约束条件。
这样我们从0行0列开始逐行插入,每一行都循环判断0到n列是否有能插入的位置,如果有就插入并判断下一行。如果没有则返回上一行,对上一行剩余位置继续查找符合约束条件的插入位置。如果顺利插入最后一行的皇后,则记录当前解空间并返回。
所有解向量
返回所有解:
void dfs(size_t row, vector<bool>& Col, vector<bool>& Dig1, vector<bool>& Dig2, vector<int>& Path, vector<vector<int>>& Ret)
{
if (row == Path.size())//插入成功
{
Ret.push_back(Path);
return;
}
for (int col = 0; col < Path.size(); col++)//尝试在这一行插入皇后
{
if (!Col[col] && !Dig1[row - col + Path.size() - 1] && !Dig2[row + col])
{
Path[row] = col;//插入当前位置
Col[col] = Dig1[row - col + Path.size() - 1] = Dig2[row + col] = true;//记录已插入的列和对角线
dfs(row + 1, Col, Dig1, Dig2, Path, Ret);//插入下一行
Col[col] = Dig1[row - col + Path.size() - 1] = Dig2[row + col] = false;//恢复
}
}
}
vector<vector<int>> NQueens(size_t n)
{
vector<bool>Col(n, false), Dig1(2 * n - 1, false), Dig2(2 * n - 1, false);
vector<int>Path(n);
vector<vector<int>>Ret;
dfs(0, Col, Dig1, Dig2, Path, Ret);
return Ret;
}
返回单个解
伪代码
function backtrackingNQueensOneSolution(n):
Col = array of n false values
Dig1 = array of (2n-1) false values
Dig2 = array of (2n-1) false values
Path = array of n integers
if dfsOneSolution(0, Col, Dig1, Dig2, Path, n):
return Path
else:
return empty array
function dfsOneSolution(row, Col, Dig1, Dig2, Path, n):
if row == n:
return true
for col from 0 to n-1:
diag1 = row - col + n - 1
diag2 = row + col
if not Col[col] and not Dig1[diag1] and not Dig2[diag2]:
Path[row] = col
Col[col] = true
Dig1[diag1] = true
Dig2[diag2] = true
if dfsOneSolution(row+1, Col, Dig1, Dig2, Path, n):
return true
Col[col] = false
Dig1[diag1] = false
Dig2[diag2] = false
return false
bool dfsOneSolution(size_t row, vector<bool>& Col, vector<bool>& Dig1, vector<bool>& Dig2, vector<int>& Path) {
if (row == Path.size())
{
return true; // 找到解
}
for (int col = 0; col < Path.size(); col++) {
if (!Col[col] && !Dig1[row - col + Path.size() - 1] && !Dig2[row + col])
{
Path[row] = col;
Col[col] = Dig1[row - col + Path.size() - 1] = Dig2[row + col] = true;//记录已经插入过的列、对角线
// 递归搜索下一行,如果找到解则立即返回
if (dfsOneSolution(row + 1, Col, Dig1, Dig2, Path))
{
return true;
}
// 回溯
Col[col] = Dig1[row - col + Path.size() - 1] = Dig2[row + col] = false;
}
}
return false; // 当前行所有列都尝试过,无解
}
vector<int> backtrackingNQueensOneSolution(size_t n)
{
vector<bool> Col(n, false);
vector<bool> Dig1(2 * n - 1, false);
vector<bool> Dig2(2 * n - 1, false);
vector<int> Path(n);
if (dfsOneSolution(0, Col, Dig1, Dig2, Path))
{
return Path; // 返回找到的解
}
return {}; // 未找到解(实际上n>3时总有解)
}
启发式修补法
我们可以先随机将皇后放入棋盘中,并确保行和列不冲突。并且统计每个位置的冲突数,然后将每行的皇后移动至冲突数最小的位置,然后重新统计冲突数,如果冲突数为0,则返回,否则继续。如下图所示:
可以发现冲突最小值的位置有时候不止一个,那么我们随机选择其中一个即可。然而在具体实现中,我发现将所有皇后移动至冲突最小的位置,不仅效率低下,关键是很容易造成死循环。因此不得不记录下已经走过的状态,因而导致效率进一步下降。
所以对其进行简单的改进,就是每次只需移动冲突最大的皇后即可。比如上图的状态可以移动至如下:
不难发现,冲突最大位置的皇后有时候也不仅一个,我们同样是随机选择其中一个移动。这样强大的随机性就极大地避免了死循环的可能。
原版
伪代码
function heuristicRepairNQueens(n, maxIter):
Col = zeros(n) # 列冲突计数
Dig1 = zeros(2n-1) # 主对角线冲突计数
Dig2 = zeros(2n-1) # 副对角线冲突计数
Path = array of n integers
RandomArrange(Path, Col, Dig1, Dig2) # 随机初始化
Mins = array of n integers (init to n+1)
Conflicts = n x n matrix
ConflictsRows = list of n lists
seen = empty set
for iter from maxIter downto 0:
if IsComplete(Path, Col, Dig1, Dig2):
return Path
CountConflicts(Path, Col, Dig1, Dig2, Mins, Conflicts, ConflictsRows)
# 移动所有皇后
for row from 0 to n-1:
oldcol = Path[row]
newcol = random choice from ConflictsRows[row]
# 更新冲突计数
Dig1[row - oldcol + n - 1] -= 1
Dig2[row + oldcol] -= 1
Col[oldcol] -= 1
Dig1[row - newcol + n - 1] += 1
Dig2[row + newcol] += 1
Col[newcol] += 1
Path[row] = newcol
# 处理循环
while Path in seen and iter > 0:
RandomArrange(Path, Col, Dig1, Dig2)
iter -= 1
add Path to seen
return empty array # 未找到解
移动所以皇后:
void RandomArrange(vector<int>& Path, vector<int>& Col, vector<int>& Dig1, vector<int>& Dig2)
{
random_device rd;
mt19937 gen(rd());
int n = Path.size();
//初始化列、对角线的皇后
fill(Col.begin(), Col.end(), 0);
fill(Dig1.begin(), Dig1.end(), 0);
fill(Dig2.begin(), Dig2.end(), 0);
for (size_t row = 0; row < n; row++)
{
size_t col = gen() % n;
//确保列不冲突
while (Col[col])col = gen() % n;
Path[row] = col;
Col[col] = 1;
//记录对角线上皇后个数
++Dig1[row - col + n - 1];
++Dig2[row + col];
}
}
bool IsComplete(vector<int>& Path, vector<int>& Col, vector<int>& Dig1, vector<int>& Dig2)
{
int n = Path.size();
for (size_t row = 0; row < n; row++)
{
int col = Path[row];
if (Col[col] + Dig1[row - col + n - 1] + Dig2[row + col] != 3)
{
return false;
}
}
return true;
}
void CountConflicts(vector<int>& Path, vector<int>& Col, vector<int>& Dig1, vector<int>& Dig2, vector<int>&Mins, vector<vector<int>>& Conflicts, vector<vector<int>>& ConflictsRows)
{
int n = Path.size();
for (size_t row = 0; row < n; row++)
{
//初始化最小值
Mins[row] = n + 1;
for (size_t col = 0; col < n; col++)
{
Conflicts[row][col] = Dig1[row - col + n - 1] + Dig2[row + col] + Col[col];
if (Path[row] == col)Conflicts[row][col] -= 3;
Mins[row] = min(Mins[row], Conflicts[row][col]);
}
}
for (size_t row = 0; row < n; row++)
{
//初始化最小值位置
ConflictsRows[row].clear();
for (size_t col = 0; col < n; col++)
{
if (Conflicts[row][col] == Mins[row])
ConflictsRows[row].push_back(col);
}
}
}
vector<int> heuristicRepairNQueens(size_t n, int maxIter = 10000)
{
// 随机初始化:每行、每列一个皇后,列位置随机
vector<int> Col(n);
//记录各个主副对角线上皇后个数
vector<int> Dig1(2 * n - 1);
vector<int> Dig2(2 * n - 1);
vector<int> Path(n);
random_device rd;
mt19937 gen(rd());
//随机生成棋盘
RandomArrange(Path, Col, Dig1, Dig2);
//记录每行冲突最小的值
vector<int>Mins(n,n+1);
// 冲突统计冲突个数
vector<vector<int>>Conflicts(n, vector<int>(n));
//记录每行冲突最小的位置
vector<vector<int>>ConflictsRows(n);
//记录当前路径是否已经存在
set<vector<int>>st;
for (int i = maxIter; i >= 0; i--)
{
//判断是否插入完成,如果插入完成则返回
if (IsComplete(Path, Col, Dig1, Dig2))return Path;
//没有插入完成则更新冲突数组
CountConflicts(Path, Col, Dig1, Dig2, Mins, Conflicts, ConflictsRows);
//
//PrintQueen(Path);
//cout << endl;
//将皇后移动至冲突最小位置
//有可能导致列冲突
for (size_t row = 0; row < n; row++)
{
int oldcol = Path[row];
int newcol = ConflictsRows[row][gen() % (ConflictsRows[row].size())];
//移动皇后
Path[row] = newcol;
//记录新的对角线皇后个数
--Dig1[row - oldcol + n - 1];
--Dig2[row + oldcol];
++Dig1[row - newcol + n - 1];
++Dig2[row + newcol];
//记录新的列皇后个数
--Col[oldcol];
++Col[newcol];
}
//如果当前路径已经被记录,则重新随机生成棋盘
while(!st.insert(Path).second&&i)
{
RandomArrange(Path, Col, Dig1, Dig2);
i--;
}
}
return {}; // 未找到解
}
改进版
伪代码
function greedyRepairNQueens(n, maxIter):
Path, Col, Dig1, Dig2 = random initial arrangement
for iter from 0 to maxIter-1:
if IsComplete(Path, Col, Dig1, Dig2):
return Path
# 找最大冲突皇后
maxConflicts = -1
conflictRows = empty list
for row from 0 to n-1:
col = Path[row]
conflicts = Col[col] + Dig1[row-col+n-1] + Dig2[row+col] - 3
if conflicts > maxConflicts:
maxConflicts = conflicts
conflictRows = [row]
else if conflicts == maxConflicts:
add row to conflictRows
row = random choice from conflictRows
oldcol = Path[row]
minConflicts = ∞
bestCol = oldcol
# 找最小冲突位置
for col from 0 to n-1:
if col == oldcol: continue
conflicts = Col[col] + Dig1[row-col+n-1] + Dig2[row+col]
if conflicts < minConflicts:
minConflicts = conflicts
bestCol = col
if bestCol ≠ oldcol:
# 移动皇后
update conflict counts
else:
RandomArrange(Path, Col, Dig1, Dig2)
return empty array
vector<int> greedyRepairNQueens(int n, int maxIter = 10000)
{
vector<int> Path(n), Col(n), Dig1(2 * n - 1), Dig2(2 * n - 1);
RandomArrange(Path, Col, Dig1, Dig2);
random_device rd;
mt19937 gen(rd());
for (int iter = 0; iter < maxIter; iter++)
{
if (IsComplete(Path, Col, Dig1, Dig2)) return Path;
//记录已插入皇后位置的最大冲突数
int maxConflicts = 0;
vector<int> conflictRows;
for (int row = 0; row < n; row++)
{
int col = Path[row];
int conflicts = Col[col] + Dig1[row - col + n - 1] + Dig2[row + col] - 3;
if (conflicts > maxConflicts)
{
maxConflicts = conflicts;
conflictRows.clear();
conflictRows.push_back(row);
}
else if (conflicts == maxConflicts)
{
conflictRows.push_back(row);
}
}
//随机选择一个最大冲突的皇后
int row = conflictRows[gen() % conflictRows.size()];
int oldcol = Path[row];
int bestCol = oldcol;
//记录当前行最小冲突位置
int minConflicts = n + 1;
for (int col = 0; col < n; col++)
{
if (col == oldcol) continue;
int conflicts = Col[col] + Dig1[row - col + n - 1] + Dig2[row + col];
if (conflicts < minConflicts)
{
minConflicts = conflicts;
bestCol = col;
}
}
if (bestCol != oldcol)
{
Path[row] = bestCol;
--Col[oldcol];
--Dig1[row - oldcol + n - 1];
--Dig2[row + oldcol];
++Col[bestCol];
++Dig1[row - bestCol + n - 1];
++Dig2[row + bestCol];
}
else//为移动则重排
{
RandomArrange(Path, Col, Dig1, Dig2);
}
}
return {}; // 未找到解
}
拉斯维加斯随机算法
我们可以先随机放入前k个然后再进行回溯。具体思路参考回溯这里就不多做赘述。
伪代码
function lasVegasHybridNQueens(n, k, maxTries):
for attempt from 0 to maxTries-1:
Col = array of n false values
Dig1 = array of (2n-1) false values
Dig2 = array of (2n-1) false values
Path = array of n integers
if randomPlaceFirstK(k, n, Path, Col, Dig1, Dig2):
if backtrack(k, n, Path, Col, Dig1, Dig2):
return Path
return empty array
function randomPlaceFirstK(k, n, Path, Col, Dig1, Dig2):
for row from 0 to k-1:
candidates = []
for col from 0 to n-1:
diag1 = row - col + n - 1
diag2 = row + col
if not Col[col] and not Dig1[diag1] and not Dig2[diag2]:
add col to candidates
if candidates is empty:
return false
col = random choice from candidates
Path[row] = col
Col[col] = true
Dig1[diag1] = true
Dig2[diag2] = true
return true
function backtrack(row, n, Path, Col, Dig1, Dig2):
# 标准回溯算法(从指定行开始)
# 同前文回溯算法实现
具体代码
// 回溯法,从第 row 行开始补全剩余皇后
bool backtrack(int row, int n, vector<int>& Path, vector<bool>& Col, vector<bool>& Dig1, vector<bool>& Dig2)
{
if (row == n) return true;
for (int col = 0; col < n; ++col)
{
if (!Col[col] && !Dig1[row - col + n - 1] && !Dig2[row + col])
{
Path[row] = col;
//记录插入位置列、对角线
Col[col] = Dig1[row - col + n - 1] = Dig2[row + col] = true;
//插入完成
if (backtrack(row + 1, n, Path, Col, Dig1, Dig2)) return true;
//回溯
Col[col] = Dig1[row - col + n - 1] = Dig2[row + col] = false;
}
}
return false;
}
// 前 K 行随机放置皇后,若成功返回 true,否则 false
bool randomPlaceFirstK(int k, int n, vector<int>& Path, mt19937& gen, vector<bool>&Col, vector<bool>&Dig1, vector<bool>&Dig2)
{
for (int row = 0; row < k; ++row)
{
vector<int> candidates;
for (int col = 0; col < n; ++col)
{
if (!Col[col]&& !Dig1[row-col+n-1]&& !Dig2[row+col])
{
candidates.push_back(col);
}
}
//插入失败
if (candidates.empty()) return false;
//插入成功
int col = candidates[gen() % candidates.size()];
Path[row] = col;
Col[col] = Dig1[row - col + n - 1] = Dig2[row + col] = true;
}
return true;
}
// 主函数:拉斯维加斯 + 回溯法
vector<int> lasVegasHybridNQueens(int n, int k = 5, int maxTries = 10000)
{
random_device rd;
mt19937 gen(rd());
vector<int> Path(n);
vector<bool>Col(n, false), Dig1(2 * n - 1, false), Dig2(2 * n - 1, false);
for (int attempt = 0; attempt < maxTries; attempt++)
{
if (randomPlaceFirstK(k, n, Path, gen,Col,Dig1,Dig2))
{
if (backtrack(k, n, Path, Col, Dig1, Dig2))
{
return Path;
}
}
}
return {}; // 未找到解
}
简单测试函数
int main()
{
cout << "回溯法" << endl;
size_t begin = clock();
backtrackingNQueensOneSolution(20);
size_t end = clock();
cout << "规模:" << 20 << " 时间:" << end - begin << "ms" << endl;
begin = clock();
backtrackingNQueensOneSolution(30);
end = clock();
cout << "规模:" << 30 << " 时间:" << end - begin << "ms" << endl;
begin = clock();
backtrackingNQueensOneSolution(33);
end = clock();
cout << "规模:" << 33 << " 时间:" << end - begin << "ms" << endl;
cout << "启发式修补法原版" << endl;
size_t begin1 = clock();
heuristicRepairNQueens(500);
size_t end1 = clock();
cout << "规模:" << 500 << " 时间:" << end1 - begin1 << "ms" << endl;
begin1 = clock();
heuristicRepairNQueens(550);
end1 = clock();
cout << "规模:" << 550 << " 时间:" << end1 - begin1 << "ms" << endl;
cout << "启发式修补法改进版" << endl;
size_t begin2 = clock();
greedyRepairNQueens(200000);
size_t end2 = clock();
cout << "规模:" << 200000 << " 时间:" << end2 - begin2 << "ms" << endl;
begin2 = clock();
greedyRepairNQueens(300000);
end2 = clock();
cout << "规模:" << 300000 << " 时间:" << end2 - begin2 << "ms" << endl;
cout << "拉斯维加斯随机算法" << endl;
size_t begin3 = clock();
lasVegasHybridNQueens(36);
size_t end3 = clock();
cout << "规模:" << 36 << " 时间:" << end3 - begin3 << "ms" << endl;
}
运行结果:
从中可以看出回溯法的效率式最差的,拉斯维加斯随机算法则次之,适合中规模。启发式修补法效率则远高于上述两者,适合大规模求解。