Theory&Thinkings #
- 为什么要用枚举?
- 解准确且全面
- 实现简单,通过循环或递归实现
- 执行效率提升空间通常较大
具体方法:
- 枚举对象
- 枚举过程
- 验证解
不是所有问题都有巧妙的解决方案,没有想到就要进一步做优化,对于枚举来说:压缩枚举空间。
枚举算法
也称之为穷举算法,就是按照问题本身的性质,一 一列举出该问题所有可能的解, 并在列举的过程中,逐一检验每个可能解是否是问题的真正解。若是则采纳这个解; 否则抛弃它。
- 不能遗漏,否则可能导致结果不正确
- 不要重复,否则可能导致效率比较低
枚举算法设计步骤
- 确定枚举对象 枚举对象也可以理解为是问题解的表达形式,一般需要 用若干参数 (p1, p2, ……, pk) 来描述
- 参数之间需要相互独立,而且参数数目越少,问题解搜索空间的维度也相应地小
- 每个参数的取值范围越小,问题解的搜索空间也越小
- 优化模型 针对问题特征,优化对象模型
- 逐一列举可能解 根据枚举对象的参数构造循环 (也可以递归),一一 列举其表达式的每一种取值情况。
- 优化过程 针对对象特征,优化列举和验证过程
- 逐一验证可能解 根据问题解的要求,一一验证枚举对象表达式的每一 个取值,如果满足条件,则采纳它; 否则,抛弃之。
e.g.s #
数组配对 #
给你一个长度为n的数组A和一个正整数k,问从数组中任选两个数使其和是k的倍数,有多少种选法?对于数组 a1=1 , a2=2 , a3=2 而言:
(a1,a2) 和 (a2,a1)被认为是同一种选法;
(a1,a2) 和 (a1,a3)被认为是不同的选法。
- 朴素方法
- 枚举对象:𝑂𝑏𝑗(𝑎𝑖,𝑎𝑗),1≤ 𝑖 < 𝑗≤ 𝑛
- 逐一列举
- 逐一验证
- 存在问题:时间复杂度较高:𝑂(𝑛2)
- 如果问题规模较大,求解问题需要的时间也呈指数级别增长
- 尝试改进:如何减少问题空间?如何更加有效的枚举?
- 利用桶思路,将原问题之间用 mod 运算降低到一个非常低的解空间中再进行枚举
- 枚举对象:𝑂𝑏𝑗(𝑎𝑖,𝑎𝑗),1≤ 𝑖 < 𝑗≤ 𝑛
- 逐一列举:
- 预处理: (𝑎𝑖 + 𝑎𝑗)%𝑘 = 0
- 逐一验证:(𝑎𝑖%𝑘 + 𝑎𝑗%𝑘)%𝑘 = 0
- 求解细节:
- 会变成两个单独数字的配对问题,存在两种情况
- 两个数字在同一桶内:组合数公式
- 两数字不在同一桶内:直接相乘,相当于组合公式
void work() {
LL ans = 0;
for (int i = 0; i < k; i++) {
int j = (k - i) % k;
if (j < i) break;
// 避免重复 else if (j == i) // 0或者k/2
ans += 1LL * b[i] * (b[i] - 1) / 2;
else ans += 1LL * b[i] * b[j];
}
cout << ans << endl;
}
绳子切割问题 #
有N条绳子,它们的长度分别为𝐿_𝑖 (≤1000)。如果从它们中切割出 K 条长度相同的绳子,这K条绳子每条最长能有多长?答案保留到小数点后2位。
枚举法的典型应用
我要枚举出一个最长长度出来,可以直接套用二分枚举的算法进行实现
- 枚举对象:绳子长度
- 枚举过程:二分枚举
- 验证方法:到每个已知绳子中验证其切割结果是否为 k
int n, k; // 全局变量,数组长度和目标数值 int a[MAX_N]; //全局变量,待查找数组
int binarySearch() {
int lb = 0;
ub = n; // 初始化解的存在范围
while (ub - lb > 0) {
int mid = (lb + ub) / 2;
if (a[mid] >= k) ub = mid;
// 解的范围更新为 [lb, mid] else
lb = mid + 1;
// 解的范围更新为 [mid + 1, ub]
}
return lb; // lb即为答案
}
关于二分法的边界问题及两种写法_二分法边界处理-CSDN博客
二分枚举基本原理
一般化模型:
- “求满足某个条件𝑪(𝒙) 的最小的𝒙”,其中𝐶(𝑥) 满足性质: 如果任意𝑥满足𝐶(𝑥),则所有 的𝑥 ≥ 𝑥也满足𝐶(𝑥)
- “求满足某个条件𝑪(𝒙) 的最大的𝒙”,其中𝐶(𝑥) 满足性质: 如果任意𝑥满足𝐶(𝑥),则所有 的𝑥 ≤ 𝑥也满足𝐶(𝑥)
算法(模型1)
1. 设置x的初始范围,即下界lb和上界ub
2. While (ub > lb)
1)𝑚𝑖𝑑 = (𝑙𝑏 + 𝑢𝑏)/2;
2)if (C(mid))
更新上界: 𝑢𝑏 ← 𝑚𝑖𝑑;
3)else
更新下界:𝑙𝑏 ← 𝑚𝑖𝑑+1
算法(模型2)
1. 设置x的初始范围,即下界lb和上界ub
2. While (ub > lb)
1)𝑚𝑖𝑑 = (𝑙𝑏 + 𝑢𝑏)/2;
2)if (C(mid))
更新下界: 𝑙𝑏 ← 𝑚𝑖𝑑;
3)else
更新上界:𝑢𝑏 ← 𝑚𝑖𝑑−1
移除石头——最重要枚举问题 #
有一条河,河中间有一些石头,石头的数量以及相邻两块石头之间的距离已知。现在可以移除一些石头,假设最多可以移除m块石头(注意:首尾两块石头不可以移除,且假定所有的石头都处于同一条直线),问最多移除m块石头后相邻两块石头之间的最小距离的最大值是多少?
- 传统的贪心策略,二叉树搜索策略要么不可行,要么复杂度过高,需要一种高效的遍历算法——二分枚举
- 问题建模:
- 求满足条件𝑪(𝒅) 的最大值𝑑,其中𝑪(𝒅) 描述为:𝑪 𝒅 ≔ 最多移除 m 个石头后最近两个石头的距离不小于 d
- 问题转换:
- 求满足条件𝑪(𝒅) 的最大值𝑑,其中𝑪(𝒅) 描述为:𝑪 𝒅 ≔ 最多移除 m 个石头后任意相邻两个石头的距离不小于 d
- 算法思路:
- 应用贪心思想
- 循环依次考虑相邻石头,如果距离小于 d,则去掉一个石头;
- 如果任意相邻石头的距离都不小于 d,则返回 true;
- 如果移除的石头个数大于 m,则返回 false
函数求根问题 #
有形如:𝑎𝑥^3+𝑏𝑥^2+𝑐𝑥+𝑑=0 的一元三次方程。给出该方程中各项的系数(a,b,c,d 均为实数),并约定该方程存在三个不同实根(根的范围在-100至100之间),且根与根之差的绝对值≥1。请设计算法求解三个实根。
输入格式
多组输入(不超过20组数据,读入以EOF结尾)
每组输入四个数字,a,b,c,d (4个数绝对值小等于30)
输出格式
每组输出一行结果,由小到大输出3个根(保留两位有效数字)
Problem A. 优美的立方质数 #
题目描述
如果一个质数能被表示为三个不同的质数的和的形式,那么我们称它为立方质数。现在给你一个数n,判断它是不是立方质数。
输入数据
正整数n,n<=1000
输出数据
Yes或者No
Problem B. 课堂作业 -6-2 木棍切割问题 #
Problem C. 李老师的幸运数字 #
题目描述
李老师的lucky number 是3,5和7,他爱屋及乌,还把所有质因数只有3,5,7的数字认定为lucky number,比如9, 15, 21, 25等等。请聪明的你帮忙算一算小于等于x的lucky number有多少个?
输入数据
一个正整数x,3=<x<=1000000000000
输出数据
小于等于x的lucky number的个数。
Problem D. 思维之花 - 简单背包 #
题目描述
李老师正准备暑假旅行,他有一个容量为L的行李箱和n个物品(n不超过20),每个物品都有自己的体积,物品可以放入行李箱,但行李箱中物品的总体积不能超过行李箱容量,李老师现在想知道他有多少种携带物品的方案(一个物品都不带也算一种方案)
输入数据
第一行为两个正整数n和L,分别代表物品总数和行李箱容量,n<=20,L<=1e9 接下来一行为n个正整数vi,代表第i个物品的体积,vi<=1e8
输出数据
方案数
Problem E. 课堂作业 -7-2 石头移除问题 #
题目描述
有一条河,河中间有一些石头,已知石头的数量和相邻两块石头之间的距离。现在可以移除一些石头,问最多移除m块石头后(首尾两块石头不可以移除),相邻两块石头之间的距离的最小值最大是多少。
输入数据
第一行输入两个数字,n(2<=n<=1000)为石头的个数,m(0<=m<=n-2)为可移除的石头数目 随后n-1个数字,表示顺序和相邻两块石头的距离d(d<=1000)
输出数据
输出最小距离的最大值
Problem F. 数对选择 #
题目描述
给你一个长度为n的数组和一个正整数k,问从数组中任选两个数使其和是k的倍数,有多少种选法。 对于数组a1=1 , a2=2 , a3=2而言:
(a1,a2)和(a2,a1)被认为是同一种选法;
(a1,a2)和(a1,a3)被认为是不同的选法。
输入数据
第一行有两个正整数n,k。n<=1000000,k<=1000000 第二行有n个正整数,每个数的大小不超过1e9
输出数据
选出一对数使其和是k的倍数的选法个数
- Q:为什么使用 1LL 进行处理?
- 在这段代码中,乘以 LL 的目的是为了确保在进行乘法运算时使用长整型(long long)进行计算,避免整数溢出问题。
- 具体来说,1LL 是一个将数字 1 转换为长整型的方式。在这里,1LL * (bucket[i] * (bucket[i] - 1) / 2) 和 1LL * (bucket[i] * bucket[j]) 中的乘法运算都会使用长整型进行计算。
- 这是因为当进行乘法运算时,如果操作数中有一个是长整型,那么结果也将是长整型。在某些情况下,特别是当计算的结果可能超过整数范围时,使用长整型是很重要的,以防止溢出。如果不使用 1LL 进行转换,整个表达式的结果可能会被截断为普通整型,导致错误的结果。