P2602 [ZJOI2010]数字计数
Posted lzy-blog
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了P2602 [ZJOI2010]数字计数相关的知识,希望对你有一定的参考价值。
------------恢复内容开始------------
蒟蒻不会数位dp也不会dfs怎么办呢?
利用类似于倍增的思想……
输入a b两数,我们把([a,b])划分为三段分别计算
以下用 (lena,lenb) 代指 (a,b) 的位数
首先,我们要统计 (a) 到 最大的 (lena) 位数的答案
(通俗:a 到 lena个9的数)
第二步 统计从最小的 (lena+1) 位数到最大的 (lenb-1) 位数的答案
(通俗:1后面lena个0的数 到 lenb-1个9的数)
最后,统计最小的 (lenb) 位数到 (b) 的答案
(通俗:1后面lenb-1个0的数 到 b)
举个栗子 如果 (a=1,b=101) 有以下三步:
1.计算 (1-9) 的答案
2.计算 (10-99) 的答案
3.计算 (100-101) 的答案
第二步可以通过找规律解决
瞪眼法观察以下数据:
9 19 19 19 19 19 19 19 19 19
180 280 280 280 280 280 280 280 280 280
2700 3700 3700 3700 3700 3700 3700 3700 3700 3700
36000 46000 46000 46000 46000 46000 46000 46000 46000 46000
第x行代表0-9在所有x位数中出现了多少次
你是否看出了第一行(0的出现次数)之间的相互关系
还有0与同行剩下数字的关系??
然后我们讨论一下第一步和第三步怎么搞,先说第一步
我们可以利用上面的数据做一个前缀和,可以获得0-最大的x位数中每个数字出现的次数
比如 我们要从(5666)算到(9999)
首先每次加(1) 暴力计算4次 现在下一个应该计算(5670)
接着,一次加(10)
(5670-5679)的答案可以拆开计算,先计算后面1位数的变化,用前缀和数组可以获得0-最大的x位数 即 x个9这么个数 中所有的统计答案
然后算上前导0的影响(打表)
接着,发现后面3位没有变过,直接在答案中把后边每个数数累加 (10^1) 次就可以
每次加上 (10) 直到后两位都变成0 -> 获得 (5700)
(注意 这里表示5700之前的数字都统计过 也就是说我们的目标是计算到10000,现在5699是被统计过的)
每次加100,统计答案方式如上 获得 (6000)
每次加1000,统计答案方式如上 获得 (10000)
完成~
其实第三步就是把第一步反过来,先加上大的,不能再加之后就加小的
例如:10000 -> 13000 -> 13900 -> 13950 -> 13956
留给读者自行思考~
其实就是把一个混杂的问题分成一个个整体一次算好多
实在没看懂可以看code:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define inf 0x7fffffff
ll a,b;
ll f[10][19];
ll sum[19];
ll ans[10];//答案数组
ll zeros[19]={0,0,1+9,2+ 108,3+ 1107,4+ 11106,5+ 111105,6+ 1111104, 7+11111103, 8+111111102, 9+1111111101, 10+11111111100,11+ 111111111099,12+ 1111111111098, 11111111111097, 111111111111096, 1111111111111095, 11111111111111094, 111111111111111093};
//打表 zeros[i]表示所有i位数包括0中有多少个前导0 当然0的前导零个数是i-1在前面额外加上
ll Getlen(ll x)//简单的获取一个数有多少位
{
if(x==-1)return 0;
if(x==0)return 1;//显然0有一位但下面算不出来
ll len=0;
while(x)
{
len++;
x/=10;
}
return len;
}
void count(ll x)//简单的把一个数字拆开统计答案
{
while(x)
{
ans[x%10]++;
x/=10;
}
}
//要计算的数字 计算位数 p=10^计算位数
//x的后w位全部为0,计算x~x+w-1 的所有答案
void Run(ll x,ll w,ll p)
{
for(int i=0;i<=9;i++)ans[i]+=f[i][w];//后w位就是000-999的排列
ans[0]+=zeros[w];//这个时候要带上前导零
x/=(p);
while(x)//后面w个数可以用排列计算,剩下的直接暴力算就行 每个数都出现了 10^w 次
{
ans[x%10]+=p;
x/=10;
}
}
signed main()
{
scanf("%lld%lld",&a,&b);
//找规律
for(int i=0;i<=9;i++)
{
f[i][1]=1;
}
f[0][2]=9;
for(int i=1;i<=9;i++)f[i][2]=19;
ll k=1;
for(int i=3;i<=13;i++)
{
ll p=f[0][i-1];
p+=9*k;
p*=10;
k*=10;
f[0][i]=p;
for(int j=1;j<=9;j++)f[j][i]=p+k*10;
}
//此时f[i][j]表示所有j位数中i的出现次数 不包含前导零
//统计跨位数的答案
ll lena=Getlen(a-1),lenb=Getlen(b+1);
for(int i=lena+1;i<=lenb-1;i++)
{
for(int j=0;j<=9;j++)ans[j]+=f[j][i];
}
for(int i=2;i<=13;i++)//前缀和
{
for(int j=0;j<=9;j++)f[j][i]+=f[j][i-1];
}
//现在f[i][j]表示从0到最大的j位数(j个9)中i的出现次数 不包含前导零
if(Getlen(a)==Getlen(a-1)&&lenb-lena>1)//确保a不是最小的Getlen(a)位数 统计从a到 最小的Getlen(a)+1位数 中间所有的数
{
ll nw=1,p=10;
while(a%p!=0)//先1个1个加
{
count(a);
a++;
}
//nw表示a的后nw位现在都是0
while(nw!=lena)//规定小于a的数字都被统计过 a是没被统计的 所以退出时a=最小的Getlen(a)+1位数+1
{
while(a%(p*10)!=0)//一次加p=10^nw个 直到nw+1位也变成0
{
Run(a,nw,p);
a+=p;
}
nw++,p*=10;
}
}
//确保b不是最大的Getlen(b)位数 统计从最小的Getlen(b)位数到b中的所有数字
if(Getlen(b)==Getlen(b+1)&&lenb-lena>1)
{
ll x=1;
for(ll i=1;i<=lenb-1;i++)x*=10;
ll nw=lenb-1;
ll p=1;
for(ll i=1;i<=nw;i++)p*=10;
//nw与p的定义同上
while(nw!=0&&x<b)//因为最后盲加了一个所以这边都用小于号
{
while(x/p!=b/p)//x与b除了后nw位不相同之外其他都相同
{
Run(x,nw,p);
x+=p;
}
nw--,p/=10;
}//因为p不能=0,所以个位就单独暴力
while(x<b)
{
count(x);
x++;
}
count(x);
}
//如果两个数的位数一样或者只差1位需要单独计算
//方法和上面都差不多 就是加上的数越来越大 发现再加就过b之后就越来越小
if(lenb-lena<=1)
{
lena=Getlen(a),lenb=Getlen(b);
ll nw=1,p=10;
while(a%p!=0&&a<b)
{
count(a);
a++;
}
p=10;
while(nw!=lenb-1&&a<b)
{
while(a%(p*10)!=0)
{
if(a+p>b)break;
Run(a,nw,p);
a+=p;
}
nw++,p*=10;
}
//其实就是把上面复制了过来QwQ
ll x=a;//连变量名都没改的
while(nw!=0&&x<b)
{
while(x/p!=b/p)
{
Run(x,nw,p);
x+=p;
}
nw--,p/=10;
}
while(x<b)
{
count(x);
x++;
}
count(x);
}
for(int i=0;i<=9;i++)printf("%lld ",ans[i]);
return 0;
}
------------恢复内容结束------------
以上是关于P2602 [ZJOI2010]数字计数的主要内容,如果未能解决你的问题,请参考以下文章