横纵斜不能有冲突
*暴力方法:全局搜索空间是N^2,即栈中的安全皇后放好后,下一个皇后的搜索路径仍然是原矩阵。
*剪枝后排除一部分不需要的遍历操作,将剩余遍历空间缩短至N!
tag数组负责记录当前的路径,和DFS中带路径的那个方法几乎一致,只不过回溯的结果是全部解(有多少个解的问题),之前写的那个单纯的dfs只需要找到一个有效解(即有没有解的问题)
下面给出N皇后(Leetcode51)的解法:
1.未优化版本(N*N超级暴力破解)
//自己最先想到并实现了的(按照全搜索空间全排列超级暴力搜,vs2015 + i7 3960X @3.7Ghz 默认频率(6皇后100ms,7皇后52s时间 8皇后需要36min
void dfsqueen3_1(int n, vector<vector<int>>&tag, vector<vector<string>>&ret) {
//每一个子递归函数都采用N*N的搜索空间进行一个一个找吧,先弄出来一个版本再说(暴力)
if (tag.size() == n)
{
int jb = 0;
while (jb < n) {
if (tag[jb][0] != jb++)
return;
}
vector<string>vs;
string c(n, '.');
for (int i = 0; i < tag.size(); ++i) {
c[tag[i][1]] = 'Q';
vs.push_back(c);
c[tag[i][1]] = '.';
}
ret.push_back(vs);
}
for (int q = 0; q < n; ++q)
{
for (int v = 0; v < n; ++v) //这一行的所有列递归完毕后
{
//判断是否满足条件
int gk = 0;
for (int i = 0; i < tag.size(); ++i) //判断此时的皇后是否互相攻击
{
if ((q == tag[i][0] || v == tag[i][1] || q - tag[i][0] == v - tag[i][1] || q - tag[i][0] == tag[i][1] - v))
{
gk = 1;
break;
}
}
if (gk == 0)
{
tag.push_back({ q,v });//并未相互攻击,此时的坐标有效,加入路径
dfsqueen3_1(n, tag, ret);
tag.pop_back();
}
}
}
}
以上方法可以说是非常暴力了,全局复杂度超级大,因为做出了很多重复工作,搞出来的解是四个方向均进行遍历的,也就是说四个方向数组的全排列过程,虽说对于该题而言方法不是很优秀,但是这种思想是有效的。
2.下面给出优化后的第一个版本
//get better2(9-QUEEN: 92s)
void dfsqueen3_2(int n, vector<vector<int>>&tag, vector<vector<string>>&ret,unordered_set<int>&rowset, unordered_set<int>&colset) {
//
if (tag.size() == n)
{
vector<string>vs;
string c(n, '.');
for (int i = 0; i < tag.size(); ++i)
{
c[tag[i][1]] = 'Q';
vs.push_back(c);
c[tag[i][1]] = '.';
}
ret.push_back(vs);
}
for (int q = tag.size(); q < n; ++q)
{
if (rowset.find(q) != rowset.end() && rowset.size() != 0) {
continue;
}
for (int v = 0; v < n; ++v) //这一行的所有列递归完毕后
{
if ((colset.find(v) != colset.end()) && colset.size() != 0) {
continue;
}
//判断是否满足条件
int gk = 0;
for (int i = 0; i < tag.size(); ++i) //判断此时的皇后是否互相攻击
{
if (q - tag[i][0] == v - tag[i][1] || q - tag[i][0] == tag[i][1] - v)
{
gk = 1;
break;
}
}
//剪枝思想:如果第三行试过了所有方案都无法插入,那么第二行的选择肯定有问题,此时无需进入第四行了,直接返回第二行修改后再继续
if (gk == 1 && v == n-1) { return; }//这一行剪一波
if (gk == 0)
{
tag.push_back({ q,v });//并未相互攻击,此时的坐标有效,加入路径
if (tag[0][0] != 0)return;
rowset.insert(q);
colset.insert(v);
dfsqueen3_2(n, tag, ret,rowset,colset);
tag.pop_back();
rowset.erase(q);
colset.erase(v);
}
}
}
}
get better2方法在row搜索时将二重循环中的row搜索过程从七点0,变为了tag中的元素个数,也就是说如果tag中有3个元素,那么前三行是不用去搜索判断的,直接从第四行的所有列进行寻找即可,免去了row的一部分判断过程;第二,该方法在判断完一行的所有列之后进行了tag检查,也就是说如果一行的所有列都没有能满足的位置,那么这一行无效,下面的行统统不需要判断了,大大减少了计算量。
3.下面给出优化后的get better3版本
void dfsqueen3_3(int n, vector<vector<int>>&tag, vector<vector<string>>&ret, vector<int>&rowset, vector<int>&colset) {
//
if (tag.size() == n)
{
vector<string>vs;
string c(n, '.');
for (int i = 0; i < tag.size(); ++i)
{
c[tag[i][1]] = 'Q';
vs.push_back(c);
c[tag[i][1]] = '.';
}
ret.push_back(vs);
}
for (int q = tag.size(); q < n; ++q)
{
if (rowset[q] != 0) {
continue;
}
for (int v = 0; v < n; ++v) //这一行的所有列递归完毕后
{
if (colset[v] != 0) {
continue;
}
//判断是否满足条件
int gk = 0;
for (int i = 0; i < tag.size(); ++i) //判断此时的皇后是否互相攻击
{
if (q - tag[i][0] == v - tag[i][1] || q - tag[i][0] == tag[i][1] - v)
{
gk = 1;
break;
}
}
//剪枝思想:如果第三行试过了所有方案都无法插入,那么第二行的选择肯定有问题,此时无需进入第四行了,直接返回第二行修改后再继续
if (gk == 1 && v == n - 1) { return; }//这一行剪一波
if (gk == 0)
{
//并未相互攻击,此时的坐标有效,加入路径
tag.push_back({ q,v });
if (tag[0][0] != 0)return;
rowset[q] = 1;
colset[v] = 1;
dfsqueen3_3(n, tag, ret, rowset, colset);
tag.pop_back();
rowset[q] = 0;
colset[v] = 0;
}
}
}
}
第三个版本把第二版中的哈希表unordered_set集合用vector来替代了,vector寻址的优势体现出来了。其他的没有做出改动
4.第四个版本(纯C数组优化常数时间)
//get better4(9-QUEEN: 18.139s 优化vector至普通数组,常数时间?)
void dfsqueen3_4(int n, vector<vector<int>>&tag, vector<vector<string>>&ret, int(&rowset)[10], int(&colset)[10]) {
//
if (tag.size() == n)
{
vector<string>vs;
string c(n, '.');
for (int i = 0; i < tag.size(); ++i)
{
c[tag[i][1]] = 'Q';
vs.push_back(c);
c[tag[i][1]] = '.';
}
ret.push_back(vs);
}
for (int q = tag.size(); q < n; ++q)
{
if (rowset[q] != 0) {
continue;
}
for (int v = 0; v < n; ++v) //这一行的所有列递归完毕后
{
if (colset[v] != 0) {
continue;
}
//判断是否满足条件
int gk = 0;
for (int i = 0; i < tag.size(); ++i) //判断此时的皇后是否互相攻击
{
if (q - tag[i][0] == v - tag[i][1] || q - tag[i][0] == tag[i][1] - v)
{
gk = 1;
break;
}
}
//剪枝思想:如果第三行试过了所有方案都无法插入,那么第二行的选择肯定有问题,此时无需进入第四行了,直接返回第二行修改后再继续
if (gk == 1 && v == n - 1) { return; }//这一行剪一波
if (gk == 0)
{
//并未相互攻击,此时的坐标有效,加入路径
tag.push_back({ q,v });
if (tag[0][0] != 0)return;
rowset[q] = 1;
colset[v] = 1;
dfsqueen3_4(n, tag, ret, rowset, colset);
tag.pop_back();
rowset[q] = 0;
colset[v] = 0;
}
}
}
}
纯方括号数组的引用,看起来很不错,不过在小范围数字内,这种方法没有比vector方法取得足够令人满意的效果。这俩只相差了几ms,几乎可以看作是误差。
5.降维(根据一开始的思路, 不遍历row了,只遍历col,把row当成形参加入递归函数)
class Solution {
public:
void dfs(int n,int row,vector<vector<int>>&tag,vector<vector<string>>&ret,int(&colset)[10]) {
if (tag.size() == n) {
vector<string>vs;
string c(n,'.');
for (int i = 0; i < tag.size(); ++i) {
c[tag[i][1]] = 'Q';
vs.push_back(c);
c[tag[i][1]] = '.';
}
ret.push_back(vs);
}
for (int v = 0; v < n; ++v) {
if (colset[v] == 1)continue;
int mute = 0;
for (int i = 0; i < tag.size(); ++i) {
if (row - tag[i][0] == v - tag[i][1] || row - tag[i][0] == tag[i][1] - v) {
mute = 1;
break;
}
}
if (mute == 0) {
tag.push_back({ row,v });
colset[v] = 1;
dfs(n,row+1,tag,ret,colset);
tag.pop_back();
colset[v] = 0;
}
}
}
vector<vector<string>> solveNQueens(int n) {
vector<vector<string>>ret;
vector<vector<int>>tag;
int pc[10]{ 0,0,0,0,0,0,0,0,0,0 };
dfs(n, 0,tag, ret, pc);
return ret;
}
};
显然,优化至目前的情况并非是最佳的,因为斜线上的皇后攻击判断仍然是每次递归后与所有tag中的元素判断,并不是O(1)复杂度的直接寻址,但是更优解法目前先不搞了。以后有机会再写。