题目描述及输入输出约定:
比赛时的想法:
10^18的K,确实让人望尘莫及,但是总觉得二分或许能排上用场。
最初的想法是直接模拟,存储10^6个字符(打表),但是在使用itoa()函数时,我竟然认为数字0会被转换成0(字符串结束符),而不是‘0’,也没手动写转换函数,后来想想真不知道那时候在想什么。
放弃上述想法后,我又想用前缀和,用sum[i]表示,序列加到i时的总长度,用a[i]表示从12345...i有多长,然后在O(N)内枚举i,虽然到不了10^18,但是当时觉得还行
也就是这样预处理:
for(int now=1;now<=100000;now++) { //a[j]是从1加到j需要占多少位 a[now]=a[now-1]+(int)log10((double)now)+1; sum[now]=sum[now-1]+a[now]; }
然后,对于输入的K,在sum数组中,二分求K的下界j(第一次大于等于K是sum[j]),那么多出来的就是K-sum[j-1],假设KK=K-sum[j-1];然后在a数组中,二分求KK的下界j,最后判断是j的第几位即可。
想法确实是这样,但是二分是我才接触的,很生,写了很长时间,边界细节一直不敢确定,结束时也没写完,最后交了一个30分的代码,遗憾离场。
没写完的版本:
#include <cstdio> #include <iostream> #include <string> #include <cmath> #include <algorithm> using namespace std; typedef long long ll; const int MAXN=1e5+5; ll a[MAXN],s[MAXN]; int main() { for(int now=1;now<=100000;now++) { //a[j]是从1加到j需要占多少位 a[now]=a[now-1]+(int)log10((double)now)+1; s[now]=s[now-1]+a[now]; } int Q; cin>>Q; while(Q--) { //s是递增的,二分K在s中的位置 ll k; scanf("%lld",&k); //求下界 int num=lower_bound(s+1,s+1+100000,k)-s; //加了从1到num的序列后,长度刚好超过k,超过k的部 int D=k-s[num-1]; //加了D个后才到k,下一步要找加到几长度能是D,从a中找 int numD=lower_bound(a+1,a+1+100000,D)-a; //加到numD长度是D或刚超过D //找出是numD的哪一位数字 int D2=D-a[numD-1]; //实际上差几个 int WS=a[numD]-a[numD-1]; //numD有几位数 , 取NUMD的第D2位数即可,正数第D2位 int pp=WS-D2+1; if(pp==0) pp=1; int ans=(numD/(pp*10))%10; cout<<ans<<endl; } }
用等差数列求和降低复杂度:
如果采用等差数列,则能以常数复杂度求出序列长度,就不必再像上面存储序列的长度,因为等差数列仅在确定的位数内成立,即从i位到第i+1位时,公差D会变化,所以预存每个边界的序列长度(加到不超过10位数,就超过了18次方)。以等差数列为基础,再二分即可。
具体的过程:求加到了第几位数i、求加到了哪个数certainValue、求加到了1-certainValue中的哪一个数dst、判定是dst的第几位。
总结:
关于这道题:
多做做二分的题,即使是浅显的二分题,主要目的是熟悉。
对于long long:如果要用long long,则中间数也要用long long,包括中间值tmp,函数参数,函数返回值。
关于这次模测:
很失败,思维混乱,不知道在想什么,或许是因为来了几个亲戚,家里比较乱。
但是,以后,对于每一个思想,都要进行可行性分析,主要是能不能写出来。
如果感觉有风险,就把保险的部分先写好,如果最后没写完,就把保险代码交上。
代码:
#include <cstdio> #include <iostream> #include <cmath> using namespace std; typedef long long ll; ll sum[50]; //sum[i]表示序列加到 10^i-1 的长度,也就是加完第k位数及其之前的 ll base[50]; //base[i]表示序列从1到10^i-1的长度 ll K; //返回longlong的函数,中间变量也要用longlong long long qpow(long long a,long long b) { if(b==0) return 1; else if( (b&1) == 0) //b是偶数 { return qpow(a*a,b/2); } else return a*qpow(a*a,b/2); //即使a是奇数,比如5,5/2=2,也不影响,但是会递归到1/2=0的时候,所以有一个边界条件是b=0; } //evaluate sum[] long long sumOfSeries(ll begin,ll n,ll d) //等差数列求和:首项,项数,公差 { return begin*n+(n-1)*n*d/2; } void evaluteSum() { for(int i=1;i<=10;i++) { ll tot=9*qpow(10,i-1); //tot表示i位数一共有多少个,比如2位数有90个,3位数有900个 base[i]=base[i-1]+tot*i; sum[i]=sum[i-1]+sumOfSeries(base[i-1]+i,tot,i); } } ll LowerBound(ll tot,int i) //求下界,找第一个大于等于的 { ll L=1,R=tot; //第i位数一共有tot个 ll certain=0; while(L<=R) { ll Mid=(L+R)/2; ll now=sumOfSeries(base[i-1]+i,Mid,i); if(now>=K) certain=Mid,R=Mid-1; else if(now<K) L=Mid+1; } return certain; } int main() { evaluteSum(); int Q; cin>>Q; while(Q--) { cin>>K; // int len=(int)log10((double)K)+1; int i=1; while(K>sum[i]) i++; //i表示加到第i位数时,序列长度到了K K-=sum[i-1]; //假设在第i位数中,加到第certain项,长度大于等于K ll certain=LowerBound(9*qpow(10,i-1),i); ll certainValue=qpow(10,i-1)+certain-1; K=K-sumOfSeries(base[i-1]+i,certain-1,i); //从1-certainValue中找下界 ll dst=0,num=0; ll L=1,R=certainValue; while(L<=R) { ll Mid=(L+R)/2; int len=(int)log10((double)Mid)+1; ll now=base[len-1]+len*(Mid-qpow(10,len-1)+1); if(now>=K) dst=Mid,num=now-K+1,R=Mid-1; else L=Mid+1; } //答案就是dst从右往左数的第num位 int len=(int)log10((double)dst)+1; ll ans=dst/( qpow(10,num-1) ) %10; cout<<ans<<endl; } }