为什么这个解决方案的复杂性对于n个女王来说如此之大?

时间:2015-06-01 19:17:12

标签: c algorithm recursion time-complexity n-queens

我正在尝试使用矩阵来代表棋盘来解决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;
}

正如您所看到的,我正在使用大量的int数组来表示棋盘。矩阵的大小为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不对程序移动的缓慢方式负责。

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

你能帮助我吗?

1 个答案:

答案 0 :(得分:2)

第二种解决方案是错误的。它输出的解决方案比平时多。例如,对于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来修复您的方法。