为啥这个解决方案对 n 个皇后的复杂性如此之大?

Posted

技术标签:

【中文标题】为啥这个解决方案对 n 个皇后的复杂性如此之大?【英文标题】:Why is the complexity of this solution to the n queens so big?为什么这个解决方案对 n 个皇后的复杂性如此之大? 【发布时间】:2015-06-01 19:17:11 【问题描述】:

我正在尝试使用矩阵来表示棋盘来解决n queens problem。这是我的第一个解决方案:

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

#define N 13

void printTable(int table[N][N], int size)

    for(int i = 0; i < size; i++)
    
        for(int j = 0; j < size; j++)
        
            printf("%d ", table[i][j]);
        
        printf("\n");
    
    printf("\n");


bool isSafe(int table[N][N], int row, int column, int size)

    // check the main diagonal
    // we add +1 because otherwise we would be comparind against the base
    // element on that line
    for(int i = row + 1, j = column + 1; i < size && j < size; i++, j++)
    
        if(table[i][j] == 1)
            return false;
    

    // check the secondary diagonal
    for(int i = row + 1, j = column - 1; i < size && j >= 0; i++, j--)
    
        if(table[i][j] == 1)
            return false;
    

    // check the column
    for(int i = row + 1, j = column; i < size; i++)
    
        if(table[i][j] == 1)
            return false;
    

    return true;


bool isSafeTable(int table[N][N], int size)

    for(int i = 0; i < size; i++)
    
        for(int j = 0; j < size; j++)
        
            if(table[i][j] == 1)
            
                if(!isSafe(table, i, j, size))
                
                    return false;
                
            
        
    
    return true;


void getQueens(int table[N][N], int size, int queens, int row)

    if(queens == size)
    
        if(isSafeTable(table, size))
        
            printTable(table, size);
        
        return;
    

    for(int i = 0; i < size; i++)
    
        table[row][i] = 1;
        if(isSafeTable(table, size))
        
            getQueens(table, size, queens + 1, row + 1);
        
        table[row][i] = 0;
    


int main()

    int table[N][N] =
    
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
    ;

    getQueens(table, 4, 0, 0);

    return 0;

如您所见,我使用大量整数数组来表示棋盘。矩阵的大小为13 x 13。为了解决少于13 皇后的问题,我研究了那个大矩阵的一个子集。

如您所见,我在每一步都使用函数isSafeTable 来检查棋盘是否具有有效配置。如果有,我切换到下一行。如果没有,我就回溯。

然而,这个函数isSafeTable 的复杂度为O(n^3)(因为它在每次迭代中调用isSafe)。因此,我认为标记已使用的元素并仅检查该空间是否可用而不是检查整个棋盘将是一个更明智的决定。

所以,我想出了这个解决方案:

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

#define N 13

void printTable(int table[N][N], int size)

    for(int i = 0; i < size; i++)
    
        for(int j = 0; j < size; j++)
        
            printf("%2d ", table[i][j]);
        
        printf("\n");
    
    printf("\n");


void _markWith(int table[N][N], int size, int row, int column, int element,
    int specialCharacter)

    for(int i = 0; i < size - row; i++)
    
        int tmp = element;
        // using the specialCharacter we can mark the queens with a different
        // character depeneding on the calling function.
        if(i == 0)
            element = specialCharacter;

        // mark the left diagonal
        if(column - i >= 0)
            table[row + i][column - i] = element;

        // mark the right diagonal
        if(column + i < size)
            table[row + i][column + i] = element;

        // mark the column
        table[row + i][column] = element;

        element = tmp;
    


// This is just a wrapper used to avoid duplicating the code for marking and
// unmarking a table.
void mark(int table[N][N], int size, int row, int column)

    _markWith(table, size, row, column, -1, 8);


// See the documentation for `mark`.
void unmark(int table[N][N], int size, int row, int column)

    _markWith(table, size, row, column, 0, 0);


void getQueens(int table[N][N], int size, int queens, int row)

    if(queens == size)
    
        printTable(table, size);
        return;
    

    for(int i = 0; i < size; i++)
    
        if(table[row][i] == 0)
        
            // This function call will result in pruning the column and the
            // diagonals of this element. It actually replaces the 0s with -1s.
            mark(table, size, row, i);

            getQueens(table, size, queens + 1, row + 1  );

            // Now replace the -1s with 0s.
            unmark(table, size, row, i);
        

    



int main()

    int table[N][N] =
    
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0
    ;

    getQueens(table, 11, 0, 0);

    return 0;

函数markunmark 用于将元素的对角线和列设置为-1。此外,元素(女王)用 8 标记(我认为在打印矩阵时人眼会更容易识别女王)。

函数_markWith 只是为了避免重写相同的代码 markunmark

这些函数的复杂度是O(n),所以程序应该运行得快一点,但事实并非如此。第一个解决方案实际上比第二个解决方案更快。

以下是n函数中的一些统计数据:

两种解决方案所花费的时间取决于n:

n |  first solution | second solution    
--+-----------------+-----------------    
4 | 0.001s          |  0.002s    
--+-----------------+-----------------    
5 | 0.002s          |  0.001s    
--+-----------------+-----------------    
6 | 0.001s          |  0.002s    
--+-----------------+-----------------    
7 | 0.004s          |  0.003s    
--+-----------------+-----------------    
8 | 0.006s          |  0.011s    
--+-----------------+-----------------    
9 | 0.025s          |  0.133s    
--+-----------------+-----------------    
10| 0.093s          |  3.032s    
--+-----------------+-----------------    
11| 0.581s          |  1m 24.210s

对于n 的小值,差异并不明显,但对于较大的值,差异就很明显了。

这是每个函数根据n 执行的递归调用次数:

n |  first solution | second solution    
--+-----------------+-----------------    
4 | 16              |  16    
--+-----------------+-----------------    
5 | 53              |  65    
--+-----------------+-----------------    
6 | 152             |  514    
--+-----------------+-----------------    
7 | 551             |  7085    
--+-----------------+-----------------    
8 | 2 056           |  129 175    
--+-----------------+-----------------    
9 | 8 393           |  2 810 090    
--+-----------------+-----------------    
10| 35 538          |  70 159 513    
--+-----------------+-----------------    
11| 16 695          |  1 962 694 935

如您所见,递归调用的数量在第二种解决方案中呈指数增长。所以函数markunmark 不对程序运行缓慢的方式负责。

我花了这一天试图找出为什么第二个解决方案与第一个解决方案相比执行了如此多的递归调用,但我无法找到答案。

你能帮帮我吗?

【问题讨论】:

第二个源代码列表与第一个相同,但缺少对 markunmark_markWith 的引用。 @uesp 我已经解决了这个问题。 【参考方案1】:

第二种方法是错误的。它输出比正常更多的解决方案。例如,对于N = 5,它输出(以及其他):

 8  0  0  0  0
-1 -1  0  0  8
-1  0  8 -1 -1
 8 -1 -1 -1 -1
-1 -1 -1  8 -1

 0  0  0  8  0
 0  8 -1 -1 -1
-1 -1 -1 -1  8
 8 -1  0 -1 -1
-1 -1 -1  8 -1

原因是你的标记代码:

if(table[row][i] == 0)

      // This function call will result in pruning the column and the
      // diagonals of this element. It actually replaces the 0s with -1s.
      mark(table, size, row, i);

      getQueens(table, size, queens + 1, row + 1  );

      // Now replace the -1s with 0s.
      unmark(table, size, row, i);

想想一个被两个皇后攻击的单元格会发生什么:你会在放置第一个皇后时标记它,进行递归调用(或更多,没关系),再次标记它,然后在返回时取消标记它第二次递归调用。然后你会忘记在第一次递归调用期间放置的皇后仍在攻击它。

请注意,在上述每个错误的解决方案中,错误放置的皇后之一被另外两个攻击,并且它也被放置在正在攻击它的另外两个之前。

显然,这会导致算法找到更多的解决方案,从而进行更多的递归调用。

经典解决方案

解决问题的正确方法是使用算法生成排列。让:

col[i] = the column of the queen placed on row i

然后您需要在col 数组中生成有效的排列。我将把必要的条件留作练习。

当然,您也可以通过递增和递减计数器来修正您的方法,而不是只使用1/0

【讨论】:

我做了一些更改,我想出了这个解决方案pastebin.com/u2KtQs7S,但它给了我n &gt;= 9 值的错误结果。例如,对于n=9,它输出322 解决方案而不是352。我该怎么办? @cristid9 - 标记代码看起来仍然很奇怪,很可能是这样。你能说服自己你用 +/- 做的事情是正确的吗?如果没有,我建议您尝试其他方法,例如排列算法。

以上是关于为啥这个解决方案对 n 个皇后的复杂性如此之大?的主要内容,如果未能解决你的问题,请参考以下文章

为啥数组声明的顺序对性能影响如此之大?

为啥“快速排序”算法的这两种变体在性能上差别如此之大?

为啥切片线程对使用 ffmpeg x264 的实时编码影响如此之大?

如何获得 N 个皇后验证器的更好时间复杂度

为啥我的 N 个皇后问题的回溯解决方案不起作用?

n皇后问题 c++