第二章 枚举算法(大道至简)
枚举算法的基本原理
1. 思考
“水仙花数”也称为n = 3 的Narcissus数,定义为 n 等于3的Narcissus数,如:153 = 1^3 + 5^3 + 3^3, 请找出所有的水仙花数。
解决:列举所有的3位数(100~999),判断各位数字的立方和是否等于该数本身。
2. 什么是枚举算法
枚举算法也称之为穷举算法,就是按照问题本身的性质,一一列举出所有可能的解,并在列举的过程中,逐一检验每个可能的解是否是问题的真正解。若是则采纳这个解,否则抛弃它。
在枚举的过程中注意两个基本原则:(1)不能遗漏,可能导致结果不正确;(2)不能重复,可能导致算法运算效率较低。
枚举算法的特点:枚举算法的解具有准确和全面的特点;其次枚举算法实现简单,一般通过循环/递归实现;执行效率较低,算法有较大的优化空间。
枚举算法的设计步骤:
1)确定枚举对象。枚举对象也可以理解为是问题解的表达形式,一般需要用若干参数(p1, p2, ..., pk)来描述。参数之间需要相互独立,而且参数数目越小问题解的搜索空间的维度也相应地小,每个参数的取值范围越小,问题解的搜索空间也越小。
2)逐一列举所有可能解。根据枚举对象的参数构造循环,一一列举其表达式的每一种取值情况。
3)逐一验证可能解。根据问题解的要求,一一验证枚举对象表达式的每一个取值,如果满足条件则采纳它,否则抛弃。
3. 模糊数字问题的枚举算法
任务描述:一张单据上有一个5位数的编码,因为保管不善,其百威数已经变的模糊不清,但是知道这个5位数是57和67的倍数。现在要设计一个算法,输出所有满足这些条件的5位数,并统计这样的数的个数。
输入:每一行对应一个测试样例,每一行包含4个数字,依次是万位数,千位数,十位数和个位数。最后一行包含4个-1,表示输入结束。
输出:每组测试样例的结果输出占一行,第一个数字表示满足条件的编码个数,后面按升序输出所有满足条件的编码,数字与数字之间用空格隔开。
分析过程:首先获得枚举对象Obj(h):记h百位数数字,h = 0 ~ 9;逐一列举 for (h=0; h<10; h++);逐一验证 19h95 是否能够同时整数 57 和 67。
4. 百钱百鸡问题的枚举算法设计
任务描述:我国古代数学家张丘建在《算经》中出了一道题,“鸡翁一,值钱五;鸡母一,值钱三;鸡雏三,值钱一。百钱买百鸡,问鸡翁、鸡母、鸡雏各几何?”现在假定各种鸡的价格不变,拥有的钱数为m,需要购买的鸡数为n,试求出所有可能的购买方案总数。
输入:每行对应一个测试样例,每一行包含2个数字,分别为 n 和 m。最后一行包含2个-1,表示输入结束。
输出:每组测试样例的结果输出占一行,输出可能购买的方案总数。
分析过程:首先确定枚举对象 obj(n1, n2, n3) 分别代表鸡翁、鸡母。鸡雏的数量,其取值均为0~n;接下来逐一列举(for 循环)逐一验证,验证条件是 n1 + n2 + n3 == n && 5*n1 + 3*n2 + n3/3 == m。
算法优化:上述直接枚举的算法复杂度为O(n^3),由于枚举对象n1, n2, n3不是相互独立的,因此其中一个参数可以用总和 n 和 ;另外两根参数表示,则此时枚举对象为两个参数 obj(n1, n2),鸡翁数为 n1,鸡母数为 n2,鸡雏数为 n-n1-n2。则可将算法中的
三层 for 循环优化为两层 for 循环,提高算法的效率。
5. 数组配对问题的枚举算法设计与枚举对象的变化策略
任务描述:给定长度为n的数组A和一个正整数k,问从数组中任选两个数使其和为k的倍数,有多少中选择方法,对于数组a1 = 1, a2 = 2, a3 = 3而言:(a1, a2) 和 (a2, a1) 被认为是同一种选法,(a1, a2) 和 (a1, a3)被认为是不同选法。
输入:第一行有两个正整数 n, k,其中 n<=1000000, k<=1000 第二行有 n 个正整数,每个数的大小不超过 1e9。
输出:选出一对数使其和是k的倍数的选法个数。
分析过程:确定枚举对象为 obj(ai, aj) 1 <= i, j <= n。逐一列举注意验证,双层 for 循环。
算法优化:根据题目描述,取出的两个数是k的倍数,则有(ai + aj)%k = 0,可以推出 (ai%k + aj%k)%k = 0成立,因此可以对输入数据进行一个分组,其中对k取模后的值相同的放在同一组,同时用 bi 记录a%k = i组的数据个数,若第 i 组和第 j 组满足对k取模后的和是k的倍数,则属于这两组的数据均可以满足该条件,因此所计算的数目为 bi * bj,其中i, j取值范围为 0~k-1。根据优化或的思路,该算法可以用单层 for 循环表示,且循环的上限为 k 的大小,大大的提高了算法的运行效率。
6. 枚举算法的优化1(二分枚举法)
任务描述:给定长度为n的单调不下降数列{a0, ..., an-1} 和目标数k,求满足条件ai>=k的最小值 i。不存在的情况下输出n。
输入:n = 5, a = {2,3,3,5,6}, k=4;输出:3
分析过程:枚举对象 i,0 <= i <= n-1,一层 for循环,按照 i 从小到大遍历并逐一验证 ai >= k,时间复杂度为O(n)
算法优化:二分法,每次和中间的元素比较,跨越式枚举,不断更新上界和下届找到目标值,时间复杂度为O(logn)。
7. 枚举算法的优化2
任务描述:有一条河,河中间有一些石头,石头的数量以及相邻两块石头之间的距离已知。现在可以移除一些石头,假设最多可以移除m块石头(注意:首尾两块石头不可以移除,且假定所有的石头都处于同一条直线)问最多移除m块石头后相邻两块石头之间的最小距离的最大值是多少。
输入:多组输入(<=20组数据,读入以EOF结尾),每组第一行输入两个数字:n(2<=n<=1000)为石头的个数,m(0<=m<=n-2)为可移除的石头数目,随后n-1个正整数,表示顺序相邻两个石头的距离d(d <= 1000)。
输出:每组输出一行结果,表示最大值。
分析过程:分析枚举对象,从 n 块石头中选择 m 块石头移走,由于m是动态的,因此构造 for 循环较困难,可以选择递归的方式,列举不定多元组的所有元素。该算法的时间复杂度为O(2^n)。
算法优化:将问题建立模型可以描述为求满足条件C(d)的最大值d,其中C(d)描述为C(d) = 最多移除m个石头后最近两个石头的距离不小于d,将其转化为求解最多移除m个石头后任意相邻两个石头的距离不小于d。可以应用贪心思想,首先循环依次考虑相邻石头,如果距离小于d,则去掉一个石头;如果任意相邻石头的距离都不小于d,则返回true;如果移除的石头个数大于m,则返回false,算法优化后的时间复杂度为O(nlogn)。
8. 小结
1. 枚举算法的基本原理
设计枚举对象,一一列举(for循环或递归),逐一验证。
2. 简单的枚举问题
模糊数字、n钱m鸡等问题
3. 枚举的优化方法
针对枚举对象的优化:
百钱百鸡问题:参数的独立性
数组配对:枚举对象的转换
针对枚举过程的优化:
二分枚举的一般模型
经典:二分查找,绳子切割和石头移除