最终代码在末尾
(不排除这不是最优解的可能)
这是我写的第一篇题解,可能写的不好。
题意
输出有多少对数满足两数相乘之后不存在小数(小数点后都是0)
需要亿点一点数学知识和前缀和知识
注意浮点数的精度问题?为什么会有精度问题?
$ 0<a_i<10000 $,加上小数点后的9位数,我们用double类型来存小数时double可以精确这14位数。
但是如果我们将两个数字相乘会发生什么,我们就需要28位数的精度( 0.001953125 × 512.000000001 0.001953125 \times 512.000000001 0.001953125×512.000000001),显然double甚至long double对此也无能为力。 要想避免精度问题,可以将这些数字乘上 1 0 9 10^9 109然后再存进一个long long的数组中。
另外,为了避免浮点数读入带来的精度误差,下面代码采用字符串读入。
long long to(string& s) //传引用避免复制
{
long long res = 0; //最终返回的值
int i = 0;
while(i<s.size() && s[i]!='.'){ //小数点前的数字加到res中
res*=10;
res+=s[i]-'0';
i++;
}
if(i==s.size()){ //如果输入的数字没有小数点 乘1e9再返回(因为我们需要的是转化为整数的数字)
res = res*mod9;
return res;
}
i++; //跳过小数点
int cnt=0; //计算有多少位小数
while(i<s.size()){ //小数点后的数字也加到res中
res*=10;
res+=s[i]-'0';
i++;cnt++;
}
while(cnt<9){ //res后面补0 使得res和原来刚好扩大1e9
res*=10;
cnt++;
}
return res;
}
现在回到题目,将小数转换为long long之后怎么判断这两个数相乘没有小数?根据乘法
2.500000000
×
4.000000000
=
1.00000000000000000
2.500000000 \times 4.000000000=1.00000000000000000
2.500000000×4.000000000=1.00000000000000000 小数点后18个0,17个为原有的,还有一个是
2.5
×
4
=
10
2.5 \times 4=10
2.5×4=10 产生的。将这个应用到整数中,可以得出
(
a
i
×
a
j
)
%
1
0
18
=
=
0
(a_i \times a_j)\%10^{18}==0
(ai×aj)%1018==0即为满足题意的一对数。
那么,问题又来了,转化为long long 之后 0 < a i < 1 0 13 0<a_i<10^{13} 0<ai<1013 两个数相乘肯定会爆long long。这时就要使用数学知识了!
先思考什么样的 x x x才能满足 x % 1 0 18 = = 0 x\%10^{18}==0 x%1018==0 那 x x x肯定要是 1 0 18 10^{18} 1018的倍数 x = k × 1 0 18 x=k \times 10^{18} x=k×1018,对x进行分解得到 x = k × 2 18 × 5 18 x=k \times 2^{18} \times 5^{18} x=k×218×518,那怎样的两个数相乘才能有这样的结果?
设有两个数 a , b a,b a,b, a = 2 p 1 × 5 p 2 × k 1 , b = 2 q 1 × 5 q 2 × k 1 a = 2^{p_1} \times 5^{p_2} \times k_1,b = 2^{q_1} \times 5^{q_2} \times k_1 a=2p1×5p2×k1,b=2q1×5q2×k1
( a × b ) % 1 0 18 = 0 (a \times b)\% 10^{18} =0 (a×b)%1018=0,即 p 1 + q 1 ≥ 18 , p 2 + q 2 ≥ 18 p_1+q_1 \geq 18,p_2+q_2 \geq 18 p1+q1≥18,p2+q2≥18
通过上面的数学推导可以发现,我们只关心一个数字2和5的因子个数,所以可以用pair<int,int> 分别存2和5的因子数量
这样就可以写出下面 O ( n 2 ) O(n^2) O(n2)的程序
#include<iostream>
#include<string>
#include<cstring>
#define ll long long
#define fr first //宏定义简化代码
#define se second
using namespace std;
typedef pair<int,int> pii; //pii在这
const ll mod9 = 1e9;
pii a[500005];
int n;
string s;
long long to(string& s) //传引用避免复制
{
long long res = 0; //最终返回的值
int i = 0;
while(i<s.size() && s[i]!='.'){ //小数点前的数字加到res中
res*=10;
res+=s[i]-'0';
i++;
}
if(i==s.size()){ //如果输入的数字没有小数点 乘1e9再返回(因为我们需要的是转化为整数的数字)
res = res*mod9;
return res;
}
i++; //跳过小数点
int cnt=0; //计算有多少位小数
while(i<s.size()){ //小数点后的数字也加到res中
res*=10;
res+=s[i]-'0';
i++;cnt++;
}
while(cnt<9){ //res后面补0 使得res和原来刚好扩大1e9
res*=10;
cnt++;
}
return res;
}
int main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
long long t2;
cin >> n;
for(int i = 1; i <= n; i++){
cin >> s; //double精度不够
t2 = to(s); //将字符串转化为乘10^9后的数字
a[i] = make_pair(0,0); //初始化
while(t2>0&&t2%2==0){ //统计t2中2的个数
a[i].fr++;
t2/=2;
}
while(t2>0&&t2%5==0){ //统计t2中5的个数
a[i].se++;
t2/=5;
}
}
//枚举每一对数
long long ans = 0; //存答案
for(int i = 1; i <= n; i++){
for(int j = i+1; j <= n; j++){ //j从i+1开始枚举,避免重复枚举
if(a[i].fr+a[j].fr>=18 && a[i].se+a[j].se>=18) //原来两个整数数相乘后2和5的因子个数
ans++;
}
}
cout << ans;
return 0;
}
兴奋的提交一波,超时!
再看一眼n的范围200000!!!
看来需要想办法优化一下最后统计答案的二重循环
将
a
[
i
]
.
f
r
+
a
[
j
]
.
f
r
≥
18
a[i].fr+a[j].fr \geq 18
a[i].fr+a[j].fr≥18移一下项得到
a
[
i
]
.
f
r
≥
18
−
a
[
j
]
.
f
r
a[i].fr \geq 18-a[j].fr
a[i].fr≥18−a[j].fr,也就是说对于一个
a
[
j
]
a[j]
a[j]我们所有满足上面不等式的
i
i
i都对答案有贡献,这就自然而然的想到前缀和,很可惜,这里需要用二维前缀和。
将 a [ i ] a[i] a[i]抽象成二维平面的点,因为需要用前缀和,而 a [ i ] a[i] a[i]的x,y最小值可以是0,所以这里将 a [ i ] . f r a[i].fr a[i].fr和 a [ i ] . s e a[i].se a[i].se都先加一再进行描点,用数组 q [ x ] [ y ] q[x][y] q[x][y]来表示 ( 0 , 0 ) − ( x , y ) (0,0)-(x,y) (0,0)−(x,y)(左下角和右上角)的点的数量,再由前缀和公式可以得到矩形区域 ( a , b ) − ( c , d ) (a,b)-(c,d) (a,b)−(c,d)点的数量为 q [ c ] [ d ] − q [ a − 1 ] [ d ] − q [ c ] [ b − 1 ] + q [ a − 1 ] [ b − 1 ] q[c][d]-q[a-1][d]-q[c][b-1]+q[a-1][b-1] q[c][d]−q[a−1][d]−q[c][b−1]+q[a−1][b−1]
因为转化为long long的数字最大为 1 0 13 10^{13} 1013
log 2 1 0 13 ≤ 44 \log_210^{13} \leq 44 log21013≤44,即前缀和数组开 45 × 45 45\times45 45×45就行了,下面代码开了 55 × 55 55\times55 55×55
如此一来,我们就可以将时间复杂度优化为 O ( n + 46 ∗ 46 ) O(n+46*46) O(n+46∗46)了!
这题的前缀和还有一些实现的细节在代码中展示
#include<iostream>
#include<string>
#include<cstring>
#define ac return 0;
#define ll long long
#define fr first //宏定义简化代码
#define se second
using namespace std;
typedef pair<int,int> pii; //pii在这
const ll mod9 = 1e9;
pii a[200005];
int n;
int mp[47][47]; //描点 用于计算前缀和
int q[47][47]; //前缀和数组
string s;
ll to(string& s)
{
ll res = 0; //最终返回的值
int i = 0;
while(i<s.size() && s[i]!='.'){ //小数点前的数字加到res中
res*=10;
res+=s[i]-'0';
i++;
}
if(i==s.size()){ //如果输入的数字没有小数点 乘1e9再返回(因为我们需要的是转化为整数的数字)
res = res*mod9;
return res;
}
i++; //跳过小数点
int cnt=0; //计算有多少位小数
while(i<s.size()){ //小数点后的数字也加到res中
res*=10;
res+=s[i]-'0';
i++;cnt++;
}
while(cnt<9){ //res后面补0 使得res和原来刚好扩大1e9
res*=10;
cnt++;
}
return res;
}
int main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
ll t2;
cin >> n;
for(int i = 1; i <= n; i++){
cin >> s; //double精度不够
t2 = to(s); //将字符串转化为乘10^9后的数字
a[i] = make_pair(0,0); //初始化
while(t2>0&&t2%2==0){ //统计t2中2的个数
a[i].fr++;
t2/=2;
}
while(t2>0&&t2%5==0){ //统计t2中5的个数
a[i].se++;
t2/=5;
}
}
memset(mp,0,sizeof(mp)); //初始化
memset(q,0,sizeof(q));
for(int i = 1; i <= n; i++){ //描点
mp[a[i].fr+1][a[i].se+1]++; //先将点加一
}
for(int i = 1; i <= 46; i++)
for(int j = 1; j <= 46; j++)
{
q[i][j] = q[i-1][j] + q[i][j-1] - q[i-1][j-1] + mp[i][j]; //前缀和公式
}
ll ans=0;
for(int i = 1; i <= n; i++){
int x = 18-a[i].fr, y = 18-a[i].se; //算出矩形的坐下角坐标 右上角坐标为最大值46
//不要忘记a[i]是加了1的所有查询区间的时候是查询[x+1,46][y+1,46]
ll t = q[46][46] - q[46][y] - q[x][46] + q[x][y];
if(a[i].fr>=x&&a[i].se>=y)t--; //如果这个if满足,意味着t个点中包含了自身,需要t--
ans += t;
}
//同一对数会数两遍,输出时要/2
cout << ans/2;
ac
}