- 题目描述
- 回溯法
- 所有解向量
- 返回单个解
- 伪代码
- 启发式修补法
- 原版
- 伪代码
- 改进版
- 伪代码
- 拉斯维加斯随机算法
- 伪代码
- 具体代码
- 简单测试函数
题目描述
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 valuesDig1 = array of (2n-1) false valuesDig2 = array of (2n-1) false valuesPath = array of n integersif dfsOneSolution(0, Col, Dig1, Dig2, Path, n):return Pathelse:return empty arrayfunction dfsOneSolution(row, Col, Dig1, Dig2, Path, n):if row == n:return truefor col from 0 to n-1:diag1 = row - col + n - 1diag2 = row + colif not Col[col] and not Dig1[diag1] and not Dig2[diag2]:Path[row] = colCol[col] = trueDig1[diag1] = trueDig2[diag2] = trueif dfsOneSolution(row+1, Col, Dig1, Dig2, Path, n):return trueCol[col] = falseDig1[diag1] = falseDig2[diag2] = falsereturn 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 integersRandomArrange(Path, Col, Dig1, Dig2) # 随机初始化Mins = array of n integers (init to n+1)Conflicts = n x n matrixConflictsRows = list of n listsseen = empty setfor iter from maxIter downto 0:if IsComplete(Path, Col, Dig1, Dig2):return PathCountConflicts(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] -= 1Dig2[row + oldcol] -= 1Col[oldcol] -= 1Dig1[row - newcol + n - 1] += 1Dig2[row + newcol] += 1Col[newcol] += 1Path[row] = newcol# 处理循环while Path in seen and iter > 0:RandomArrange(Path, Col, Dig1, Dig2)iter -= 1add Path to seenreturn 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 arrangementfor iter from 0 to maxIter-1:if IsComplete(Path, Col, Dig1, Dig2):return Path# 找最大冲突皇后maxConflicts = -1conflictRows = empty listfor row from 0 to n-1:col = Path[row]conflicts = Col[col] + Dig1[row-col+n-1] + Dig2[row+col] - 3if conflicts > maxConflicts:maxConflicts = conflictsconflictRows = [row]else if conflicts == maxConflicts:add row to conflictRowsrow = random choice from conflictRowsoldcol = Path[row]minConflicts = ∞bestCol = oldcol# 找最小冲突位置for col from 0 to n-1:if col == oldcol: continueconflicts = Col[col] + Dig1[row-col+n-1] + Dig2[row+col]if conflicts < minConflicts:minConflicts = conflictsbestCol = colif bestCol ≠ oldcol:# 移动皇后update conflict countselse: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 valuesDig1 = array of (2n-1) false valuesDig2 = array of (2n-1) false valuesPath = array of n integersif randomPlaceFirstK(k, n, Path, Col, Dig1, Dig2):if backtrack(k, n, Path, Col, Dig1, Dig2):return Pathreturn empty arrayfunction 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 - 1diag2 = row + colif not Col[col] and not Dig1[diag1] and not Dig2[diag2]:add col to candidatesif candidates is empty:return falsecol = random choice from candidatesPath[row] = colCol[col] = trueDig1[diag1] = trueDig2[diag2] = truereturn truefunction 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;}
运行结果:
从中可以看出回溯法的效率式最差的,拉斯维加斯随机算法则次之,适合中规模。启发式修补法效率则远高于上述两者,适合大规模求解。