二进制问题(第十二届蓝桥杯国赛cb组)数位dp(记忆化搜索、递推 详细思路)

题目描述:

小蓝最近在学习二进制。他想知道 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。 1N106,1K10
对于 60% 的评测用例, 1 ≤ N ≤ 2 × 1 0 9 , 1 ≤ K ≤ 30 。 1 ≤ N ≤ 2 × 10^9, 1 ≤ K ≤ 30。 1N2×109,1K30
对于所有评测用例, 1 ≤ N ≤ 1 0 18 , 1 ≤ K ≤ 50 。 1 ≤ N ≤ 10^{18}, 1 ≤ K ≤ 50。 1N1018,1K50

思路:

是一道标准的数位dp问题,求解在某个数字的区间内,满足某中性质的的数字个数。 一般来说这种问题都不会从这个区间的下界和上界来进行求解。而是利用前缀和的思想求出 s1 = 1~上界 和 s2 = 1~下界,最后的答案为 s1 - s2。而这一题的要求只是要找到1 ~上界满足条件的方案数,所以只需要求s1即可。下面会从记忆化搜索和普通dp两种方法对此题进行求解。

记忆化搜索:

记忆化搜索本质上是DFS, 但同时对每次搜索的结果进行保存, 当搜索到之前保存的状态时可以直接返回(剪枝), 所以思考保存那种状态就是这一题最关键的一点。考虑要求解的上界被分解成有x位的01序列,我们假设为1100k = 2, 当我们枚举到第二个位时,如果为0,既后面的位置可以随意安排,因为无论如何,当第二位为010xx永远不会大于上界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;
}
  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值