数位统计问题
请统计某个给定范围[L, R]的所有整数中,数字2出现的次数。
比如给定范围[2, 22],数字2在数2中出现了1次,在数12中出现1次,在数20中出现1次,在数21中出现1次,在数22中出现2次,所以数字2在该范围内一共出现了6次。
枚举法:直接从L~R枚举,对每一个x进行2出现次数的统计。
数学计数法:以每一个数位为统计单元进行计数,利用前缀和的思想直接处理出1~n中满足性质的数的个数f(n),则答案即为f(r)-f(l-1)。下图以第三高位作为统计单元为例:
代码:
#include<iostream>
#include<vector>
using namespace std;
int f(int n){//1~n中2出现的次数
vector<int>num;
while(n){
num.push_back(n%10);
n/=10;
}
n=num.size();
int i,j,left,right,power,ans=0;
for(i=n-1;i>=0;--i){
left=right=0;
power=1;
for(j=n-1;j>i;--j){
left=left*10+num[j];
}
for(j=i-1;j>=0;--j){
right=right*10+num[j];
power*=10;
}
ans+=left*power;
if(num[i]==2)ans+=right+1;
else if(num[i]>2)ans+=power;
}
return ans;
}
int main(){
int l,r;
cin>>l>>r;
cout<<f(r)-f(l-1);
return 0;
}
加强版问题:
试计算在区间1 到n 的所有整数中,数字x(0 ≤ x ≤ 9)共出现了多少次?
例如,在1到11 中,即在1、2、3、4、5、6、7、8、9、10、11 中,数字1 出现了4 次。
显而易见的是,如果x!=0,则直接为上述考虑方案,唯一需要独立考虑的为x=0即前导0的情况。
需要注意的是,本身abcdef这个数字的表示即说明了a位置上不可能取0,所以在讨论0出现的次数时,对于最高位,不应该进行讨论,因为如果最高位取0,会产生前导0,不应统计在内;如果不取0,也不可能进行第二种情况的讨论,因为若a=0,后面可取00000~99999的情况还是有前导0,所以对于x=0的情况,不需要讨论最高位。
注:尽管可以将两个特判结合,但为了简化思路,直接分开x是否为0的两种情况计算。
#include<iostream>
#include<vector>
using namespace std;
int f(int n,int x){//1~n中2出现的次数
vector<int>num;
while(n){
num.push_back(n%10);
n/=10;
}
n=num.size();
int i,j,left,right,power,ans=0;
if(!x){
for(i=n-1;i>=0;--i){
left=right=0;
power=1;
for(j=n-1;j>i;--j){
left=left*10+num[j];
}
--left;
for(j=i-1;j>=0;--j){
right=right*10+num[j];
power*=10;
}
ans+=left*power;
if(num[i]==0)ans+=right+1;
else if(num[i]>0)ans+=power;
}
}
else{
for(i=n-1;i>=0;--i){
left=right=0;
power=1;
for(j=n-1;j>i;--j){
left=left*10+num[j];
}
for(j=i-1;j>=0;--j){
right=right*10+num[j];
power*=10;
}
ans+=left*power;
if(num[i]==x)ans+=right+1;
else if(num[i]>x)ans+=power;
}
}
return ans;
}
int main(){
int n,x;
cin>>n>>x;
cout<<f(n,x);
return 0;
}
双指针优化循环
在社交媒体上,经常会看到针对某一个观点同意与否的民意调查以及结果。例如,对某一观点表示支持的有 1498 人,反对的有 902 人,那么赞同与反对的比例可以简单的记为 1498:902 。
不过,如果把调查结果就以这种方式呈现出来,大多数人肯定不会满意。因为这个比例的数值太大,难以一眼看出它们的关系。对于上面这个例子,如果把比例记为 5:3 ,虽然与真实结果有一定的误差,但依然能够较为准确地反映调查结果,同时也显得比较直观。
现给出支持人数A,反对人数 B ,以及一个上限 L ,请你将 A 比 B 化简为 A ’比 B ’,要求在 A ’和 B ’均不大于 L 且 A ’和 B ’互质(两个整数的最大公约数是 1 )的前提下, A ’ /B ’ ≥ A/B 且 A ’ /B ’ - A/B 的值尽可能小。
输入描述:
输入共一行,包含三个整数 A,B,L ,每两个整数之间用一个空格隔开,分别表示支持人数、反对人数以及上限。
输出描述:
输出共一行,包含两个整数 A ’, B ’,中间用一个空格隔开,表示化简后的比例。
备注:
对于 100% 的数据, 1 ≤ A ≤ 1,000,000,1 ≤ B ≤ 1,000,000,1 ≤ L ≤ 100,A/B ≤ L
枚举法:由于L的数据范围很小,可以直接枚举A,B,选择符合要求的答案即可。
关于互质要求的判断:此题没有必要判断互质,因为若x和y互质,最大公因子为d,x和y均<L,那么上下同时除去d的结果为x'=x/d,y'=y/d,其中x'与y'也是满足<L的条件,因此x'/y'会在x/y之前计算,也就是互质的会比不互质的先算,并且不互质的不会导致答案改变即本次计算并不会导致差值变小,只要我们在设计算法的时候要求差值严格小于之前的结果时更新结果即可。
关于差值的增减性判断:
1.如果为了避免浮点计算的误差,将式子进行交叉相乘得,A'B-B'A>0,即当A'B-B'A的结果大于0且小于上次的结果时再更新。
#include<iostream>
using namespace std;
int main(){
int A,B,L,i,j,a,b,delta=1e9;
cin>>A>>B>>L;
for(i=1;i<=L;++i){
for(j=1;j<=L;++j){
if(i*B-A*j>=0&&i*B-A*j<delta){
delta=i*B-A*j;
a=i,b=j;
}
}
}
cout<<a<<" "<<b;
return 0;
}
2.由于本题的数据范围小,对于double来说精度误差可以有效控制,因此可以直接用double来比较增量。
#include<iostream>
using namespace std;
int main(){
int A,B,L,i,j,a,b;
cin>>A>>B>>L;
double delta=1e9,X=(double)A/B,x;
for(i=1;i<=L;++i){
for(j=1;j<=L;++j){
x=(double)i/j;
if(x>=X&&x-X<delta){
delta=x-X;
a=i,b=j;
}
}
}
cout<<a<<" "<<b;
return 0;
}
双指针优化:假设我们现在有一个初始的A’/B‘,现在要求能够使得A'/B'-A/B取得最小的A'与B',为了保证A'/B'>=A/B,因此我们势必要增大分子,而为了是两者距离更近,同时我们也需要增大分母,分母的增加必须保证A'/B'>=A/B,因此就A'和B'各自的变化趋势来看都是单调不减的,因而可以采用双指针算法将O(n^2)做法优化至O(n)。
可能的疑惑点:让分子变小一点分母增大一点并且保持结果>=A/B的情况是否可能发生?
疑惑解答:首先我们规定对每一个分母j,求一个分子i是使得i/j刚好满足i/j>=A/B的第一个i,那么上述疑问可以转换成,存在j'>j,i'<i,且i'/j'>=A/B。其实这种情况和我们的规定是相违背的。
我们已知,i/j>=A/B,i'/j'>=A/B,j'>j,i'<i那么i'/j>i'/j'>=A/B,那么当j作为分子的时候,求取的第一个满足条件的分子应该是i'而不是i,与规定矛盾。
代码中为了实现方便,尤其是避免分母为0的情况,因此优先对分母进行枚举。
#include<iostream>
using namespace std;
int main(){
int A,B,L,i,j,a,b,delta=1e9;
cin>>A>>B>>L;
for(i=0,j=1;j<=L;++j){
while(i<=L&&i*B-A*j<0)++i;
if(i<=L&&i*B-A*j<delta){//如果i处于满足<=L的范围并且可以进行更新
a=i,b=j;
delta=i*B-A*j;
}
}
cout<<a<<" "<<b;
return 0;
}