Hard

题目描述

给定两个整数 nk

对于任何正整数 x,定义以下序列:

  • p₀ = x
  • pᵢ₊₁ = popcount(pᵢ) 对于所有 i >= 0,其中 popcount(y)y 的二进制表示中设置位(1)的数量。

这个序列最终会达到值 1。

x 的 popcount-depth 定义为使得 pᵈ = 1 的最小整数 d >= 0

例如,如果 x = 7(二进制表示为 “111”),那么序列是:7 → 3 → 2 → 1,所以 7 的 popcount-depth 是 3。

你的任务是确定范围 [1, n] 中 popcount-depth 恰好等于 k 的整数数量。

返回这样的整数数量。

示例 1:

输入:n = 4, k = 1
输出:2
解释:
范围 [1, 4] 中 popcount-depth 恰好等于 1 的整数如下:

x | 二进制 | 序列
2 | "10"   | 2 → 1
4 | "100"  | 4 → 1

因此答案是 2。

示例 2:

输入:n = 7, k = 2
输出:3
解释:
范围 [1, 7] 中 popcount-depth 恰好等于 2 的整数如下:

x | 二进制 | 序列
3 | "11"   | 3 → 2 → 1
5 | "101"  | 5 → 2 → 1  
6 | "110"  | 6 → 2 → 1

因此答案是 3。

约束条件:

  • 1 <= n <= 10¹⁵
  • 0 <= k <= 5

解题思路

这道题需要用到数位动态规划和组合数学的知识。

核心思路:

  1. 预计算深度表:首先计算每个可能的 1 的数量(0 到 64)对应的 popcount-depth。由于最多 64 位,1 的数量最多为 64,而 64 的 popcount 是 2,2 的 popcount 是 1,所以深度最多为 3。

  2. 数位动态规划:使用数位 DP 统计不超过 n 的数字中,恰好有 j 个 1 的数字个数。定义状态 dp[pos][ones][tight]

    • pos:当前处理到第几位
    • ones:已经选择的 1 的数量
    • tight:是否受到上界限制
  3. 状态转移:对于每一位,可以选择放 0 或 1(如果 tight 为真,还要考虑不超过 n 对应位的限制)。

  4. 合并答案:对于每个可能的 1 的数量 j,如果其对应的 popcount-depth 等于 k,就将其对应的数字个数加入答案。

算法步骤:

  1. 预计算深度数组 depth[j],表示 j 个 1 对应的 popcount-depth
  2. 使用数位 DP 计算 count[j],表示不超过 n 且恰好有 j 个 1 的数字个数
  3. 遍历所有 j,如果 depth[j] == k,将 count[j] 加入答案

代码实现

class Solution {
public:
    long long popcountDepth(long long n, int k) {
        // 预计算每个1的个数对应的popcount-depth
        vector<int> depth(65, 0);
        for (int i = 1; i <= 64; i++) {
            int cur = i;
            int d = 0;
            while (cur > 1) {
                cur = __builtin_popcount(cur);
                d++;
            }
            depth[i] = d;
        }
        
        // 数位DP
        string binary = "";
        long long temp = n;
        while (temp > 0) {
            binary = (char)('0' + temp % 2) + binary;
            temp /= 2;
        }
        
        int len = binary.length();
        vector<vector<vector<long long>>> dp(len + 1, vector<vector<long long>>(65, vector<long long>(2, -1)));
        
        function<long long(int, int, bool)> solve = [&](int pos, int ones, bool tight) -> long long {
            if (pos == len) {
                return ones;
            }
            
            if (dp[pos][ones][tight] != -1) {
                return dp[pos][ones][tight];
            }
            
            int limit = tight ? (binary[pos] - '0') : 1;
            long long result = 0;
            
            for (int digit = 0; digit <= limit; digit++) {
                bool newTight = tight && (digit == limit);
                result += solve(pos + 1, ones + digit, newTight);
            }
            
            return dp[pos][ones][tight] = result;
        };
        
        // 计算每个1的个数对应的数字个数
        vector<long long> count(65, 0);
        for (int i = 0; i <= 64; i++) {
            fill(dp.begin(), dp.end(), vector<vector<long long>>(65, vector<long long>(2, -1)));
            count[i] = solve(0, 0, true);
            if (i > 0) count[i] -= count[i-1];
        }
        
        long long answer = 0;
        for (int i = 1; i <= 64; i++) {
            if (depth[i] == k) {
                answer += count[i];
            }
        }
        
        return answer;
    }
};
class Solution:
    def popcountDepth(self, n: int, k: int) -> int:
        # 预计算每个1的个数对应的popcount-depth
        depth = [0] * 65
        for i in range(1, 65):
            cur = i
            d = 0
            while cur > 1:
                cur = bin(cur).count('1')
                d += 1
            depth[i] = d
        
        # 数位DP
        binary = bin(n)[2:]
        length = len(binary)
        
        from functools import lru_cache
        
        @lru_cache(None)
        def dp(pos, ones, tight):
            if pos == length:
                return 1 if ones >= 0 else 0
            
            limit = int(binary[pos]) if tight else 1
            result = 0
            
            for digit in range(limit + 1):
                new_tight = tight and (digit == limit)
                result += dp(pos + 1, ones - digit, new_tight)
            
            return result
        
        # 计算每个1的个数对应的数字个数
        count = [0] * 65
        for i in range(65):
            count[i] = dp(0, i, True)
        
        # 转换为差分数组
        for i in range(64, 0, -1):
            count[i] -= count[i-1]
        
        # 计算答案
        answer = 0
        for i in range(1, 65):
            if depth[i] == k:
                answer += count[i]
        
        return answer
public class Solution {
    public long PopcountDepth(long n, int k) {
        // 预计算每个1的个数对应的popcount-depth
        int[] depth = new int[65];
        for (int i = 1; i <= 64; i++) {
            int cur = i;
            int d = 0;
            while (cur > 1) {
                cur = System.Numerics.BitOperations.PopCount((uint)cur);
                d++;
            }
            depth[i] = d;
        }
        
        // 数位DP
        string binary = Convert.ToString(n, 2);
        int len = binary.Length;
        long[,,] memo = new long[len + 1, 65, 2];
        
        for (int i = 0; i <= len; i++) {
            for (int j = 0; j <= 64; j++) {
                for (int k2 = 0; k2 < 2; k2++) {
                    memo[i, j, k2] = -1;
                }
            }
        }
        
        long Solve(int pos, int ones, bool tight) {
            if (pos == len) {
                return ones;
            }
            
            int tightInt = tight ? 1 : 0;
            if (memo[pos, ones, tightInt] != -1) {
                return memo[pos, ones, tightInt];
            }
            
            int limit = tight ? (binary[pos] - '0') : 1;
            long result = 0;
            
            for (int digit = 0; digit <= limit; digit++) {
                bool newTight = tight && (digit == limit);
                result += Solve(pos + 1, ones + digit, newTight);
            }
            
            return memo[pos, ones, tightInt] = result;
        }
        
        // 计算每个1的个数对应的数字个数
        long[] count = new long[65];
        for (int i = 0; i <= 64; i++) {
            for (int j = 0; j <= len; j++) {
                for (int l = 0; l <= 64; l++) {
                    for (int m = 0; m < 2; m++) {
                        memo[j, l, m] = -1;
                    }
                }
            }
            count[i] = Solve(0, 0, true);
            if (i > 0) count[i] -= count[i-1];
        }
        
        long answer = 0;
        for (int i = 1; i <= 64; i++) {
            if (depth[i] == k) {
                answer += count[i];
            }
        }
        
        return answer;
    }
}
/**
 * @param {number} n
 * @param {number} k
 * @return {number}
 */
var popcountDepth = function(n, k) {
    function popcount(x) {
        let count = 0;
        while (x > 0) {
            count += x & 1;
            x >>= 1;
        }
        return count;
    }
    
    function getDepth(x) {
        let depth = 0;
        while (x !== 1) {
            x = popcount(x);
            depth++;
        }
        return depth;
    }
    
    if (k === 0) return n >= 1 ? 1 : 0;
    
    // Find the range of popcount values that lead to depth k-1
    const targetPopcounts = new Set();
    for (let pc = 1; pc <= 64; pc++) {
        if (getDepth(pc) === k - 1) {
            targetPopcounts.add(pc);
        }
    }
    
    if (targetPopcounts.size === 0) return 0;
    
    function countWithPopcount(limit, target) {
        const s = limit.toString(2);
        const memo = new Map();
        
        function dp(pos, count, tight) {
            if (pos === s.length) {
                return count === target ? 1 : 0;
            }
            
            const key = `${pos},${count},${tight}`;
            if (memo.has(key)) return memo.get(key);
            
            const maxDigit = tight ? parseInt(s[pos]) : 1;
            let result = 0;
            
            for (let digit = 0; digit <= maxDigit; digit++) {
                result += dp(
                    pos + 1,
                    count + digit,
                    tight && digit === maxDigit
                );
            }
            
            memo.set(key, result);
            return result;
        }
        
        return dp(0, 0, true);
    }
    
    let result = 0;
    for (const target of targetPopcounts) {
        result += countWithPopcount(n, target);
    }
    
    return result;
};

复杂度分析

复杂度类型复杂度
时间复杂度O(log²n × 64)
空间复杂度O(log n × 64)

其中 log n 是 n 的二进制位数,64 是可能的 1 的数量上限。数位 DP 需要遍历每个位置和每种可能的 1 的数量,预计算深度数组的复杂度是常数。

相关题目