登录以参加训练计划
3585(洛谷P2118)这道题(NOIP 2014 普及组 T2)虽然看起来像是数学题,但结合数据范围来看,其实是一道非常适合暴力枚举的题目。
关键理解:问题的解空间是所有 且 的组合。
以下是解题思路提示:
1. 突破口:数据范围
请仔细看题目最后的数据范围说明:
这一行非常关键! 最大只有 100。这意味着, 和 的取值范围都在 1 到 100 之间。 如果我们写一个双重循环,把 从 1 枚举到 100,把 从 1 枚举到 100,总共只运算 次。这就好比在一秒钟内数一万个数,对计算机来说简直是“秒杀”。
2. 核心逻辑:打擂台
既然可以把所有可能的分子分母组合都试一遍,我们就可以用“打擂台”的方法找最佳答案。
- 准备工作:定义两个变量
ans_a和ans_b用来存当前找到的最好的分子和分母。初始值可以设得比较离谱(比如让比值非常大),或者直接用 1 和 1(如果题目保证答案存在)。 - 循环枚举:
- 外层循环
i代表分子(1 到 )。 - 内层循环
j代表分母(1 到 )。
- 外层循环
- 判断条件(只要满足以下三个条件,就有资格挑战擂主):
- 互质:题目要求
gcd(i, j) == 1。 - 不小于原值:题目要求 。
- 小技巧:为了避免浮点数除法的精度问题,你可以把除法变成乘法:
i * B >= A * j(注意数据范围, 会超过int,建议用long long或double)。
- 小技巧:为了避免浮点数除法的精度问题,你可以把除法变成乘法:
- 更接近:当前的差值 比之前记录的最小差值还要小。
- 互质:题目要求
- 更新答案:如果满足上述条件,就把
ans_a更新为i,ans_b更新为j。
3. 细节处理
- 浮点数:这道题涉及比大小,最简单的方法是直接把分子分母强转成
double进行计算。因为 很小,精度误差通常不会影响结果。 - 互质函数:你需要写一个
gcd函数,或者使用 C++ 自带的__gcd(a, b)(在<algorithm>头文件中)。 - 初始值:一开始的“擂主”要设得足够“烂”,比如设一个比值很大的数,确保第一个合法的组合能替换掉它。
4. 代码结构框架
int ans_a = 1000000, ans_b = 1; // 初始设一个很大的比值
for (int i = 1; i <= L; i++) {
for (int j = 1; j <= L; j++) {
// 1. 检查互质
// 2. 检查 i/j >= A/B
// 3. 检查 i/j - A/B < ans_a/ans_b - A/B
// 如果都满足 -> 更新 ans_a, ans_b
}
}
输出 ans_a, ans_b
抓住 很小这个特点,大胆地去枚举吧!加油!
请用启发式的方式详细讲解“解空间”的概念。
关键理解:问题的解空间是所有 且 的组合。
你好!这是一个非常棒的思维切入点。在计算机科学和算法竞赛(OI)中,“解空间”(Solution Space) 是一个核心概念。
把“解题”想象成“找东西”。 解空间就是:哪怕是最笨的方法,你也知道答案一定在这个圈子里。这个“圈子”的所有可能性,就叫解空间。
为了让你彻底理解,我们用**“淘金”**的例子来启发式地拆解它。
第一步:划定“金矿”的边界(定义解空间)
题目让你找两个数 和 ,这就好比你要去挖两块金子。 虽然世界上有很多金子,但题目给了一个强硬的限制:上限 。
- 的可能性:只能是 中的一个。
- 的可能性:只能是 中的一个。
什么是解空间? 想象一个巨大的棋盘或者表格。
- 横坐标是 ,从 1 到 。
- 纵坐标是 ,从 1 到 。
- 这个棋盘上每一个格子 ,都是一个**“候选人”**。
这个棋盘上的所有格子加起来,就是这道题的全解空间。 它的大小是:。
第二步:评估“矿场”的大小(复杂度分析)
作为 OIer,拿到题目的第一反应应该是:“这个矿场大不大?我能不能把每块石头都翻一遍?”
-
如果 : 解空间大小 。 对于计算机(每秒运算 次)来说,翻遍 1 万个格子,只需要 0.0001 秒。 结论:矿场很小,我们可以暴力地把每个格子都检查一遍!
-
如果 : 解空间大小 (100亿)。 计算机要跑几分钟甚至几小时。 结论:矿场太大,不能暴力枚举,需要更聪明的算法(比如数学推导或二分)。
这道题之所以是“普及组”难度,就是因为它的解空间()非常小,允许我们进行全解空间搜索。
第三步:在解空间里“筛选”(约束条件)
现在我们知道答案一定在 这 个格子里。虽然里面有 10000 个组合,但绝大多数都是“废石”。我们需要拿着筛子(题目条件)去过滤。
让我们把解空间里的点分为三类:
-
废点(不合法):
- 题目要求 和 互质。比如 ,最大公约数是 2,不是 1。扔掉!
- 题目要求 。如果算出来的比例比原比例小,扔掉!
-
可行解(合法,但不一定最好):
- 比如 和 假设都满足互质和比例要求。它们都是“金子”,先留着。
-
最优解(我们要找的那个):
- 在所有“可行解”中,我们要找 最小 的那个。
第四步:代码实现的本质
你的代码里的双重循环,本质上就是遍历整个解空间的过程:
// 这一步是在遍历解空间的每一行
for (int i = 1; i <= L; i++) {
// 这一步是在遍历解空间的每一列
for (int j = 1; j <= L; j++) {
// 现在的 (i, j) 就是解空间里的一个具体坐标点
// --- 下面是筛选器 ---
if (gcd(i, j) == 1 && (double)i/j >= (double)A/B) {
// --- 下面是打擂台找最优解 ---
if (这一块金子成色更好) {
更新答案;
}
}
}
}
总结
“解空间”思维对你的启发应该是:
- 先看边界:变量的范围决定了空间的维度和大小。
- 算算大小: 意味着 的空间。
- 决定策略:
- 空间小 暴力枚举(遍历整个空间)。
- 空间大 剪枝/贪心/二分(只去空间里最有可能藏着答案的角落找)。
在这道题里,理解了解空间是 的矩阵,你就完全不用担心“我是不是漏掉了什么情况”,因为你已经把所有可能的 组合全部一网打尽了!这就是暴力算法的完备性。
比赛时如何根据解空间大小规模来筛选算法时间复杂度?
这是一个非常关键的实战技巧!在考场上,“看数据范围猜算法” 是每个 OIer 必须掌握的生存技能。它能帮你快速排除错误的思路,锁定正确的解法。
核心基准:计算机 1 秒钟大约能运行 (1亿到2亿)次基本运算。
为了保险起见,我们通常把目标控制在 次运算以内。
🌟 速查表:数据范围与时间复杂度对应关系
请把这张表刻在脑子里,拿到题目先看 的大小:
| 数据规模 () | 推荐时间复杂度 | 对应算法/技巧举例 | 运算次数估算 |
|---|---|---|---|
| 或 | DFS 搜索、状态压缩 DP | (安全) | |
| Floyd 最短路、区间 DP、高斯消元 | (非常安全) | ||
| 普通 DP、朴素最短路、二维前缀和 | (非常安全) | ||
| (勉强) | 同上,但要注意常数 | (危险,可能会卡) | |
| 二分、归并/快速排序、线段树/树状数组、堆、Set/Map、Dijkstra | (非常安全) | ||
| 双指针、KMP、线性筛、单调队列、Hash | (安全) | ||
| (常数要小) | 线性筛、O(1) 读入优化 | (稍紧,不能有复杂逻辑) | |
| 判断质数、分解质因数、BSGS | (安全) | ||
| 或 | 快速幂、GCD、数论结论、矩阵快速幂 | (秒杀) |
🔧 实战演练:如何推导?
拿到题目后,按照以下步骤思考:
第一步:看题目给出的最大数据范围
通常在题目底部的“说明/提示”部分。
-
案例 1:P2118 [比例简化]
- 题目写着:。
- 分析:解空间涉及两个数 。如果是双重循环,复杂度是 。
- 验证:。
- 对比基准:。
- 结论:可以直接暴力枚举,甚至三层循环 都能过。
-
案例 2:P8813 [乘方]
- 题目写着:。
- 分析:如果写
for(int i=0; i<b; i++),复杂度是 。 - 验证: 次运算。
- 对比基准:。
- 结论:暴力循环绝对 TLE(超时)。
- 修正:必须找 的算法(快速幂)或者利用题目特殊性质(剪枝)变成 。
-
案例 3:常见的排序题
- 题目写着:。
- 分析:如果你写冒泡排序 。
- 验证:。
- 对比基准:。
- 结论:冒泡排序必死。必须用
std::sort() 或者桶排序 ()。
第二步:估算 1 秒能跑多少次
在脑子里默念:“一秒一亿次”。
- 把你的 代入公式算一下。
- 如果算出来的数是 :稳过。
- 如果算出来的数是 :危险,可能需要卡常数(优化代码细节)或者看评测机快不快。
- 如果算出来的数是 :死定了,必须换算法。
⚠️ 两个重要的“坑”
1. 多个变量的乘积
有时候题目不只一个 ,比如图论里的点数 和边数 。
- Floyd 算法:。如果 ,就是 ,会 TLE。
- SPFA / Dijkstra:跟边数 有关。如果 ,用 没问题。
2. “多组数据” (Multi-testcases)
题目可能会说:“输入第一行有一个整数 ,表示有 组数据”。
- 这时候你的总复杂度是:。
- 如果 ,你的算法是 ,总运算 ,稳过。
- 如果 ,你的算法是 ,总运算 ,超时!
总结口诀
20 枚举搜状压 (N<=20 用 DFS/状压) 100 只要三次方 (N<=100 可以用 N^3) 千位两方万平方 (N<=1000 可以 N^2,N=1万 N^2 危险) 十万 N log 很正常 (N=10万 必须 N log N) 百万千万 O N 刚 (N=100万~1000万 必须 O(N)) 更大只能 log 狂 (N再大 只能 O(log N) 或 O(1))
在考场上,先看数据范围,再定算法,这能帮你省下大量走弯路的时间!
这里用到了 algorithm的 __gcd,请列表总结一下常用的algorithm方法和用法说明
你好!作为 OI 教练,我非常推荐你熟练掌握 <algorithm> 库。它是 C++ STL(标准模板库)中最强大的武器库之一。在考场上,手写快排、手写二分虽然是基本功,但直接调用这些经过千锤百炼、极致优化的标准函数,能帮你节省大量时间并避免写出 Bug。
需要注意的是,__gcd 实际上是 GCC 编译器的一个内建函数(非标准 C++,但在 CSP/NOIP 环境下通常可用),在 C++17 标准中,正式的写法是 std::gcd,包含在 <numeric> 头文件中。不过在竞赛中包含 <bits/stdc++.h> 后通常都能直接用。
下面为你总结竞赛中最常用的 <algorithm> 函数:
1. 数学与统计类(最常用)
| 函数名 | 用法示例 | 功能说明 | 复杂度 |
|---|---|---|---|
__gcd |
__gcd(a, b) |
求最大公约数。如果需要最小公倍数,公式是 (a*b)/__gcd(a,b)。 |
|
max |
max(a, b) |
返回较大值。也可以传多个:max({a, b, c, d}) (需C++11)。 |
|
min |
min(a, b) |
返回较小值。同上。 | |
swap |
swap(a, b) |
交换两个变量的值。比自己写 t=a; a=b; b=t; 快且不容易错。 |
|
abs |
abs(a) |
求绝对值(注意:整数在 <stdlib.h>/<cmath>,浮点在 <cmath>,但在 <algorithm> 配合 <bits/stdc++.h> 通常都能混用)。 |
2. 数组极值查找(不用写循环了)
注意: 这些函数返回的是迭代器(指针),如果要获取数值需要加星号 *,如果要获取下标需要减去数组首地址。
| 函数名 | 用法示例 | 功能说明 |
|---|---|---|
max_element |
*max_element(a, a+n) |
返回数组/容器中最大值。 |
min_element |
*min_element(a, a+n) |
返回数组/容器中最小值。 |
教练演示:
int a[5] = {2, 5, 1, 9, 3};
int max_val = *max_element(a, a+5); // 结果是 9
int max_pos = max_element(a, a+5) - a; // 结果是 3 (下标)
3. 排序与去重(必考)
| 函数名 | 用法示例 | 功能说明 | 复杂度 |
|---|---|---|---|
sort |
sort(a, a+n) |
默认从小到大排序。 | |
sort (cmp) |
sort(a, a+n, cmp) |
自定义排序(如从大到小 greater<int>() 或 Lambda)。 |
|
reverse |
reverse(a, a+n) |
将数组/字符串翻转(倒序)。 | |
unique |
unique(a, a+n) |
去重(只能去除相邻的重复元素,所以必须先 sort)。 |
经典组合拳(排序+去重):
// 想要统计数组中有多少个不重复的数
sort(a, a+n); // 1. 先排序
int new_len = unique(a, a+n) - a; // 2. 去重,返回去重后的新长度
// 此时 a[0] 到 a[new_len-1] 就是不重复的元素
4. 二分查找(神器)
前提: 数组必须是有序的(通常是单调递增)。
| 函数名 | 用法示例 | 功能说明 | 返回值 |
|---|---|---|---|
binary_search |
binary_search(a, a+n, k) |
判断 是否存在。 | bool (true/false) |
lower_bound |
lower_bound(a, a+n, k) |
找第一个 的位置。 | 返回迭代器 |
upper_bound |
upper_bound(a, a+n, k) |
找第一个 的位置。 |
教练演示(求下标):
int a[5] = {1, 2, 4, 4, 6};
// 找第一个 >= 4 的位置下标
int idx1 = lower_bound(a, a+5, 4) - a; // 结果是 2
// 找第一个 > 4 的位置下标
int idx2 = upper_bound(a, a+5, 4) - a; // 结果是 4
5. 全排列(暴力枚举帮手)
| 函数名 | 用法示例 | 功能说明 | 复杂度 |
|---|---|---|---|
next_permutation |
next_permutation(a, a+n) |
将数组变成字典序下的下一个排列。 |
典型用法:
int a[3] = {1, 2, 3};
do {
// 输出当前排列:1 2 3 -> 1 3 2 -> ... -> 3 2 1
} while (next_permutation(a, a+3));
6. 赋值与填充
| 函数名 | 用法示例 | 功能说明 |
|---|---|---|
fill |
fill(a, a+n, val) |
将数组区间全部赋值为 val。 |
memset |
memset(a, 0, sizeof(a)) |
注意: 在 <cstring> 头文件。只能赋值 0 或 -1,或者字符。赋值其他整数请用 fill。 |
教练的总结建议
- 头文件:考试时直接写
#include <bits/stdc++.h>,上面这些全都有,不用记具体在哪个头文件里。 - 迭代器理解:
sort,unique,lower_bound等函数操作的都是左闭右开区间[begin, end)。对于数组a,开始是a,结束是a+n(下标为 n 的那个位置,实际取不到)。 - 一定要排序:用
unique、lower_bound、binary_search之前,千万记得先 sort,否则结果是错的!
熟练使用这些函数,你的代码会从几十行缩减到几行,而且跑得飞快!
上面的心得还有什么可以补充的?
你的心得总结已经非常全面和详细了!我来提供一些可以补充的内容,让你的总结更加完整:
一、关于解空间的补充
-
解空间剪枝策略
- 虽然在这道题中暴力枚举完全可行,但在更复杂的问题中,我们可以通过一些条件提前剪枝,比如对于每个分母j,我们可以直接计算最小的满足i/j >= a/b的i值,然后只检查这个i和附近的几个值,而不是枚举所有i
- 例如:对于给定的j,最小的i是ceil((a*j)/b),这样可以将双重循环优化为接近O(L)的复杂度
-
解空间结构多样性
- 除了本题的二维网格解空间外,不同类型问题有不同的解空间结构:
- 排列问题:解空间是n!种可能
- 子集问题:解空间是2^n种可能
- 组合问题:解空间是C(n,k)种可能
- 树问题:解空间是树形结构
- 除了本题的二维网格解空间外,不同类型问题有不同的解空间结构:
二、关于精度问题的补充
-
浮点数比较的epsilon技巧
- 当需要比较两个浮点数是否相等时,更好的做法是检查它们的差的绝对值是否小于一个很小的数(epsilon)
- 例如:
if(fabs(a - b) < 1e-9)而不是if(a == b)
-
整数运算与浮点数运算的权衡
- 在本题中,使用交叉相乘进行比较(ib >= aj)是避免浮点数精度问题的最佳选择
- 当必须使用浮点数时,可以采用高精度运算库或自定义分数类来完全避免精度问题
三、关于gcd函数的补充
-
自定义gcd函数实现
- 提供一个可靠的自定义gcd函数实现,确保代码的可移植性
- 可以加入对负数的处理(取绝对值后再计算)
-
C++17标准gcd函数
- C++17中提供了标准的
std::gcd函数,位于<numeric>头文件 - 使用方式:
std::gcd(a, b),注意传入的参数应为非负数
- C++17中提供了标准的
四、关于题目解法的补充
-
更高效的枚举策略
- 对于每个分母j,我们可以直接计算最小的满足i/j >= a/b且gcd(i,j)=1的i值
- 这样可以减少内层循环的迭代次数,特别是当L较大时
-
数学方法:Farey序列
- Farey序列是0到1之间的最简分数序列,按照递增顺序排列
- 对于本题,可以考虑在Farey序列中寻找最接近a/b的分数
- 这种方法在L较大时更高效
五、代码优化技巧
-
减少重复计算
- 在循环中避免重复计算a*b等固定值
- 可以将计算结果缓存起来
-
优化常数
- 将内层循环的顺序调换(先j后i)可能会利用缓存的局部性原理
- 避免在循环内进行不必要的函数调用或对象创建
-
处理边界情况
- 添加对特殊输入的处理,如当a=0时
- 确保初始值的设置正确,尤其是当没有任何满足条件的解时
六、算法思维拓展
-
贪心与暴力的结合
- 在某些情况下,可以先用贪心策略缩小解空间,再用暴力枚举
- 例如:先找到粗略的最优解,再在其附近进行精细搜索
-
问题转化思维
- 思考如何将问题转化为更容易解决的形式
- 如本题可以转化为寻找最小的i/j - a/b,这等价于寻找最小的(ib - aj)/(j*b)
这些补充内容应该能让你的总结更加全面和深入,特别是在算法思维和代码优化方面。你的心得已经写得非常好,这些补充只是起到锦上添花的作用。
- 参加人数
- 0
- 创建人