《算法竞赛·快冲300题》每日一题:“二进制中的1”

算法竞赛·快冲300题》将于2024年出版,是《算法竞赛》的辅助练习册。
所有题目放在自建的OJ New Online Judge
用C/C++、Java、Python三种语言给出代码,以中低档题为主,适合入门、进阶。


二进制中的1” ,链接: http://oj.ecustacm.cn/problem.php?id=1803

题目描述

【题目描述】 f(i)表示数字i中二进制1的数量,求f(1)f(2)…*f(n):
【输入格式】 输入一个正整数n,n不超过10^15。
**【输出格式】**输出一个数字表示答案,对10000007取模。
【输入样例】

10

【输出样例】

96

题解

   n最大是 1 0 15 10^{15} 1015,如果直接求f(1)、f(2)、…、f(n),超时。
   如何求f(1)×f(2)×…×f(n)?可以利用二进制的特征,找规律。
   在1~n这n个数的二进制表示中,包含1个“1”的数有1、10、100、1000、…,设共有a个;
   包含2个“1”的数有11、101、110、1001、…,设共有b个;
   包含3个“1”的数有111、1011、1101、…等等。
   那么 f ( 1 ) × f ( 2 ) × . . . × f ( n ) = 1 a × 2 b × 3 c × 4 d × . . . f(1)×f(2)×...×f(n) = 1^a×2^b×3^c×4^d×... f(1)×f(2)×...×f(n)=1a×2b×3c×4d×...,只要求出a、b、c、d、…即可算出答案。
   如何求a、b、c、d、…?例如n = 1 0 15 10^{15} 1015时,在1~n这n个数中,包含1个“1”的数有a = 50个,含有2个“1”的数有b = 1225个;… 含有3个“1”的数有c = 19600个;…等等。
   本题是一道典型的数位统计DP题目:计算1~n中包含k个“1”的数有多少个。k = 1, 2, 3, … len,这里len是数的二进制表示的最大长度,例如22,二进制10110,len = 5。
   注:下面的题解可能还是有些晦涩,需要先学习数位统计DP的原理和两种编码方法,参考《算法竞赛》,清华大学出版社,罗勇军、郭卫斌著,333~338页。
   定义状态dp[pos][sum],表示当前二进制数长度为pos位,前面1的个数为sum个时,含有k个“1”的数字个数。
   例:
   (1)k=1。dp[1][0]=1,二进制数长度为1,即数字0和1,且前面没有“1”,那么含有k=1个“1”的数有1个,就是1。dp[2][1]=1,二进制数长度为2,数字范围00-11,前面有1个“1”,那么00-11这4个数只能取00,只有1个。dp[5] [0] = 5,在数字00000-11111中,可以取1、10、100、1000、10000,共5个。
   (2)k=2。dp[3][0]= 3,二进制数长度为0,数字范围000-111,前面没有“1”,那么含有k=2个“1”的数有3个,即011、101、110。dp[4] [1] = 4,二进制数长度为4,数字范围0000-1111,前面有一个“1”,那么0000-1111中只能取有1个“1”的情况,即0001、0010、0100、1000。
   数位统计DP有两种代码实现:递推实现、记忆化搜索。记忆化搜索的逻辑更清晰一些。
   函数dfs(int pos, int sum, bool limit, int k):计算dp[pos][num]。
   返回值:有k个“1”的数有多少个。
   pos:当前处理到第pos位数;
   sum:前面有sum个1;
   limit:当前最高pos位有没有限制。false表示没有限制,这一位等于0或1都行;true表示有限制,只能是1,或者只能是0。
   k:这个二进制数字中只能有k个1。
   以n = 22为例,n的二进制10110,长度len=5,下面介绍如何计算k=1个“1”的数有多少个。
   第一次,dfs(len, 0, true,1):当前处理到第pos = len = 5位,即最高位;sum = 0,前面没有出现过1;limit = true,表示最高位(len=5这一位)有限制,只能是1;k=1,这个二进制数中只能有1个“1”。
   dfs(len, 0, true,1)的返回值:返回1~10110范围内,只有k = 1个“1”的数有多少个。答案是5个,即1、10、100、1000、10000。
   继续dfs时,有两种情况:
   (1)若最高位pos=1,说明出现了一个“1”,下一步这样继续dfs:
   dp[pos][sum] += dfs(pos-1, sum+1, limit&&(i==up), k);

   pos-1,继续处理下一位;sum+1,前面出现了1个“1”;limit&&(i==up),当前最高位有限制且是1。

   (2)若最高位pos=0,说明前面没有没有出现“1”,下一步这样继续dfs:
   dp[pos][sum] += dfs(pos-1, sum, limit&&(i==up), k);

   pos-1,继续处理下一位;sum,前面没有出现“1”,sum不变;limit&&(i==up),当前最高位有限制且是0。

   用DP计算出a、b、c、…后,再计算 f ( 1 ) × f ( 2 ) × . . . × f ( n ) = 1 a × 2 b × 3 c × . . . f(1)×f(2)×...×f(n) = 1^a×2^b×3^c×... f(1)×f(2)×...×f(n)=1a×2b×3c×...。由于a、b、c、…可能很大,需要用快速幂计算。例如 n = 1 0 15 ≈ 2 50 n = 10^{15} ≈ 2^{50} n=1015250时, f ( 1 ) × f ( 2 ) × . . . × f ( n ) = 1 a × 2 b × 3 c × . . . × 4 9 y × 5 0 z , a + b + c + . . . + y + z ≈ 2 50 f(1)×f(2)×...×f(n) = 1^a×2^b×3^c×...×49^y×50^z,a+b+c+... +y+z ≈ 2^{50} f(1)×f(2)×...×f(n)=1a×2b×3c×...×49y×50za+b+c+...+y+z250
【重点】 数位统计DP。

C++代码

 #include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll dp[60][60];
int num[60];
ll MOD = 1e7+7;
ll fastpow(ll a, ll n){       //快速幂
    ll ans = 1;
    a %= MOD;
    while(n){
        if (n&1)    ans = ans*a % MOD;
        a= a*a % MOD;
        n >>= 1;
    }
    return ans;
}
ll dfs(int pos, int sum, bool limit, int k){   //返回值:数字中有k个1的数是多少个?
    if (pos == 0)       //递归到0位数,即整个数处理完了
        return sum == k;  //如果正好有sum=k个1,说明这个数字符合要求,返回1
    if (!limit && dp[pos][sum]!=-1)  //当前最高位没有限制,并且以前计算过,直接返回
        return dp[pos][sum];         //记忆化搜索
    int up;                          //up:当前最高位的最大值
    if (limit)  up = num[pos];       //当前最高位有限制,就是num[pos]
    else        up = 1;              //当前最高位没有限制,那么最大值是1
    dp[pos][sum] = 0;                //准备计算dp[pos][sum]
    for (int i=0 ; i<=up ; i++){
        if(i==1)  dp[pos][sum] += dfs(pos-1, sum+1, limit&&(i==up), k); 
        else      dp[pos][sum] += dfs(pos-1, sum,   limit&&(i==up), k);
    }
    return dp[pos][sum];
}
int main(){
    ll n; cin >> n;
    int len = 0;
    while(n){       //对n进行二进制分解,存在num[]中。
        len++;      //二进制的位数
        num[len] = n%2;
        n >>= 1;
    }
    ll ans = 1;
    for (int i=1 ; i<=len; i++){
        memset(dp, -1, sizeof(dp));
        ll cnt = dfs(len, 0, true, i);  //cnt:1~n中包含i个1的数有多少个
        ans = ans*fastpow(i, cnt)%MOD;
    }
    cout << ans;
    return 0;
}

Java代码

import java.util.Scanner;

public class Main {
    static long[][] dp = new long[60][60];
    static int[] num = new int[60];
    static long MOD = 10000000L + 7L;

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        long n = sc.nextLong();
        int len = 0;
        while (n != 0) {
            len++;
            num[len] = (int) (n % 2);
            n >>= 1;
        }
        long ans = 1L;
        for (int i = 1; i <= len; i++) {
            for (int j = 0; j <= len; j++) //初始化
                for (int k = 0; k <= len; k++)
                    dp[j][k] = -1L;
            long temp = dfs(len, 0, true, i);
            ans = ans * fastpow(i, temp) % MOD;
        }
        System.out.println(ans);
        sc.close();
    }
    static long fastpow(long a, long n) {
        long ans = 1L;
        a %= MOD;
        while (n != 0) {
            if ((n & 1) == 1)  ans = ans * a % MOD;
            a = a * a % MOD;
            n >>= 1;
        }
        return ans;
    }

    static long dfs(int pos, int sum, boolean limit, int k) {
        if (pos <= 0)  return (sum == k) ? 1L : 0L;
        if (!limit && dp[pos][sum] != -1)   return dp[pos][sum];
        int up = -1;
        if (limit)   up = num[pos];
        else         up = 1;
        dp[pos][sum] = 0L;
        for (int i = 0; i <= up; i++) {
            if (i == 1)   dp[pos][sum] += dfs(pos - 1, sum + 1, limit && (i == up), k);
            else          dp[pos][sum] += dfs(pos - 1, sum, limit && (i == up), k);
        }
        return dp[pos][sum];
    }
}

Python代码

dp = [[-1 for _ in range(60)] for _ in range(60)]
num = [0] * 60
MOD = 10000000 + 7

def fastpow(a, n):
    ans = 1
    a %= MOD
    while n:
        if n & 1:   ans = ans * a % MOD
        a = a * a % MOD
        n >>= 1
    return ans

def dfs(pos, sum, limit, k):
    if pos <= 0:   return int(sum == k)
    if not limit and dp[pos][sum] != -1:   return dp[pos][sum]
    up = -1
    if limit:   up = num[pos]
    else:       up = 1
    dp[pos][sum] = 0
    for i in range(up + 1):
        if i == 1: dp[pos][sum] += dfs(pos - 1, sum + 1, limit and (i == up), k)
        else:      dp[pos][sum] += dfs(pos - 1, sum,     limit and (i == up), k)
    return dp[pos][sum]

n = int(input())
len = 0
while n:
    len += 1
    num[len] = n % 2
    n >>= 1
ans = 1

for i in range(1, len + 1):
    for j in range(0, len + 1):
        for k in range(0, len + 1):
            dp[j][k] = -1
    cnt = dfs(len, 0, True, i)
    ans = ans * fastpow(i, cnt) % MOD
print(ans)

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

罗勇军

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值