#P2795. 第3题-幸运数字
          
                        
                                    
                      
        
              - 
          
          
                      1000ms
            
          
                      Tried: 25
            Accepted: 7
            Difficulty: 8
            
          
          
          
                       所属公司 : 
                              美团
                                
            
                        
              时间 :2025年4月5日-算法岗
                              
                      
          
 
- 
                        算法标签>动态规划          
 
第3题-幸运数字
题解
题面描述
给定区间 [l,r] 和两个数字 x 与 k,数字 num 如果其各个位上的数字中 x 恰好出现 k 次,则称 num 为“小美的幸运数字”。
题目要求计算在区间 [l,r] 中有多少个幸运数字。
思路
由于区间的上界可以达到 1012,无法对所有数字进行枚举,因此采用**数字动态规划(Digit DP)**进行统计。基本思路如下:
- 
区间转换
先将问题转化为计算函数 f(n),表示区间 [0,n] 中满足条件的数字个数,然后答案为 f(r)−f(l−1)。 - 
状态设计
在数字动态规划中设计状态:- pos:当前处理的位,从最高位到最低位,数字总长度记为 L。
 - cnt:已经在非前导零部分中出现了数字 x 的次数。
 - tight:是否当前数字前缀与 n 相等(若为 1,则当前位的数字上界为对应位上的数字,否则上界为 9)。
 - started:是否已经遇到非零数字(前导零不计入计数)。
 
 - 
状态转移
在每一位上枚举当前数字 d,更新状态:- 如果 started 为 0 且 d=0,则数字仍处于前导零状态,计数不改变。
 - 若数字开始(即 started=1)且 d=x,则令 cnt 增加 1。
 - 当 cnt>k 时该状态无效,不予转移。
 
 - 
终止条件
当 pos=L 时,判断:- 若数字始终未开始(即数字为 0),则认为其表示为 “0”,此时若 x=0 且 k=1,计数为 1;否则计数为 0。
 - 否则判断 cnt 是否恰好等于 k,是则返回 1,否则返回 0。
 
 - 
记忆化
利用记忆化保存状态以防重复计算。 
代码分析
- 状态转移:通过枚举当前位的数字 d(范围从 0 到当前位上界),根据 tight 与 started 状态更新下一个状态,并增加计数(当且仅当数字已经开始且 d=x 时)。
 - 递归终止:当处理完所有位(pos=L)时,根据是否开始以及 cnt 是否等于 k 返回相应计数。
 - 记忆化:使用多维数组(或缓存)存储状态 dp[pos][cnt][tight][started],以降低时间复杂度。
 
C++ 代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
// 将数字转换为字符串表示
string toStr(ll n) {
    return to_string(n);
}
// 全局变量:
// dp 数组用于记忆化递归,vis 数组记录状态是否访问过
// dig 数组存储数字的每一位,L 表示数字长度
ll dp[20][20][2][2];
bool vis[20][20][2][2];
int dig[20];
int L;
// 数字动态规划递归函数
// cnt:已计数数字x出现的次数
// tight:是否受限于上界(0或1)
// started:是否已开始(0或1)
// x:目标数字x,k:要求出现次数k
ll solveDP(int pos, int cnt, int tight, int started, int x, int k) {
    if (pos == L) { // 处理完所有位
        if (!started) {
            // 数字全为前导零,此时数字为0
            // 如果x=0且k=1,则认为0中0出现一次
            return (x == 0 && k == 1) ? 1LL : 0LL;
        }
        return (cnt == k) ? 1LL : 0LL;
    }
    if (vis[pos][cnt][tight][started]) {
        return dp[pos][cnt][tight][started];
    }
    vis[pos][cnt][tight][started] = true;
    ll res = 0;
    int limit = (tight ? dig[pos] : 9);
    for (int d = 0; d <= limit; d++) {
        int ntight = (tight && (d == limit)) ? 1 : 0;      // 更新tight 状态
        int nstarted = (started || d != 0) ? 1 : 0;          // 更新 started状态
        int ncnt = cnt;
        // 如果数字已开始且当前位数字为x,则计数加1
        if (nstarted && d == x) ncnt++;
        if (ncnt > k) continue; // 超出要求次数则剪枝
        res += solveDP(pos + 1, ncnt, ntight, nstarted, x, k);
    }
    dp[pos][cnt][tight][started] = res;
    return res;
}
ll countLucky(ll n, int x, int k) {
    string s = toStr(n);
    L = s.size();
    for (int i = 0; i < L; i++) {
        dig[i] = s[i] - '0'; // 将字符转换为数字
    }
    memset(vis, 0, sizeof(vis)); // 重置记忆化数组
    return solveDP(0, 0, 1, 0, x, k);
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int t;
    cin >> t;
    while (t--) {
        ll l, r;
        int x, k;
        cin >> l >> r >> x >> k;
        ll ans = countLucky(r, x, k) - countLucky(l - 1, x, k);
        cout << ans << "\n";
    }
    return 0;
}
Python 代码
# 使用数字动态规划计算幸运数字个数
from functools import lru_cache
def count_lucky(n, x, k):
    """
    计算区间 [0, n] 中满足条件的幸运数字个数
    n:上界数字
    x:目标数字
    k:要求出现次数
    """
    s = str(n)
    L = len(s)
    
    @lru_cache(maxsize=None)
    def dp(pos, cnt, tight, started):
        # cnt:已计数数字x出现的次数
        # tight:是否受限于上界(True 或 False)
        # started:是否已开始(True 或 False)
        if pos == L:
            if not started:
                # 数字全为前导零,即数字为0
                return 1 if (x == 0 and k == 1) else 0
            return 1 if cnt == k else 0
        res = 0
        limit = int(s[pos]) if tight else 9
        for d in range(limit + 1):
            ntight = tight and (d == limit)  # 更新 tight 状态
            nstarted = started or (d != 0)    # 更新 started 状态
            ncnt = cnt
            # 如果数字已开始且当前位为目标数字x则计数加1
            if nstarted and d == x:
                ncnt += 1
            if ncnt > k:  # 剪枝,超出要求次数
                continue
            res += dp(pos + 1, ncnt, ntight, nstarted)
        return res
    return dp(0, 0, True, False)
if __name__ == "__main__":
    t = int(input())
    for _ in range(t):
        l, r, x, k = map(int, input().split())
        # 答案为 count_lucky(r, x, k) - count_lucky(l - 1, x, k)
        print(count_lucky(r, x, k) - count_lucky(l - 1, x, k))
Java 代码
import java.util.Scanner;
public class Main {
    // 全局变量:
    // dp:用于记忆化的数组
    // vis:记录状态是否访问过
    // dig:存储数字的每一位,L 表示数字长度
    static long[][][][] dp;
    static boolean[][][][] vis;
    static int[] dig;
    static int L;
    
    // 数字动态规划递归函数
    // cnt:已计数数字x出现的次数
    // tight:是否受限于上界(0或1)
    // started:是否已开始(0或1)
    // x:目标数字x,k:要求出现次数k
    public static long solveDP(int pos, int cnt, int tight, int started, int x, int k) {
        if (pos == L) { // 处理完所有位
            if (started == 0) {
                // 数字全为前导零,即数字为0
                return (x == 0 && k == 1) ? 1L : 0L;
            }
            return (cnt == k) ? 1L : 0L;
        }
        if (vis[pos][cnt][tight][started]) {
            return dp[pos][cnt][tight][started];
        }
        vis[pos][cnt][tight][started] = true;
        long res = 0;
        int limit = (tight == 1) ? dig[pos] : 9;
        for (int d = 0; d <= limit; d++) {
            int ntight = (tight == 1 && d == limit) ? 1 : 0;    // 更新 tight状态
            int nstarted = (started == 1 || d != 0) ? 1 : 0;      // 更新 started状态
            int ncnt = cnt;
            // 若数字已开始且当前位为目标数字x则计数加1
            if (nstarted == 1 && d == x) {
                ncnt++;
            }
            if (ncnt > k) continue; // 剪枝,超出要求次数
            res += solveDP(pos + 1, ncnt, ntight, nstarted, x, k);
        }
        dp[pos][cnt][tight][started] = res;
        return res;
    }
    
    // 计算区间 [0, n] 中满足条件的幸运数字个数
    public static long countLucky(long n, int x, int k) {
        String s = Long.toString(n);
        L = s.length();
        dig = new int[L];
        for (int i = 0; i < L; i++) {
            dig[i] = s.charAt(i) - '0';
        }
        // 初始化 dp 和 vis 数组,维度为[L+1][k+2][2][2]
        dp = new long[L + 1][k + 2][2][2];
        vis = new boolean[L + 1][k + 2][2][2];
        return solveDP(0, 0, 1, 0, x, k);
    }
    
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int t = sc.nextInt();
        while (t-- > 0) {
            long l = sc.nextLong();
            long r = sc.nextLong();
            int x = sc.nextInt();
            int k = sc.nextInt();
            // 答案为 countLucky(r, x, k) - countLucky(l - 1, x, k)
            long ans = countLucky(r, x, k) - countLucky(l - 1, x, k);
            System.out.println(ans);
        }
        sc.close();
    }
}
        题目内容
小美认为一个数字num,如果其数位上的数字x恰好出现k次,那么认为num是她的幸运数字,请你帮助小美计算在1至r有多少个幸运数字。
输入描述
第一行一个整数t(1≤t≤104),表示数据组数,对于每组数据格式为:
每行四个整数l,r,x,k(1≤l≤r≤1012,0≤x≤9,1≤k≤12)。
输出描述
对于每组数据输出一个整数,表示当前询问下的幸运数字个数。
样例1
输入
3
1 20 1 1
1 20 1 2
114514 5201314 5 2
输出
10
1 
550093