N皇后问题(回溯、启发式算法、随机算法)

题目描述

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;

}

运行结果:
在这里插入图片描述
从中可以看出回溯法的效率式最差的,拉斯维加斯随机算法则次之,适合中规模。启发式修补法效率则远高于上述两者,适合大规模求解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值