枚举思想——算法学习(一)

枚举思想——算法学习(一)


前言

在算法学习的道路上,枚举思想是一种简单却强大的思想。作为一种暴力求解方法,枚举算法通过穷尽所有可能的解,从中找到满足条件的最优解或所有解。虽然它看似“低效”,但在解决许多实际问题时却显得直观且有效,尤其是在问题规模可控的情况下。

(本文代码均使用C#语言)

概念

枚举是一种通过穷举所有可能的情况来解决问题的算法思想。它的核心在于全面性简单性:即按照某种规则,逐一列举出问题的所有可能解,然后通过判断筛选出符合条件的解。

枚举的特点

  1. 直观易懂
    枚举算法通常不需要复杂的数学推导或高级技巧,直接通过遍历所有可能情况即可解决问题,因此非常适合初学者理解和使用。

  2. 适用范围广
    枚举可以用于许多场景,例如搜索所有排列、组合、子集,验证某种结构或解决约束满足问题(CSP)。

  3. 时间复杂度高
    由于需要遍历所有可能的解,枚举的时间复杂度通常较高,容易随着问题规模的增加而变得不可接受,因此一般用于规模较小的问题

枚举算法的核心步骤

  1. 定义解空间
    明确所有可能的解构成的范围。解空间可以是一个排列、一组数字的组合,甚至是一个几何图形。

  2. 遍历解空间
    通过循环或递归的方式,逐一生成所有可能的解。

  3. 判断筛选
    对每个可能的解进行判断,筛选出符合问题条件的解。

解空间

解空间是所有可能解构成的集合,等于其相关变量值域的笛卡尔积

最大/最小值问题

枚举法是列举解空间中的所有元素,以找到问题的合法解或最优解

问题形式一般为:给定一组数字,输出其中最大/最小的数字

例题

例:

解答

本题使用枚举思想求解,首先定义一个最大值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. 解空间:对于最大值问题,解空间为数组元素的下标组成的集合{ 1 , 2 , ... , n },是一个1维的解空间
  2. 时间复杂度: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; //回溯,当一轮枚举结束后,把使用标记重置,进行下一轮枚举
        }
    }
 }

分析

  1. 解空间:在本题中,解空间的维度依赖于输入n,解空间规模应为n!
  2. 时间复杂度:递归树的分支数是变化的,从 n 到 1,递归的总次数与解空间规模 n! 成正比。因此这个方法的时间复杂度为O(n*n!)
posted @ 2024-12-19 17:48  CloverJoyi  阅读(9)  评论(0编辑  收藏  举报