题目描述
难度分:1800
输入二进制数s,范围1≤s<21000,不含前导零。
然后输入k(0≤k≤1000)。
定义f(x)为x的二进制表示中的1的个数。
定义f∗(x)为使f(f(…f(x)))=1的最少嵌套(迭代)次数。也就是不断地把x更新为f(x),最少要更新多少次,才能使x变成1。
例如f∗(6)=2,因为f(f(6))=f(2)=1。
输出[1,s]中有多少个数x满足f∗(x)=k。答案模109+7。
输入样例1
110
2
输出样例1
3
输入样例2
111111011
2
输出样例2
169
算法
数位DP
这个题比较容易看出来是数位DP
,因为即使s可以达到21000,它经过一步f也会变为1000以内。所以我们先做一个预处理,暴力计算x∈[1,1000]中所有x变到1的f∗值,存入到f[x]数组中,然后就可以数位DP
了。数位DP
过程用灵佬的模板即可,非常套路。
状态定义
通过递归函数来定义状态,递归函数需要4个参数:(1) 当前遍历到第i位;(2) 当前构造的数有x个1;(3) 前面填的数字是否跟上限s一样limit;(4) 之前是否填过数字isnum。
状态转移
对齐上限s的位数,从最高位(从左往右第0位)开始往最低位(第n−1位)填数。
base case:已经遍历完了最低位,且isnum为1,表示前面填过数字,此时就刚好形成了一种有效的方案,只要f[x]+1=k就说明当前构造出来一个s以内的n位二进制数,可以在k步操作后变成1。加1是因为存在一步从大数变到1000以内的先导操作,f[x]只是1000以内的数变到1的操作次数。
一般情况:index<n,说明当前位需要填数,分为以下几种情况:
- isnum为0表示前面的位没有填过数,当前位仍然可以不填数,反正如果最后一个数都没填,这个分支返回的方案数就是0。
- 当前位填数,如果前面的位和上限一模一样,那么当前位i能填的最大数ub就是s[i],否则就是1。而我们是不允许有前导零的,所以如果前面填过数,就可以从0开始,否则应该从1开始。因此得到当前位能填的数字范围d∈[1−isnum,ub],一个个尝试。
最后需要注意一种情况,如果k=1,那说明1多算了一次,需要从最终答案中减去。因为1经过一次操作后是保持不变的,不能按照上述思路的f[1]+1=k来统计。
复杂度分析
设s的长度为n。
时间复杂度
相较于DP
过程,预处理的时间复杂度可以忽略不计,因此只要分析数位DP
的时间复杂度即可。状态数量是4n2,即O(n2)规模,单次转移的时间复杂度为O(1),因此整个算法的时间复杂度为O(n2)。
空间复杂度
空间消耗主要在于DP
数组,即状态数量的空间级别。因此,额外空间复杂度为O(n2)。
C++ 代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1001, MOD = 1e9 + 7;
string s;
int n, k, f[N], dp[N][N][2][2];
int dfs(int i, int x, int limit, int isnum) {
if(i == n) {
return isnum? (f[x] + 1 == k? 1: 0) : 0;
}
int &v = dp[i][x][limit][isnum];
if(v != -1) return v;
int digit = s[i] - '0';
int cnt = isnum? 0: dfs(i + 1, x, 0, isnum);
int ub = limit == 1? digit: 1;
for(int d = 1 - isnum; d <= ub; d++) {
cnt = (cnt + dfs(i + 1, x + d, (limit && d == digit)? 1: 0, 1)) % MOD;
}
return v = cnt;
}
int main() {
cin >> s >> k;
if(k == 0) {
cout << "1" << endl;
return 0;
}
n = s.size();
for(int i = 0; i <= n; i++) {
for(int j = 0; j <= n; j++) {
dp[i][j][0][0] = dp[i][j][0][1] = dp[i][j][1][0] = dp[i][j][1][1] = -1;
}
}
for(int i = 1; i <= 1000; i++) {
int x = i, op = 0;
while(x > 1) {
x = __builtin_popcount(x);
op++;
}
f[i] = op;
}
cout << (dfs(0, 0, 1, 0) - (k == 1? 1: 0)) << '\n';
return 0;
}