P1763 埃及分数

1、读题:

将一个真分数表示为一堆分子为 \(1\) 的分式相加,其中我们可以简单概括为

\[\frac{a}{b} = \frac{1}{x_1} + \frac{1}{x_2} + \dots \frac{1}{x_k}
\]

对于题目要求是求解找到合适的最优解法之一。

满足的最优性质按照优先级排列如下:

  • \(x\) 数组的数量最少,也就是 \(k\) 最小。
  • 如果 \(k\) 相同的,分母最大值越小越好,为了方便比较,并且按照题目输出,默认 \(x_1 < x_2 \dots < x_k\) 。

2、思路:

2.1、最优性的分析

基础的思路实现是将整个答案拆解为一堆数字之和,也就是每次给定一个可选的数字,可以直接采用搜索的方式解决。

但是本题有一个优先级关系,输出的数组长度越小越好,即个数最少,

此时刚好满足迭代加深搜索的基本要求

  • 答案个数存在单调优先关系,越少越好
  • 找到合适的答案,就可以表示这个答案个数可以。

所以我们可以限定答案的个数(也就是搜索深度),采用迭代加深搜索,只要找到合适的答案,就可以停止继续加深搜索深度。

概括如下:

while (!dfs(2, a, b, 1))
{
max_depth++;
}

2.2、搜索的实现和细节处理

搜索的过程主要分为三步:

  • 截止条件,到达最大深度,停止搜索, \(u\) 表示上一个分母的选择, \(a, b\) 分别表示剩余的分子和分母, \(depth\) 表示当前的深度

    • bool dfs(ll u, ll a, ll b, int depth)
      if (depth == max_depth) // 此时说明找到答案了
  • 答案比较, 其中 \(res\) 数组存储真正的最优答案, \(tmp\) 表示的是当前这个搜索过程之中得到的一组解。

    • if (a == 1)  // 此时说明只剩下一个分数了,可以直接求解答案
      {
      // 如果分母大于了10^7,则与题目给出的数据范围不符,直接返回false
      if (b > 1e7) return false;
      tmp[depth - 1] = b;
      // 如果是第一次算出解,或者已经算出的解不够最优,则更新答案
      if (!res[0] || b < res[depth - 1])
      {
      for (int i = 0; i < max_depth; i++) res[i] = tmp[i];
      }
      return true;
      }
      return false;
    • 按照上述过程,需要关注的几个重点,只剩下一个分子为 \(1\) 的分数, 此时的分母不用枚举了,可以直接得到最终的分母。

    • 更新最优答案有两个选择,如果之前没有存储过答案,也就是 \(res[0] = 0\) ,直接将 \(tmp\) 先给到最优解存储,

      如果之前存储过,只需要比较最大的分母即可,因为此时长度是一样的。

  • 搜索下一个的选项范围

    • 附带性的需要重点关注,因为此时范围把控范围不对,第一容易出现负数情况,第二容易超时。

    • 此时需要给出一定的不等式关系,帮助理解,如下所示, 假设当前枚举的分母是 \(i\) , \(a', b'\) 是剩余的分子和分母, 需要满足:

      • 条件 \(1\) : \(\frac{a'}{b'} > \frac{1}{i}\) ,因为最后一项单独判断,所以每次枚举的都不能超过剩余的分数,推导不等式可以得到

        \(i > \frac{a'}{b'}\) ,此时的答案是范围的下界。

      • 条件 \(2\) :假设当前分子是 \(x_1 = i\), 后面的所有分母都是 \(i\), 我们可以直接知道

        \[\frac{1}{x_1}+\frac{1}{x_2}+\dots+\frac{1}{x_{maxdep-dep + 1}} < \frac{1}{i}+\frac{1}{i}+\dots+\frac{1}{i} = (maxdep-dep+1)\times \frac{1}{i}
        \]

        如果这个数字按照最好情况都比 \(\frac{a'}{b'}\) 还小,那么这个数值一定不可能。

        也就是

        \[\frac{a'}{b'} < (maxdep-dep+1)\times \frac{1}{i} \\

        i < (maxdep-dep+1)\times \frac{b'}{a'}
        \]

2.3、综合性代码的展示

搜索代码实现如下:

int max_depth = 1;// 迭代加深搜索
// u表示本层应该从哪里开始枚举分母
bool dfs(ll u, ll a, ll b, int depth)
{
if (depth == max_depth)
{
if (a == 1)
{
// 如果分母大于了10^7,则与题目给出的数据范围不符,直接返回false
if (b > 1e7) return false;
tmp[depth - 1] = b;
// 如果是第一次算出解,或者已经算出的解不够最优,则更新答案
if (!res[0] || b < res[depth - 1])
{
for (int i = 0; i < max_depth; i++) res[i] = tmp[i];
}
return true;
}
return false;
}
bool flag = false;
for (ll i = max(u, (b / a) + 1); i <= 1ll * b / a * (max_depth - depth + 1); ++i)
{
ll nx = a * i - b, ny = b * i;
ll g = gcd(nx, ny);
nx /= g, ny /= g;
tmp[depth - 1] = i;
if (dfs(i + 1, nx, ny, depth + 1))
// 另外还需要关注的是,当前深度还没有找到最优解之前,不能直接范围
{
flag = true;
}
}
return flag;
}

主函数实现如下:

int main()
{
ll a,b;
cin>>a>>b;
ll Gcd = gcd(a,b);
a /= Gcd;
b /= Gcd;
while (!dfs(2, a, b, 1))
{
max_depth++;
}
for (int i = 0; i < max_depth; i++)
{
cout<<res[i]<<' ';
}
return 0;
}

2.4、额外实现参考

之前求解的时候,尝试过bfs的写法过程,即使知道一定会超过内存限制,可以拿到虚假的 90pts

仅供参考

#include <bits/stdc++.h>
using namespace std;
#define ll long long
int max_depth = 6;// 最大深度
const int maxn = 1000;
int tar_dep;// 目标深度
ll a,b;
struct P{
int num;// 当前分母的个数
ll num_a;// 剩余的分子
ll num_b;// 剩余的分母
ll st;// 下一次可以选择的最小数字
vector<ll>res;// 答案数组
};
ll ans[maxn];
ll gcd(ll a,ll b)// 求两个数字的最大公约数
{
return (b==0)?a:gcd(b,a%b);
}
void bfs()
{
queue<P> Q;
Q.push({1,a,b,2});// 初始状态
while (!Q.empty())
{
P u = Q.front();
Q.pop();
if (tar_dep!=0 && u.num>tar_dep)// 说明在这一层已经出现答案了,不需要继续往后一层找
{
break;
}
if(u.num_a==1)// 说明这就是最后一个分母 ,也就是这一层就是正确答案
{
if (u.num_b <= u.res[u.res.size()-1]) continue;// 比最后一个枚举的还小
u.res.push_back(u.num_b);
if (tar_dep==0)// 说明第一次出现答案
{
tar_dep = u.num;// 更新目标深度
for (int i = 0; i < u.res.size(); i++)
{
ans[i] = u.res[i];
}
} // 说明有更优的答案
if (u.num_b < ans[tar_dep-1])
{
for (int i = 0; i < u.res.size(); i++)
{
ans[i] = u.res[i];
}
}
}
if (tar_dep!=0 && u.num==tar_dep)// 说明在这一层已经出现答案了,不需要继续更新
{
continue;
}
for (ll i = max(u.st, (u.num_b / u.num_a) + 1); i <= 1ll * u.num_b / u.num_a * (max_depth - u.num + 1); ++i) // 枚举优化
{
ll nx = u.num_a * i - u.num_b;
ll ny = u.num_b * i;
ll g = gcd(nx, ny);
nx /= g, ny /= g;
u.res.push_back(i);// 添加i作为答案
Q.push({u.num+1, nx, ny, i + 1,u.res});
u.res.pop_back();// 下一个枚举对象也要使用,记得清空数据
}
}
return ;
}
int main()
{ cin>>a>>b;
ll Gcd = gcd(a,b);
a /= Gcd;
b /= Gcd;
if (a==1)// 特殊判断
{
cout<<b<<endl;
return 0;
}
bfs();
for (int i=0;i<tar_dep;++i)
{
cout<<ans[i]<<' ';
}
return 0;
}

3、实现优化:

3.1、基础优化 \(1\) :直接将最后两个答案单独计算。

按照最新的数据来说,上述的时间复杂度过高,考虑优化。

上述实现过程之中,枚举的数值会越来越大,所以我们考虑最终求解的时候,

假设前面的答案搜索已经完成,

之前的截止条件是

\[\frac{a'}{b'} = \frac{1}{i}
\]

此时只需要判断, \(a'\) 是否等于 \(1\) ,

那么当然也可以直接考虑如果只剩下两个分数的时候,是否可以直接求解,这样就可以少一重最大的递归枚举了

假设最终剩下的数字满足这两个关系

\[\frac{a'}{b'} = \frac{1}{x} + \frac{1}{y}
\]

进行通分之后可以得到关系式

\[\frac{a'}{b'} = \frac{x + y}{xy}
\]

此时又因为得到的 \(a'\) 和 \(b'\) 都是最简的分数的形式,也就是呈互质关系,可以得到表示关系如下:

\[\left\{\begin{matrix}
a' \times p = x + y\\
b' \times p = x \times y
\end{matrix}\right.
\]

其中上述的 \(p\) 是公倍数的关系。

因为存在两个表达式,两个未知数,显然上式可以得到解,

将其中一个式子,代入到另外一个式子,可以得到

\[b' \times p = x \times (a' \times p - x)
\]

所以上述过程就是一个关于 \(x\) 的一元二次方程组,可以利用数学知识点解决

这里我们直接补充, 对于一元二次方程组是 \(ax^2 + bx + c = 0\) 的形式

得到的解一共有两个,分别是

\[\left\{\begin{matrix}
x_1 = \frac{-b - \sqrt{b^2 - 4ac}}{2a}\\
x_2 = \frac{-b + \sqrt{b^2 - 4ac}}{2a}
\end{matrix}\right.
\]

其中,我们通常将公共部分表示为一个符号,叫做 \(Delta\), 也就是 $\sqrt{b^2 - 4ac} = \Delta $

所以首先将我们得到的二次函数进行基本化

\[x^2 - a'\times p \times x + b' \times p = 0
\]

此时按照上述的公式,直接求解就可以得到两个解,分别是

\[\left\{\begin{matrix}
x_1 = \frac{-a'\times p - \Delta}{2}\\
x_2 = \frac{-a'\times p + \Delta}{2}
\end{matrix}\right.
\]

此时的 \(x_1 = x, x_2 = y\) 。

其中首先需要判断 \(\Delta\) 是否是整数答案, \(\Delta = \sqrt{(a'\times p)^2 - 4 \times b' \times p}\)

另外还需要判断 \(x_1,x_2\) 是否也是整数答案

分步骤表示为, 下方的 \(i\) 代表的就是此时的 \(p\) 。

ll delta = a * a * i * i - ((b * i) << 2);
ll Sqrt = sqrt(delta);
if (Sqrt * Sqrt != delta || ((a * i - Sqrt) & 1))
{
continue;
}
tmp[depth - 1] = (a * i - Sqrt) >> 1; // x_1
tmp[depth] = (a * i + Sqrt) >> 1; // x_2
if (!res[0] || tmp[depth] < res[depth])
{
for (int i = 0; i < max_depth; i++)
{
res[i] = tmp[i];
}
return true;
}

3.2、基础优化 \(2\) : 上下界优化

关键的一点是整个过程之中,还没有处理 \(p\) 的枚举范围,

此时我们可以考虑到整个答案过程要求的分母不能超过 \(10 ^ 7\) ,我们直接设置为

\(INF = 10 ^ 7\) ,

此时的话利用好之前的一些式子,可以得到最终的上下界边界

边界 \(1\) :

\(\Delta = \sqrt{(a'\times p)^2 - 4 \times b' \times p} > 0\)

推导后可以得到

\(p > \frac{4\times b'}{{a'}^2}\) 。

边界 \(2\) :

\[\left\{\begin{matrix}
a' \times p = x + y = x_1 + x_2\\
b' \times p = x \times y = x_1 \times x_2
\end{matrix}\right.
\]

我们可以知道

\[x_1 + x_2 < 2 \times INF \\
x_1 \times x_2 < INF \times INF
\]

所以可以直接知道

\(p < min(\frac{2 \times INF}{a'}, \frac{INF \times INF}{b'})\) 。

最终的枚举范围可以确定,但是此时还没有剪枝通过。

可以考虑最终剩余三个数字的情况,但是此时的关系式只有两个,优化起来过于麻烦。

关键代码实现:

#define ll long long
const int maxn = 1e7;
ll INF = 1e7;
ll res[maxn], tmp[maxn];
ll gcd(ll a, ll b)
{
return (b == 0) ? a : gcd(b, a % b);
}
int max_depth = 1; // 迭代加深搜索
// u表示本层应该从哪里开始枚举分母
bool dfs(ll u, ll a, ll b, int depth)
// 当前选择 分子 分母 深度
{
if (depth == max_depth)
{
if (a == 1)
{
// 如果分母大于了10^7,则与题目给出的数据范围不符,直接返回false
if (b > 1e7)
{
return false;
}
tmp[depth - 1] = b;
// 如果是第一次算出解,或者已经算出的解不够最优,则更新答案
if (!res[0] || b < res[depth - 1])
{
for (int i = 0; i < max_depth; i++)
{
res[i] = tmp[i];
}
}
return true;
}
return false;
}
if (depth == max_depth - 1) // 说明到达倒数第二层
{
ll l = ((b << 2ll) / a / a) + 1;
ll r = min(((INF<< 1ll)) / a, (INF - 1)* (INF / b));
for (ll i = l; i <= r; i++)
{
ll delta = a * a * i * i - ((b * i) << 2);
ll Sqrt = sqrt(delta);
/*
delta不为完全平方数或者为0的答案需要去除
*/
if (Sqrt * Sqrt != delta || ((a * i - Sqrt) & 1))
{
continue;
}
tmp[depth - 1] = (a * i - Sqrt) >> 1;
tmp[depth] = (a * i + Sqrt) >> 1;
if (!res[0] || tmp[depth] < res[depth])
{
for (int i = 0; i < max_depth; i++)
{
res[i] = tmp[i];
}
return true;
}
}
return false;
}
bool flag = false;
// 1/x <= b/a x>= (b/a)+1 maxdep-dep+1 * (1/x) >= a/b
// 枚举当前层的分数的分母
for (ll i = max(u, (b / a) + 1); i <= 1ll * b / a * (max_depth - depth + 1); ++i)
{
ll nx = a * i - b, ny = b * i;
ll g = gcd(nx, ny);
nx /= g, ny /= g;
tmp[depth - 1] = i;
if (dfs(i + 1, nx, ny, depth + 1))
{
flag = true;
}
}
return flag;
}

3.3、附加优化:

思考一个实现过程,如果修改 \(INF\) 的数值,可以在一个比较小的时候找到答案,一定比在更大的时候优秀。

所以提前将初始的 \(INF\) 设置为 \(1e6\),如果可以找到答案,就直接退出,否则修改为 \(1e7\) 。

4、小结:

  • 利用迭代加深进行搜索的方式是必学项,广搜虽然可以较快的处理,但是实际的空间浪费的较多,另外广搜的实现中,必须找到当前这一层的最优解才能退出,这对于初学者显然不是非常友好。
  • 基本的搜索剪枝也是必学项,最优性的关系可以处理上界(范围的最大值),比上一个答案大可以处理下界(范围的最小值)。
  • 至于最后单独处理两个答案,本质上是数学的方法,当然剩余三个数,四个数,可以采用求解线性方程组的秩提前处理,但是作为基本选项,难度考察上暂时没有到达这个要求,学会理解二次函数即可,如果还没有学过解二次函数,建议先理解透彻前面的剪枝优化,数学学到这个时候,理解这个会更加透彻,不然欲速则不达。

P1763 埃及分数(小理解,后续补充线性方程优化)的更多相关文章

  1. 华为OJ平台——将真分数分解为埃及分数

    题目描述: 分子为1的分数称为埃及分数.现输入一个真分数(分子比分母小的分数,叫做真分数),请将该分数分解为埃及分数.如:8/11 = 1/2+1/5+1/55+1/110. 输入: 输入一个真分数, ...

  2. UVA12558 Egyptian Fractions (HARD version)(埃及分数)

    传送门 题目大意 给出一个真分数 a/b,要求出几个互不相同的埃及分数(从大到小),使得它们之和为 a/b (埃及分数意思是分子为1的分数,详见百度百科) 如果有多组解,则分数数量少的优先 如果分数数 ...

  3. 埃及分数问题_迭代加深搜索_C++

    一.题目背景 http://codevs.cn/problem/1288/ 给出一个真分数,求用最少的1/a形式的分数表示出这个真分数,在数量相同的情况下保证最小的分数最大,且每个分数不同. 如 19 ...

  4. codevs1288 埃及分数(IDA*)

    1288 埃及分数  时间限制: 1 s  空间限制: 128000 KB  题目等级 : 钻石 Diamond     题目描述 Description 在古埃及,人们使用单位分数的和(形如1/a的 ...

  5. 一本通例题埃及分数—题解&&深搜的剪枝技巧总结

    一.简述: 众所周知,深搜(深度优先搜索)的时间复杂度在不加任何优化的情况下是非常慢的,一般都是指数级别的时间复杂度,在题目严格的时间限制下难以通过.所以大多数搜索算法都需要优化.形象地看,搜索的优化 ...

  6. mysql_查的小理解

    show create table employee; 对这个语句的小理解: 顿悟呀,之前一直不太理解这条语句,现在忽然觉得明朗起来.他就是展示创建这个表格时的SQL语句.执行上述代码之后结果如下: ...

  7. net core体系-web应用程序-4net core2.0大白话带你入门-8asp.net core 内置DI容器(DependencyInjection,控制翻转)的一点小理解

    asp.net core 内置DI容器的一点小理解   DI容器本质上是一个工厂,负责提供向它请求的类型的实例. .net core内置了一个轻量级的DI容器,方便开发人员面向接口编程和依赖倒置(IO ...

  8. 埃及分数&&The Rotation Game&&骑士精神——IDA*

    IDA*:非常好用的搜索,可以解决很多深度浅,但是规模大的搜索问题. 估价函数设计思路:观察一步最多能向答案靠近多少. 埃及分数 题目大意: 给出一个分数,由分子a 和分母b 构成,现在要你分解成一系 ...

  9. Vijos 1308 埃及分数(迭代加深搜索)

    题意: 输入a.b, 求a/b 可以由多少个埃及分数组成. 埃及分数是形如1/a , a是自然数的分数. 如2/3 = 1/2 + 1/6, 但埃及分数中不允许有相同的 ,如不可以2/3 = 1/3 ...

  10. JDOJ 1770 埃及分数

    JDOJ 1770: 埃及分数 https://neooj.com/oldoj/problem.php?id=1770 Description 分子均为1的分数叫做埃及分数,因为古代埃及人在进行分数运 ...

随机推荐

  1. 奇技淫巧之如何在JavaScript中使用Python代码

    @charset "UTF-8"; .markdown-body { line-height: 1.75; font-weight: 400; font-size: 15px; o ...

  2. Summary of Indexing operation in DataFrame of Pandas

    Summary of Indexing operation in DataFrame of Pandas For new users of pandas, the index of DataFrame ...

  3. 一个大对象引起的血案,GC的踩坑实录

    背景:   问题: 有个渠道支付服务,负责与所有支付相关服务进行交互,包括 渠道下单支付,渠道成功通知,渠道的对账等 服务4台机,平时跑的都很稳定,通过thrift进行对外提供服务,且平时并未发现访问 ...

  4. Spring Boot中使用注解实现简单工厂模式

    前言 从设计模式的类型上来说,简单工厂模式是属于创建型模式,又叫静态工厂模式(Simple Factory Pattern),但不属于23种GOF设计模式之一.简单工厂模式是由一个工厂对象决定创建出接 ...

  5. Hive对JSON格式的支持研究

    一.背景 JSON是一种通用的存储格式,在半结构化存储中十分常见,部分场景已经开始存在以JSON格式贴源存储的数据,作为下游数据使用方,我们亟需对JSON格式的数据进行加工和处理,以提取出我们需要的数 ...

  6. C语言基础算法

    C语言基础算法 目录 C语言基础算法 1.阶乘 递归实现 循环实现 2.排序 冒泡排序 选择排序 3.斐波那契数列 4.ASCII码的使用 1.阶乘 递归实现 #include <stdio.h ...

  7. 题解:P1763 埃及分数

    题目链接:link. 先放上代码,然后再讲解: #include<bits/stdc++.h> using namespace std; typedef long long ll; ll ...

  8. ChatMoney,你的就业指导明灯

    本文由 ChatMoney团队出品 介绍说明 Hey!亲爱的小伙伴们,今天我要给大家带来一个职场利器--AI就业指导机器人! 在这个充满变数的职场江湖,找到一份既能养家糊口又能实现自我价值的工作是多么 ...

  9. 揭秘 ChunJun:如何实现 e2e&session 日志隔离

    本文将从 e2e 的基本介绍,e2e 的使用与扩展,session 日志隔离三个维度为大家带来 ChunJun e2e & session 日志隔离的分享. 大量具体代码和演示请看视频教程️ ...

  10. 致谢每一位ChunJun Contributor!这里有一份礼物等你领取!

    作为一个批流统一的数据集成框架,秉承着易用.稳定.高效的目标,ChunJun于2018年4月29日在Github上将内核源码正式开放. 从还被叫作FlinkX,写下第一行代码开始,ChunJun已经走 ...