C++算法之排列枚举
一、介绍
枚举排列时,我们考虑了如下几项内容:
-
排列的表示形式:数组。
-
排序的大小:按照字典序大小规定。
字典序,又叫字母序,是规定两个序列比较大小的一种方式。其规则是对于两个序列a
和b
:
从第一个字母开始比较,如果在第i个位置满足,i没有超过两个序列的长度,小于i处两个序列对应位置的元素都相等,而第i
位两个序列对应位置元素不等的话,则若a[i] < b[i]
,那么序列a
小于序列b
,否则序列b小于序列a。
若从第一个位置直到其中一个序列的最后一个位置都相等的话,则比较a
和b
的长度,若a
的长度小于b
,则序列a
小于序列b
(此时a
是b
的前缀),而如果b
序列的长度小于a
,那么序列b
小于序列a
。
若两个序列长度相等,并且所有元素相等,则序列a
等于序列b
。 举例:
abc < bbc // 因为第一个字母a < b
ab < abc // 因为两个串前面所有对应位置字母相同,但第一个串长度小于第二个串
ac > abb // 因为第二个字母c > b
生成下一个排列的方式:调用STL
中的next_permutation
函数。
二、取宝石问题
假设在一个大房间有n
个宝石,每一处宝石用一个坐标(x, y)
表示。如果你从任意一处宝石的地方出发,依次经过每个放宝石的地方并取走宝石,最终要求回到出发地点,问最短需要走的距离是多少。
在这个情境里,经过不同地点的顺序会改变最终的行走距离。所以,我们要枚举的就是经过1~n
一共n
个位置的顺序。
用next_permutation
函数解决“取宝石问题”,因为要用枚举法解决第一个问题,所以,代入到题目的情境中,我们可以设计如下算法:
1、枚举所有n
个点的排列
2、维护最短距离。检查新枚举的排列产生的行走距离是否比之前的最短距离还短。如果短,就更新答案。
三、代码实现
代码如下(示例):
#include <bits/stdc++.h>
#define N 15
using namespace std;
int n, id[N];
double x[N], y[N];
// 求两个点(x_1, y_1)和(x_2, y_2)之间的直线距离
double dis(double x_1, double y_1, double x_2, double y_2) {
double dx = x_1 - x_2;
double dy = y_1 - y_2;
return sqrt(dx * dx + dy * dy);
}
int main() {
cin >> n;
for (int i = 1; i <= n; ++i) {
cin >> x[i] >> y[i];
id[i] = i; // 因为我们枚举标号的排列,所以要将标号存进数组里
}
double ans = -1; // 因为最开始ans中没有值,所以我们可以将其设置为一个不合法的值
// 用do...while循环是为了防止第一次调用时数组id中的值已经被重排
// 所以会导致标号为1, 2, ..., n的排列没有被计算。
do {
// 求解按照id[1], id[2], ..., id[n], id[1]作为行走路线的总距离。
double cur = dis(x[id[1]], y[id[1]], x[id[n]], y[id[n]]);
for (int i = 1; i < n; ++i)
cur += dis(x[id[i]], y[id[i]], x[id[i + 1]], y[id[i + 1]]);
// 如果当前路线的总距离小于之前最优解,就更新。
if (ans < 0 || cur < ans) ans = cur;
} while (next_permutation(id + 1, id + n + 1));
// 输出答案,这里因为是浮点数,所以我们设置精度为4。
cout << setprecision(4) << ans << endl;
return 0;
}
四、复杂度分析
使用next_permutation
函数枚举排列代码的复杂度分析:do while
循环的循环次数,也就是长度为n
的排列个数为n!
。调用next_permutation
函数一次的复杂度为O(n)
,所以枚举排列的复杂度为O(n!×n)
。