枚举思想——算法学习(一)
枚举思想——算法学习(一)
前言
在算法学习的道路上,枚举思想是一种简单却强大的思想。作为一种暴力求解方法,枚举算法通过穷尽所有可能的解,从中找到满足条件的最优解或所有解。虽然它看似“低效”,但在解决许多实际问题时却显得直观且有效,尤其是在问题规模可控的情况下。
(本文代码均使用C#语言)
概念
枚举是一种通过穷举所有可能的情况来解决问题的算法思想。它的核心在于全面性和简单性:即按照某种规则,逐一列举出问题的所有可能解,然后通过判断筛选出符合条件的解。
枚举的特点
-
直观易懂
枚举算法通常不需要复杂的数学推导或高级技巧,直接通过遍历所有可能情况即可解决问题,因此非常适合初学者理解和使用。 -
适用范围广
枚举可以用于许多场景,例如搜索所有排列、组合、子集,验证某种结构或解决约束满足问题(CSP)。 -
时间复杂度高
由于需要遍历所有可能的解,枚举的时间复杂度通常较高,容易随着问题规模的增加而变得不可接受,因此一般用于规模较小的问题。
枚举算法的核心步骤
-
定义解空间
明确所有可能的解构成的范围。解空间可以是一个排列、一组数字的组合,甚至是一个几何图形。 -
遍历解空间
通过循环或递归的方式,逐一生成所有可能的解。 -
判断筛选
对每个可能的解进行判断,筛选出符合问题条件的解。
解空间
解空间是所有可能解构成的集合,等于其相关变量值域的笛卡尔积
最大/最小值问题
枚举法是列举解空间中的所有元素,以找到问题的合法解或最优解
问题形式一般为:给定一组数字,输出其中最大/最小的数字
例题
例:
解答
本题使用枚举思想求解,首先定义一个最大值max,然后将max置为最小,然后枚举所有数据与max进行比较,若大于max则将max更新为此数,若小于max则比较下一个。
代码
using System;
class Test
{
static void Main(string[] args) {
var input = Console.ReadLine();
var n =int.Parse(input);
input = Console.ReadLine();
string[] inputs=input.Split(' ');//将输入的字符串按空格分隔保存为数组
int[] a= new int[n];
for(int i = 0; i < n; i++) {
a[i]=int.Parse(inputs[i]);
}
int max=int.MinValue;
for(int i = 0; i < n; ++i) {
if (a[i]>max)
max = a[i];
}
Console.Write(max);
}
}
分析
- 解空间:对于最大值问题,解空间为数组元素的下标组成的集合{ 1 , 2 , ... , n },是一个1维的解空间
- 时间复杂度:O( n )
全排列问题(递归实现枚举)
题干
n排列是指将数字1∼n以某种顺序排序形成的数列,如3,4,2,1是一个4排列。而n全排列是所有n排列形成的集合。
输入说明
一个整数n(1≤n≤9)。
输出说明
若干行,每行一个n排列,数字间用空格分隔,排列按照字典序排序。
输入样例
3
输出样例
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
解答
与上一题不同,本题中枚举的循环次数由n决定,利用简单的枚举方法无法确定枚举时的循环次数,此时可以利用递归调用的方式实现枚举。
(递归:即自己调用自己)
需要注意的是,全排列在一次排列中不允许一个数字重复出现,所以需要定义一个bool类数组used,用于记录一个数是否已经被枚举过,防止重复数字。在一层递归结束,回溯到上一层中时,需要将相应的used标记重置。
伪代码
Function permutation(P,idx,used):
if(idx > n) then
print P;
return;
end
for i <- 1 to n do
if Used[i] = true then continue;
P[idx] <- i;
Used[i] <- true;
permutation(P,idx + 1,used);
Used[i] <- false;
end
完整代码
using System;
class Test {
static bool[] used;
static void Main() {
int n = int.Parse(Console.ReadLine());
int[] P = new int[n + 1];
used = new bool[n + 1];//定义一个bool类数组用作数字是否被使用过的标记
Permutation(P, 1, n);
}
static void Permutation(int[] P, int idx, int n) {
//边界条件,达成条件时停止循环
if (idx > n) {
for (int i = 0; i < n; ++i)
Console.Write(P[i].ToString() + " "); //停止循环后输出结果
Console.WriteLine();
return;
}
for (int i = 1; i <= n; i++) {
if (used[i]) continue; //如果数字i已经枚举过了,就跳过这个数
P[idx - 1] = i; //将本次枚举的数字i加入数组
used[i] = true; //将i标记为已使用,防止重复枚举
Permutation(P, idx + 1, n); //递归调用,进行下一层枚举,并将idx加一
used[i] = false; //回溯,当一轮枚举结束后,把使用标记重置,进行下一轮枚举
}
}
}
分析
- 解空间:在本题中,解空间的维度依赖于输入n,解空间规模应为n!
- 时间复杂度:递归树的分支数是变化的,从 n 到 1,递归的总次数与解空间规模 n! 成正比。因此这个方法的时间复杂度为O(n*n!)