计算机

暴力枚举

暴力枚举(brute force)指的是枚举所有可能的情况并一一检验的方法。

在不优化的情况下,暴力枚举的时间复杂度通常是指数级。

基本枚举

枚举全排列

以 4 个数为例,4 个数的全排列有 24 种,按字典序排列如下:

1 2 3 4
1 2 4 3
1 3 2 4
1 3 4 2
1 4 2 3
1 4 3 2
2 1 3 4
2 1 4 3
2 3 1 4
2 3 4 1
2 4 1 3
2 4 3 1
3 1 2 4
3 1 4 2
3 2 1 4
3 2 4 1
3 4 1 2
3 4 2 1
4 1 2 3
4 1 3 2
4 2 1 3
4 2 3 1
4 3 1 2
4 3 2 1

思想

枚举字典序的下一个全排列时,我们当然想贪心地尽可能找靠右边的,这样“增量”小。

但是,如果排列的某一个后缀是降序的,说明这里作为一个子排列已经极大,不能再找更大字典序了。所以,我们需要找到最末的升序对。

找到最后升序对 a_i < a_{i + 1} 后,根据贪心原则,我们只寻找后缀 a_i, a_{i + 1}, \dots, a_n 里的下一个排列。此时 a_i 必须增大一位,增大到哪里呢?显然就是增大到大于 a_i 的最小数。于是,我们将 a_i 和大于 a_i 的最小数 a_k 交换位置。

交换完位置后,a_i(原来的 a_k 固定了),我们需要将剩下的数放进后面的空位 a_{i + 1}, \dots, a_n,显然升序是字典序最小的排列,所以我们把这些数按升序填进去。

算法

我们可以尝试从当前全排列转移到字典序上的下一个全排列,算法如下:

  • 寻找最后一个相邻升序对 a_i < a_{i + 1},若找不到,则说明已经是最后一个排列;
  • 寻找 a_{i + 1}, \dots, a_n 中大于 a_i 的最后一个数 a_k;
  • 交换 a_i 和 a_k;
  • 对 a_{i + 1}, \dots, a_n 升序排序。由于 a_{i + 1}, \dots, a_n 此时一定降序,这个过程可以翻转实现。

这一算法对应 STL 中的 next_permutation 函数。

时间复杂度

单次运行的最坏复杂度为 O(n),但是平均复杂度是常数 O(1)。

证明:算法的时间复杂度取决于最后升序对的出现位置,也就是取决于排列的最长降序后缀。如果最长降序后缀为 L,则 next_permutation 的时间复杂度为 O(L)。

降序后缀长度大于等于 L!,要求最后的 L 个元素降序排列,由消序法得到情况数是 n! / L!。而后缀长为 L 时时间复杂度为 O(L),可以分摊给大于等于 1, 2, \dots, L 的后缀一个 O(1),因此运行 n! 次,总的复杂度就是:

T(n) = \sum_{L = 1}^n O(L) \left(\frac{n!}{(L - 1)!} - \frac{n!}{L!}\right) = n!\sum_{L = 1}^n \frac{(L - 1) O(L)}{L!} = n! \sum_{L = 2}^n \frac{1}{(L - 2)!} O(1).

而由微积分,有

\sum_{i = 0}^n \frac{1}{i!} < \sum_{i = 0}^{\infty} \frac{1}{i!} = e.

所以 T(n) = O(1) \cdot n!,即平均时间复杂度为 O(1)。

实现

bool next_permutation(int *a, const int n) {
	int i;
	for (i = n - 2; i >= 0; i--)
	{
		if (a[i] < a[i + 1])
		{
			break;
		}
	}
	if (i == -1)
	{
		return false;
	}
	int k;
	for (k = n - 1; k > i; k--)
	{
		if (a[k] > a[i])
		{
			break;
		}
	}
	swap(a[i], a[k]);
	reverse(a + i + 1, a + n);
	return true;
}

这一函数的调用将使用 do-while 循环:

int main()
{
    do {
        for (size_t i = 0; i < n; i++)
        {
            cout << a[i] << " ";
        }
        cout << "\n";
    } while (next_permutation(a, n));
}

STL

在 STL 中,std::next_permutation 有相似的实现,通过传入首尾指针实现排序。

枚举组合

枚举从 n 个元素选 r 个的全部组合情况。

例如,从 5 个元素中选 3 个一共有以下情况:

1 2 3
1 2 4
1 2 5
1 3 4
1 3 5
1 4 5
2 3 4
2 3 5
2 4 5
3 4 5

算法流程为:

  • 从后往前数,找到第一个满足 a_i \ne n - r + i 的位置 i,若找不到,则说明已经是最后一个组合;
  • 令 a_i 自增 1;
  • 对 j \in \{i + 1, i + 2, \dots, r\},令 a_j = a_{j - 1} + 1。
BEYOND THE ARTICLE

读完了,有什么想法?

阅读已保存的问答示例,并循着引用回到原文。

查看公开问答示例
— 本文完 — 返回文章列表