题目描述:
小蓝最近在学习二进制。他想知道 1 到 N 中有多少个数满足其二进制表示
中恰好有 K 个 1。你能帮助他吗?
输入格式:
输入一行包含两个整数 N 和 K。
输出格式
输出一个整数表示答案。
样例输入
7 2
样例输出
3
评测用例规模与约定
对于 30% 的评测用例,
1
≤
N
≤
1
0
6
,
1
≤
K
≤
10
。
1 ≤ N ≤ 10^6, 1 ≤ K ≤ 10。
1≤N≤106,1≤K≤10。
对于 60% 的评测用例,
1
≤
N
≤
2
×
1
0
9
,
1
≤
K
≤
30
。
1 ≤ N ≤ 2 × 10^9, 1 ≤ K ≤ 30。
1≤N≤2×109,1≤K≤30。
对于所有评测用例,
1
≤
N
≤
1
0
18
,
1
≤
K
≤
50
。
1 ≤ N ≤ 10^{18}, 1 ≤ K ≤ 50。
1≤N≤1018,1≤K≤50。
思路:
是一道标准的数位dp问题,求解在某个数字的区间内,满足某中性质的的数字个数。 一般来说这种问题都不会从这个区间的下界和上界来进行求解。而是利用前缀和的思想求出 s1 = 1~上界
和 s2 = 1~下界
,最后的答案为 s1 - s2
。而这一题的要求只是要找到1 ~上界满足条件的方案数,所以只需要求s1即可。下面会从记忆化搜索和普通dp两种方法对此题进行求解。
记忆化搜索:
记忆化搜索本质上是DFS, 但同时对每次搜索的结果进行保存, 当搜索到之前保存的状态时可以直接返回(剪枝), 所以思考保存那种状态就是这一题最关键的一点。考虑要求解的上界被分解成有x
位的01序列
,我们假设为1100
、k = 2
, 当我们枚举到第二个位时,如果为0
,既后面的位置可以随意安排,因为无论如何,当第二位为0
时10xx
永远不会大于上界1100
,所以我们所需要的就是后两位可以组成为有1(也就是k - 1)个1
的方案数即可。
所以我们可以定义状态数组f[i][j]为前i位的01序列可以任意放的情况下,有j个1的情况
。
#include<iostream>
#include<cstring>
using namespace std;
#define ll long long
ll n, k;
ll f[100][100];
int a[100];
/*
** pos : 枚举到第pos位
** pre : 第pos位之前有多少个1
** limit : 当前状态是否为上界
*/
ll dfs(int pos, int pre, int limit)
{
if(!pos) return pre == k; //枚举结束,判断上界是否合法
//在当前位置不在上界上,且已经背搜索的情况下可以直接返回
if(!limit && ~f[pos][pre]) return f[pos][pre];
int max_i = limit ? a[pos] : 1;
ll res = 0;
//这里其实只有0、1两种情况,但是对于一般的数位dp在不是上界的情况下可以枚举到(进制的大小-1)。
for(int i = 0; i <= max_i; i++ )
{
res += dfs(pos - 1, pre + i, limit && i == a[pos]);
}
return limit? res : f[pos][pre] = res;
}
ll calu(ll x)
{
memset(f, -1, sizeof f); //初始化f数组,-1的情况为,没有被搜到
int pos = 0;
while(x) a[++pos] = x % 2, x >>= 1; //上界构成的01序列
return dfs(pos, 0, 1);
}
int main()
{
cin >> n >> k;
cout << calu(n) << endl;
}
状态转移dp:
按照上面的思路我们从高位向低位枚举, 如果当前位置在没有取到上界的情况下它的方案数为 f[i][k - pre]
。就是k减去在这个位置之前上界有多少个1。而如果在当前位置任意的情况下 f[i][j]
只跟f[i - 1][j](当前位置为0))
和 f[i - 1][j - 1](当前位置为1)
有关。所以 f[i][j] = f[i - 1][j] + f[i - 1][j - 1]
这个就是状态转移方程。
#include<iostream>
#include<vector>
#include<cmath>
using namespace std;
#define ll long long
ll n, k;
ll f[100][100];
void init()
{
f[1][1] = 1;
f[1][0] = 1;
//1e18 大概为 2的64次方
for(int i = 2; i < 70; i++ )
{
//枚举1 - k
for(int j = 0; j <= 50; j++ )
{
//这里如果不是2进制的情况下要,用一个循环枚举每个数
if(j) f[i][j] += f[i - 1][j - 1]; //特判没有一个1的情况,如果 j - 1 为负值不会报错
f[i][j] += f[i - 1][j]; //程序会反向寻址
}
}
}
ll dp(int x)
{
vector<int> a;
while(x) a.push_back(x % 2), x /= 2;
ll res = 0, pre = 0;
for(int i = a.size() - 1; i >= 0; i-- )
{
int u = a[i];
//可以不是循环,与上面同理 。
//j 不能等于 u 就是不能枚举上界,应为在上界的情况下,后面的取值是有限制的。
for(int j = 0; j < u; j++ )
res += f[i][k - pre];
pre += u; //判断上界的第i位是否为1
if(pre == k) //如果上界已经有为1了之后的位置可以全填0
{
res++;
break;
}
//枚举到最后一位,且上界正好有k个1
if(!i && pre == k) res++;
}
return res;
}
int main()
{
init();
cin >> n >> k;
cout << dp(n) << endl;
return 0;
}