引用:刘聪《浅谈数位类统计问题》
在信息学竞赛中,有这样一类问题:求给定区间中,满足给定条件的某个 D 进制数或此类数的数量。所求的限定条件往往与数位有关,例如数位之和、指定数码个数、数的大小顺序分组等等。题目给定的区间往往很大,无法采用朴素的方法求解。此时,我们就需要利用数位的性质, 设计 log(n)级别复杂度的算法。 解决这类问题最基本的思想就是 “逐位确定”的方法。下面就让我们通过几道例题来具体了解一下这类问题及其思考方法。
SPOJ-2319
题意:给定所有 K 位二进制数:0,1,…,2^K-1。你需要将它们分成恰好 M 组,每组都是原序列中连续的一些数。设 Si(1 ≤ i ≤ M)表示第 i 组中所有数的二进制表示中 1 的个数,S 等于所有Si中的最大值。你的任务是令 S 最小。
分析:二分枚举每组含有的1的个数的上限,通过判定该值是否满足的情况下最终得到最优解。判定的过程是一个贪心过程,即在已知某组1个数上限的情况下,使得一组的中的1尽可能的多,具体而言程序需要完成这几个功能:
1.查询[1, x]之间的所有数中1的个数:count(x)
2.查询1的个数小于 y 的[1, x]区间中最大的x:max(x) 满足 count(x) <= y这样对于枚举的一个值,进行一个最小化的分组,然后判定该分组数是否小于题目所给定的即可。第一个功能函数很好解决,第二个的话刚开始的时候是二分去找,这样时间复杂度受不了,相比逐位确定多了常数。逐位确定方式是,从高位开始一次试探该位为1是否可行,如果可能的话就将该位置1,当有一个问题就是某位如果是1,那么后面的取值将影响该位1出现的次数,因此就要设计一个累加的计数变量,表示该位之前有多少个1出现,如果某位为1,其对前面出现每一个1贡献该位的位权。
这题使用网上的大数模板也是出了问题,在大数减法里面一个变量使用错误,郁闷......程序跑了27S+,效率确实不是很高,不过打表输出应该也可以吧。
#include#include #include #include #include using namespace std; #define MAXN 9999#define MAXSIZE 10#define DLEN 4class BigNum{ private: int a[500]; //可以控制大数的位数 int len; //大数长度public: BigNum(){ len = 1;memset(a,0,sizeof(a)); } //构造函数 BigNum(const int); //将一个int类型的变量转化为大数 BigNum(const char*); //将一个字符串类型的变量转化为大数 BigNum(const BigNum &); //拷贝构造函数 BigNum &operator=(const BigNum &); //重载赋值运算符,大数之间进行赋值运算 friend istream& operator>>(istream&, BigNum&); //重载输入运算符 friend ostream& operator<<(ostream&, BigNum&); //重载输出运算符 BigNum operator+(const BigNum &) const; //重载加法运算符,两个大数之间的相加运算 BigNum operator-(const BigNum &) const; //重载减法运算符,两个大数之间的相减运算 BigNum operator*(const BigNum &) const; //重载乘法运算符,两个大数之间的相乘运算 BigNum operator/(const int &) const; //重载除法运算符,大数对一个整数进行相除运算 BigNum operator^(const int &) const; //大数的n次方运算 int operator%(const int &) const; //大数对一个int类型的变量进行取模运算 bool operator>(const BigNum & T)const; //大数和另一个大数的大小比较 bool operator<(const BigNum & T) const; bool operator==(const BigNum & T) const; bool operator>(const int & t)const; //大数和一个int类型的变量的大小比较 bool operator<(const int &t) const; bool operator==(const int &t) const; void print(); //输出大数}; bool BigNum::operator==(const BigNum & T) const { return !(*this > T) && !(T > *this);}bool BigNum::operator==(const int &t) const { BigNum T = BigNum(t); return *this == T;}bool BigNum::operator<(const BigNum & T) const { return T > *this; }bool BigNum::operator<(const int &t) const { return BigNum(t) > *this;}BigNum::BigNum(const int b) //将一个int类型的变量转化为大数{ int c,d = b; len = 0; memset(a,0,sizeof(a)); while(d > MAXN) { c = d - (d / (MAXN + 1)) * (MAXN + 1); d = d / (MAXN + 1); a[len++] = c; } a[len++] = d;}BigNum::BigNum(const char*s) //将一个字符串类型的变量转化为大数{ int t,k,index,l,i; memset(a,0,sizeof(a)); l=strlen(s); len=l/DLEN; if(l%DLEN) len++; index=0; for(i=l-1;i>=0;i-=DLEN) { t=0; k=i-DLEN+1; if(k<0) k=0; for(int j=k;j<=i;j++) t=t*10+s[j]-'0'; a[index++]=t; }}BigNum::BigNum(const BigNum & T) : len(T.len) //拷贝构造函数{ int i; memset(a,0,sizeof(a)); for(i = 0 ; i < len ; i++) a[i] = T.a[i]; } BigNum & BigNum::operator=(const BigNum & n) //重载赋值运算符,大数之间进行赋值运算{ int i; len = n.len; memset(a,0,sizeof(a)); for(i = 0 ; i < len ; i++) a[i] = n.a[i]; return *this; }istream& operator>>(istream & in, BigNum & b) //重载输入运算符{ char ch[MAXSIZE*4]; int i = -1; in>>ch; int l=strlen(ch); int count=0,sum=0; for(i=l-1;i>=0;) { sum = 0; int t=1; for(int j=0;j<4&&i>=0;j++,i--,t*=10) { sum+=(ch[i]-'0')*t; } b.a[count]=sum; count++; } b.len =count++; return in;}ostream& operator<<(ostream& out, BigNum& b) //重载输出运算符{ int i; cout << b.a[b.len - 1]; for(i = b.len - 2 ; i >= 0 ; i--) { cout.width(DLEN); cout.fill('0'); cout << b.a[i]; } return out;}BigNum BigNum::operator+(const BigNum & T) const //两个大数之间的相加运算{ BigNum t(*this); int i,big; //位数 big = T.len > len ? T.len : len; for(i = 0 ; i < big ; i++) { t.a[i] +=T.a[i]; if(t.a[i] > MAXN) { t.a[i + 1]++; t.a[i] -=MAXN+1; } } if(t.a[big] != 0) t.len = big + 1; else t.len = big; return t;}BigNum BigNum::operator-(const BigNum & T) const //两个大数之间的相减运算 { int i,j,big; bool flag; BigNum t1,t2; if(*this>T) { t1=*this; t2=T; flag=0; } else { t1=T; t2=*this; flag=1; } big=t1.len; for(i = 0 ; i < big ; i++) { if(t1.a[i] < t2.a[i]) { j = i + 1; while(t1.a[j] == 0) j++; t1.a[j--]--; while(j > i) t1.a[j--] += MAXN; t1.a[i] += MAXN + 1 - t2.a[i]; } else t1.a[i] -= t2.a[i]; } t1.len = big; while(t1.a[t1.len - 1] == 0 && t1.len > 1) { t1.len--; big--; } if(flag) t1.a[big-1]=0-t1.a[big-1]; return t1; } BigNum BigNum::operator*(const BigNum & T) const //两个大数之间的相乘运算 { BigNum ret; int i,j,up; int temp,temp1; for(i = 0 ; i < len ; i++) { up = 0; for(j = 0 ; j < T.len ; j++) { temp = a[i] * T.a[j] + ret.a[i + j] + up; if(temp > MAXN) { temp1 = temp - temp / (MAXN + 1) * (MAXN + 1); up = temp / (MAXN + 1); ret.a[i + j] = temp1; } else { up = 0; ret.a[i + j] = temp; } } if(up != 0) ret.a[i + j] = up; } ret.len = i + j; while(ret.a[ret.len - 1] == 0 && ret.len > 1) ret.len--; return ret; } BigNum BigNum::operator/(const int & b) const //大数对一个整数进行相除运算{ BigNum ret; int i,down = 0; for(i = len - 1 ; i >= 0 ; i--) { ret.a[i] = (a[i] + down * (MAXN + 1)) / b; down = a[i] + down * (MAXN + 1) - ret.a[i] * b; } ret.len = len; while(ret.a[ret.len - 1] == 0 && ret.len > 1) ret.len--; return ret; }int BigNum::operator %(const int & b) const //大数对一个int类型的变量进行取模运算 { int i,d=0; for (i = len-1; i>=0; i--) { d = ((d * (MAXN+1))% b + a[i])% b; } return d;}BigNum BigNum::operator^(const int & n) const //大数的n次方运算{ BigNum t,ret(1); int i; if(n<0) exit(-1); if(n==0) return 1; if(n==1) return *this; int m=n; while(m>1) { t=*this; for( i=1;i<<1<=m;i<<=1) { t=t*t; } m-=i; ret=ret*t; if(m==1) ret=ret*(*this); } return ret;}bool BigNum::operator>(const BigNum & T) const //大数和另一个大数的大小比较{ int ln; if(len > T.len) return true; else if(len == T.len) { ln = len - 1; while(a[ln] == T.a[ln] && ln >= 0) ln--; if(ln >= 0 && a[ln] > T.a[ln]) return true; else return false; } else return false; }bool BigNum::operator >(const int & t) const //大数和一个int类型的变量的大小比较{ BigNum b(t); return *this>b;}void BigNum::print() //输出大数{ int i; printf("%d", a[len-1]); for (int i = len-2; i >= 0; --i) { printf("%04d", a[i]); } puts("");}//---------------------大数分割线------------------------//const int N = 115;int K, M;BigNum _pow[N];BigNum last[N];BigNum f[N]; // f[i]表示i位完全可以取得的情况下含有1的个数 bool fcal[N]; // 用以记录f[i]是否已经被计算过 int bit[N];BigNum getone(int p, bool e) { if (p == -1) return 0; if (!e && fcal[p]) return f[p]; int LIM = e ? bit[p] : 1; BigNum sum = 0; for (int i = 0; i <= LIM; ++i) { sum = sum + getone(p-1, e&&i==LIM); if (i == 1) { // 如果枚举的该位为0 if (e&&i==LIM) { sum = sum + last[p-1] + 1; } else { sum = sum + _pow[p]; } } } if (!e) { f[p] = sum; fcal[p] = true; } return sum;}BigNum count(BigNum x) { // 1-x之间的数共有多少个1 int idx = 0; if (x == 0) return BigNum(0); while (!(x == 0)) { bit[idx++] = x % 2; x = x / 2; } last[0] = bit[0]; for (int i = 1; i < idx; ++i) { if (bit[i]) last[i] = _pow[i] + last[i-1]; else last[i] = last[i-1]; } return getone(idx-1, true);}BigNum find(BigNum x) { // 找到尽可能大的一个数k使得[1-k]中1的个数不超过x BigNum ret = 0; int tot = 0; x = x + 1; for (int i = K-1; i >= 0; --i) { ret = ret * 2; BigNum tmp; if (i > 0) tmp = f[i-1]+_pow[i]*tot+1; else tmp = _pow[i]*tot+1; if (x > tmp) { ret = ret + 1; ++tot; x = x-tmp; } } return ret;}bool check(BigNum mid) { int group = 0; BigNum sta = 1; while (_pow[K] > sta) { sta = find(count(sta-1)+mid) + 1; ++group; if (group > M) break; } return group <= M;}int main(){ memset(fcal, 0, sizeof (fcal)); _pow[0] = 1; for (int i = 1; i < 110; ++i) _pow[i] = _pow[i-1] * 2; while (scanf("%d %d", &K, &M) != EOF) { BigNum l = 1, r = count(_pow[K]), ret = 1; while (!(l > r)) { // 枚举每个分组中1的上限数量 BigNum mid = (l + r) / 2; if (check(mid)) { // 收缩限制 ret = mid; r = mid - 1; } else { // 由于枚举的1的个数不足以分成M组,因此应放宽限制 l = mid + 1; } } ret.print(); }}
ZOJ-2599
题意:定义一个数的各个位数字之和为该数的关键字,把1-n之间的数字,按照数位和排序,对于数位和相同的数字,字典序靠前的数排在前面。现在问K(1<=K<=n)这个数排在什么位置,排在第K位置的数是哪个数?n的范围为1-10^18。
分析:将第一问和第二问分开来考虑,对于第一问可以通过数位dp得出一个数组f[i][j]表示 i 位无限制数位和为 j 的数一共有多少个,当然也可以计算出任意一个数x,1-x之间有多少个数的数位和为特定的值。通过计算得到K的数位和w[K],那么所有数位和小于w[K]的数一定排在其前面,对于数位和相同的数,通过枚举字典序在K前面的数的长度来得到。对于第二问,首先通过之前统计的某个数位和的个数计算出第K个数的数位和为多少以及在数位和中字典序的位置,然后采用逐位确定的方法,相当于是枚举一个字符串的前缀一般,每次枚举出一个串就按照第一问的方式进行询问,如果大于等于得出的字典序,那么就可以确定一位,如果等于刚好要查的字典序,通过判定数位和是否满足要求来退出。
这里有几个trick:在计算某个数字x排在数位和为固定值中的字典序值的时候,对x的扩展要达到和n相同的长度,并且对可能的溢出要进行处理;其次第二问中在枚举每一位数字的时候有时候将出现没有一个数位满足,此时应将该位赋值为9,因为这样才能保证最后得到一个字典序非常大的数。
#include#include #include #include #include using namespace std;typedef long long LL;LL f[20][200];LL n, K;LL sum[200];int bit[20];LL cal(int idx, int k, bool e) { if (idx == 0) return k == 0; // 已经没有其余位,如果所需和值为0,返回1 if (!e && f[idx][k] != -1LL) return f[idx][k]; LL sum = 0; int LIM = e ? bit[idx] : 9; for (int i = 0; i <= LIM; ++i) { if (k < i) break; sum += cal(idx-1, k-i, e&&i==LIM); } if (!e) f[idx][k] = sum; return sum;}LL count(LL x, int k) { // 同一[1, x]中和为k的数共有多少个数 int idx = 1; while (x) { bit[idx++] = x % 10; x /= 10; } return cal(idx-1, k, true);}int getdigit(LL x) { int ret = 0; while (x) { ret += x % 10; x /= 10; } return ret; }int getlen(LL x) { int ret = 0; while (x) { ret++; x /= 10; } return ret;}LL check(LL x, int w) { LL ret = 0; int len = getlen(x); int lmax = getlen(n); for (LL t = x; t > 0; t /= 10) { // 枚举长度小于等于K长度的数字 ret += count(t, w)-max(f[len-1][w], 0LL); len--; } len = getlen(x); for (LL t = x; len+1 <= lmax; ) { if (t <= n/10 && t*10 <= n) { t *= 10; len++; ret += count(t, w)-max(f[len-1][w], 0LL)-(getdigit(t) == w); } else { len++; ret += count(n, w)-max(f[len-1][w], 0LL); } // -1是因为乘10之后的数字数位和可能为digit,但是其字典序在K之后 } return ret;}void solve1(LL K) { // 解决第一个问题:数 K 排在什么位置 int w = getdigit(K); LL ret = sum[w-1]; // 接下来就是要求出K所在数位和为digit的字典序,方法是枚举数位 // 和同为digit但是字典序比K小的数的长度,通过给K乘10或者除10来 // 扩展K,这个扩展并不影响K与其比较,因为一旦长度确定其一定在K // 的合理位置(不被扩展来的0决定,不被除去的位确定)就比较出大小 ret += check(K, w); cout << ret << " ";}void solve2(LL K) { // 解决第二个问题:排在第 K 位置的数是 int w; // 排在第K位置的数字的数位和 for (int i = 1; i < 200; ++i) { if (sum[i] >= K) { K -= sum[i-1]; w = i; break; } } // 计算出数位和所在的位置,接着找出字典序为新的K的数字,其取值为1到给数位和的总数字 int len = getlen(n); LL ret = 0; bool finish = false; for (int i = 1; i <= len && !finish; ++i) { // 从len位枚举到第一位 ret *= 10; int choose = -1; for (int j = ret?0:1; j <= 9; ++j) { LL tmp = check(ret + j, w); if (tmp >= K) { if (tmp == K && getdigit(ret + j) == w) { finish = true; choose = j; break; } choose = j - 1; break; } } if (choose == -1) choose = 9; ret += choose; // 把每一位的和统计起来 } cout << ret << endl;}int main() { memset(f, 0xff, sizeof (f)); while (cin>>n>>K, n|K) { for (int i = 1; i < 200; ++i) { sum[i] = sum[i-1] + count(n, i); } // sum[i]表示位数之和为[1, i]的所有数的个数 solve1(K); solve2(K); } return 0;}
SGU-390
题意:给定区间[L, R],现在有一个售票员从L开始每次给顾客一个编号的票,并且卖给一个顾客连续编号的票,知道这些编号的数位和不小于K为止。问该区间能卖给多少个顾客。分析:此题虽是一个区间的题目,但是去没有办法想以往那般先计算区间[1, R]再计算区间[1, L],然后两者作差得到最后的结果。因为题目中有一个连续卖票的要求,意味着L编号的票必须卖给第一个顾客,并且当前的数位和值为0,两者的差值不能够保证这一点。因此换一个思路,直接将上下界代入到求解的过程中,引入两个变量lbound表示是否枚举到数位的下边界以及rbound表示是否枚举到数位的上边界。设状态dp[i][sum][rem]表示后 i 位任意,后 i 位之前已经确定的数字(数字是通过枚举来确定的)的数位和为sum,之前数字(相当于票卖到了后 i 位全部取0然后减一那个数时)剩下的尚未分组的数位和为rem。可以肯定rem一定是小于K的。那么这个状态就可以进行递推:
dp[i][sum][rem] = dp[i-1][sum + j][lastrem]
其中 j 的取值范围为0-9,当 j 取0的时候,lastrem取得值就是rem,后面lastrem要进行迭代,其实也就是不断的模拟后面连着的数字。
#include#include #include #include using namespace std;typedef long long LL;struct Node { LL a, b; Node () {} Node (LL _a, LL _b) : a(_a), b(_b) {}};Node f[20][200][1005];char fcal[20][200][1005];LL L, R; int K;// f[i][j][k] 表示i位数字可以任意取值,该区间需要累加的数位和值为 j,该区间之前有 k 的空间 // a元素表示分组的数量,b表示剩下的不能够在分组的数位和int low[20], high[20];int getdigit(LL x, int ary[]) { int idx = 1; while (x) { ary[idx++] = x % 10; x /= 10; } return idx;}Node count(int p, int sum, int rem, bool lb, bool rb) { // gt表示是否数值挨到了下边界、ls表示数值是否挨到了上边界 if (p == 0) { if (rem+sum