幸运数字的二进制映射:长度分块与第K个数求解
如果你刷算法题时刷到洛谷这类OJ的P开头编号看到“P3499 幸运数字”这个名字第一反应大概率是“又一道数论题”。但我把这题做完之后发现它披着“幸运数字”的外衣内核其实是个非常经典的字典序编号问题给定一个只由4和7组成的数字集合求第n个数字或者统计某个区间内有多少个这样的数字。只要你抓住了“二进制映射”这个点整道题的代码可以短到十几行而且不容易错。本文就按这个思路来拆先讲清楚幸运数字这个集合的顺序规律再给出从暴力到打表、再到二进制定位的完整推导过程最后把区间计数的扩展做法和容易翻车的边界细节一起讲掉。适合刚接触这类“第K个数字”题目的新手也适合想复习计数类思路的竞赛选手。1. 读懂题面背后的顺序规律幸运数字本质上是字典序编号1.1 经典题面与两种常见问法这类题在不同题库里定义会有细微差别但最经典的版本是这样的一个正整数如果它的十进制表示中每一位都只含有4和7就称它为幸运数字。从小到大列出这些数字前几个是4, 7, 44, 47, 74, 77, 444, 447, 474, 477, 744, 747, 774, 777, 4444, ...题目最常见的问法有两种给定n输出第n个幸运数字给定区间[l, r]统计这个区间内幸运数字的个数。第二种问法还经常变形成“区间内第k个幸运数字是谁”本质上是一样的套路。很多第一次做这道题的人会先去枚举所有数字逐个判断是不是只含4和7。这个思路本身没错但它完全没有看到这个序列的内在结构所以一旦数据范围变大就会超时。1.2 把4和7翻译成0和1长度分块的威力你盯着上面这个序列看一会儿会发现在字典序也就是字符串比较顺序下有一个非常漂亮的分块结构长度为1的数字有2个4, 7长度为2的数字有4个44, 47, 74, 77长度为3的数字有8个444, 447, 474, 477, 744, 747, 774, 777长度为k的数字正好有2^k个。为什么是2^k因为长度为k的幸运数字每一位只能在4和7里二选一所以排列组合就是2^k。而这个规律意味着整个“所有幸运数字”的集合如果按长度从小到大排每个长度是一个独立的“块”块内部再按字典序排。更关键的一步来了把4看成二进制的0把7看成二进制的1。那么长度为k的幸运数字就和所有k位二进制数一一对应。例如长度为3的块里二进制映射幸运数字000444001447010474011477100744101747110774111777你会发现同长度块内部的字典序正好就是二进制数值从0到2^k-1升序。这不是巧合而是因为4和7两个字符的顺序对应了0和1两个数字的顺序。一旦看到这一层“求第n个幸运数字”就变成了“先确定它属于哪个长度块再在块内把序号翻译成二进制串”。这里有个容易理解偏的点不能直接用十进制整数比较来排全部幸运数字。因为4如果映射成0按整数看它小于一切但在幸运数字序列里所有一位数都排在两位数前面。长度是最高优先级其次才是块内的字典序。所以解题时要始终记得“先分长度块再处理块内偏移”。2. 暴力枚举与打表预处理看起来很简单但为什么不能直接交2.1 逐个判断的复杂度账本先看看大多数人的第一反应。写一个判断函数检查一个数字的十进制每一位是不是只有4和7然后从1开始往上枚举bool isLucky(long long x) { while (x 0) { long long d x % 10; if (d ! 4 d ! 7) return false; x / 10; } return true; }如果要找第n个幸运数字就写个循环从1开始计数每遇到一个幸运数字就把计数器加1直到计数器等于n。这个代码在n很小时完全没问题。但你看一下复杂度从1枚举到目标数字每个数字还要做一次位数级别的判断总复杂度大约O(target * len)。就算题目只要求n是1e6量级你也得枚举到几百万勉强能跑一旦n到1e9甚至1e12直接没戏。更现实的是这类题往往把n设置到1e18附近暴力枚举连边都摸不到。我见过不少新手在这道题上先写暴力然后试图用各种优化去“拯救”暴力比如减少取模次数、用整数缓存等。没用瓶颈是枚举的数字个数本身不是单次判断的速度。2.2 DFS打表什么时候它是可用方案除了逐个枚举另一个常见思路是用DFS递归构造所有幸运数字void dfs(string cur, int len, vectorstring out) { if ((int)cur.size() len) { out.push_back(cur); return; } dfs(cur 4, len, out); dfs(cur 7, len, out); }这样每一层递归决定一位先生成4再生成7正好也符合字典序。如果最高只需要到长度为K的块生成的幸运数字总数是2^1 2^2 ... 2^K 2^(K1) - 2当K20时这个数大约是200万完全可以接受甚至可以直接在本地把所有答案打成一个静态表提交。当K30时数量已经超过20亿时间和内存同时爆炸当K60时那是指数级灾难。所以打表方案是有适用边界的如果题目问第n个幸运数字且n对应的最大长度只有十几二十那么打表是又稳又快的做法如果n到了1e18最大长度接近60那就必须走二进制定位路线。这里也提醒一下很多人做这种题一上来就“打表预处理”其实是在小数据范围内够用但题目稍微改大一点就失灵了。真正值得掌握的是下面这种直接算的方法。3. 核心推导确定长度、计算偏移、做二进制映射3.1 等比数列与最小长度k把序列按长度分块后第一步要确定第n个幸运数字所在块的长度k。记长度不超过k的幸运数字总数为F(k) 2^1 2^2 ... 2^k 2^(k1) - 2要找第n个就是找最小的k使得F(k) n。例如n10F(2)6不够F(3)14够了所以k3。实现上不用真去解不等式直接用一个变量total累加2的幂就行k从1开始int k 1; long long total 0; while (total n) { total (1LL k); k; }循环结束时totalF(k-1)最后一次加进去的那一项是2^(k-1)所以循环结束后k比实际长度多1但要注意处理方式。我更推荐把total和k分开维护写成下面这样更清晰int k 1; long long total 2; // F(1) 2 while (total n) { k; total (1LL k); } // 此时k就是最小满足F(k) n的长度循环结束后长度小于k的所有幸运数字总数是F(k-1) (2^k) - 2所以第n个幸运数字在长度k块内的1-based偏移量pos n - (F(k-1))这个pos一定在[1, 2^k]范围内。接下来就是核心的一步把pos翻译成具体字符串。3.2 偏移量到01串的转换pos-1才是真正的二进制序号现在我们已经知道目标数字长度为k并且在长度为k的块内是第pos个。前面那张表告诉我们同一个块内二进制数值与字典序一一对应且块内第1个对应二进制0第2个对应二进制1依此类推块内第pos个对应二进制值pos-1。所以要先做pos pos - 1再将pos看成k位二进制数。输出时从高位到低位扫描每一位二进制0对应数字4二进制1对应数字7。举例求第3个幸运数字。F(2)6 3所以k2F(1)2pos 3 - 2 1pos-1 0两位二进制是00映射0-40-4得到44。正确。再看第4个pos 4 - 2 2pos-1 1两位二进制是01映射后是47。正确。3.3 完整代码与手算验证核心代码如下#include bits/stdc.h using namespace std; const long long LIMIT 1e18; string kthLuckyNumber(long long n) { // 1. 确定长度k int k 1; long long total 2; // F(1) 2 while (total n) { k; total (1LL k); } // 2. 计算长度k块内的1-based偏移 long long smaller (1LL k) - 2; // F(k-1) 2^k - 2 long long pos n - smaller; // [1, 2^k] // 3. 偏移转k位二进制并映射成4/7 pos--; string ans(k, 4); for (int i 0; i k; i) { // 从高位向低位检查 pos 的第 k-1-i 位 if (pos (1LL (k - 1 - i))) ans[i] 7; else ans[i] 4; } return ans; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); long long n; cin n; cout kthLuckyNumber(n) \n; return 0; }来手算几个关键值验证n计算过程输出1k1, smaller0, pos1, pos-10, 二进制042k1, smaller0, pos2, pos-11, 二进制173k2, smaller2, pos1, pos-10, 二进制004410k3, smaller6, pos4, pos-13, 二进制01147714k3, smaller6, pos8, pos-17, 二进制11177715k4, smaller14, pos1, pos-10, 二进制00004444n10对应的序列第10项确实是477n15也正好是长度4块的第一项4444。这说明整套推导是自洽的。这里还要注意一个细节代码里(1LL k)当k比较大有溢出风险。如果n的限制是1e18最大长度k不会超过601LL 60大概在1.15e18还在long long范围内。但如果你把n调大到接近9e18就需要格外小心这一点我在第5节专门讲。4. 从第n个到区间计数前缀和与二分扩展4.1 countUpTo按长度累加再对长度块二分如果题目改成“统计[l, r]范围内有多少个幸运数字”常见的做法是拆成前缀计数差cnt(r) - cnt(l-1)。问题就变成了写一个函数countUpTo(x)统计不超过x的幸运数字数量。有了上一节的基础countUpTo可以这样做如果x 4直接返回0取x的十进制位数k先累加上所有长度小于k的幸运数字总数即2^k - 2再统计长度等于k、且字符串不大于x的幸运数字有多少个。第4步是唯一需要动脑的地方。因为长度为k的幸运数字按字典序排等价于按二进制数0到2^k-1排所以我们可以二分这个块的内部序号。一个朴素的check是用kthLuckyNumber的思想生成块内第mid个长度为k的幸运数字把它和x做字符串比较。不过更稳的写法是直接在长度为k的二进制序号范围[0, 2^k-1]里二分最大的序号mid使得gen(mid) x其中gen的实现和之前pos-1映射完全一样。二分结束后长度等于k且不超过x的个数就是mid1。string genBySeq(int k, long long id) { // id 是长度为k的块内从0开始的序号 string s(k, 4); for (int i 0; i k; i) { if (id (1LL (k - 1 - i))) s[i] 7; } return s; } long long countUpTo(string x) { if (x.size() 1 x[0] 4) return 0; int k (int)x.size(); long long res (1LL k) - 2; // 长度小于k的所有幸运数字总数 long long lo 0, hi (1LL k) - 1; long long best -1; while (lo hi) { long long mid (lo hi) / 2; string cur genBySeq(k, mid); if (cur x) { best mid; lo mid 1; } else { hi mid - 1; } } if (best 0) res best 1; return res; }这个实现的复杂度是O(k^2)k是x的十进制位数最大也就是60左右完全能应付1e18量级的输入。要注意cur x用的是字符串字典序比较因为两者长度相同字符串比较正好等价于数字大小比较。如果你不想二分也可以从高位到低位做贪心统计但二分的简洁性和正确性更好验证。每次二分都要生成一次字符串最多二分60轮实际跑起来非常快。4.2 区间第k个幸运数字与常见变形有了countUpTo很多变体题目都能顺手解决。比如“问[l, r]区间内第k个幸运数字是谁”先算a countUpTo(l-1)、b countUpTo(r)如果k b - a说明区间内没那么多幸运数字输出-1否则第k个就是全局序列中的第a k个直接调用kthLuckyNumber(a k)返回字符串。再比如“输出所有长度不超过n的幸运数字”直接用DFS构造就行和上一节的打表方案一样。还有一类更隐蔽的变形不限定4和7而是给定两个字符d1、d2让你求第n个只由这两个字符组成的数字。本质完全一样只需要把映射那里的4换成d1、7换成d2。理解了“长度分块二进制映射”这种题就是换两个字符的事。5. 现场写题最容易踩的坑边界、溢出与测试用例设计5.1 偏移量减一和长度循环边界两个最常见的翻车点我见过很多提交WA的代码问题几乎都出在两处。第一处是忘记pos-1。块内第1个幸运数字对应二进制0但如果你直接用pos去映射就会导致第1个被映射成二进制1也就是“47”而不是“44”。这种错误非常隐蔽因为小数据你用手算几个容易蒙混过去一旦n稍微大一点就全乱了。第二处是长度循环的边界条件。如果写成while (total n) { ... }当n恰好等于某个F(k)时比如n2total初值是2total n成立会多走一次循环把k变成2最后输出的答案从7变成44。一定要记住长度循环是找到满足F(k) n的最小k当F(k)恰好等于n时就应该停下来。5.2 溢出边界与输入优化再说溢出。1LL k在k62时大约是4.6e18k63时就超了long long的范围属于未定义行为。如果题目给到1e18最大长度k在60以内没有风险但如果题目把上限改到1e18以上或者你的total累加逻辑不够小心就可能在while循环里把1LL k算到溢出。比较保守的做法是提前估算答案的最大长度然后给循环加一个上限判断或者在累加total之前检查1LL k是否超过某个安全值。还有更省心的方案如果你用的是C且确定数据范围很大可以直接用__int128来存中间值但要注意__int128不能直接cin读入得先转成字符串或者long long再赋值。输入优化也是个细节。这类题如果有多组询问记得在main开头加这两行ios::sync_with_stdio(false); cin.tie(nullptr);不然cin处理大量数据时可能平白无故多出不少耗时在在线评测环境里有时候就是TLE和AC的区别。5.3 自测用例表怎么验证自己的写法没毛病最后分享一个我写这类题必做的自测用例表。因为幸运数字序列前几个很容易手推拿这些用例去验证kthLuckyNumber特别高效输入n期望输出说明14第一项27最后一项长度为1344长度2块的第一项677长度2块的最后一项7444长度3块的第一项10477长度3块的第四项14777长度3块最后一项154444长度4块第一项307777F(4)30整块结束3144444长度5块第一项这个表还有一个用途就是验证区间计数的正确性。比如countUpTo(77)应该返回6countUpTo(777)应该返回14而countUpTo(444)应该返回7。跑这个用例时如果答案对不上说明你的countUpTo在边界处理上还有问题。我个人写这类题还有一个习惯写完核心函数后随机生成一些n用DFS打表的答案和kthLuckyNumber的结果互相验证。两者如果在小范围内都一致这个解法基本就稳了。这个交叉验证法对“第K个序列元素”这类题目特别适用因为暴力和打表在数据量小时本身就是完美的参照物。说到底P3499这道题难的地方不在于算法有多高深而在于你能不能从“一堆字符串”里看出“二进制编号”这个结构。一旦你看透了长度分块和4/7映射这两件事后面的代码就是顺水推舟的事。以后遇到任何由少量固定字符组成的“XX数字”题目第一步都该想进制映射而不是硬生生去枚举。

相关新闻

Python循环全攻略:for/while、迭代器与避坑技巧

Python循环全攻略:for/while、迭代器与避坑技巧

Python里的循环,说多不多,说少不少,但每次带新人或者自己回看老代码,总能发现一些值得唠叨的细节。这份整理我是按“极简但不简单”的方式做的,目的不是把文档抄一遍,而是把 for 和 while 两条主线、break/…

2026/10/12 9:32:24 阅读更多 →
企业级大模型私有化部署实战:基于vLLM的推理引擎调优指南

企业级大模型私有化部署实战:基于vLLM的推理引擎调优指南

做企业级大模型私有化部署这件事,老实说最让我头疼的不是模型算法,而是推理引擎的选型和调优。第一次尝试时我图省事,直接用通用的模型加载库起服务,结果并发一起来,显存被动态KV缓存塞得乱七八糟,接口延迟…

2026/10/12 9:32:27 阅读更多 →
无热影响区,微米级精切 | 不同种类金属薄材紫外皮秒激光切割实录

无热影响区,微米级精切 | 不同种类金属薄材紫外皮秒激光切割实录

△ 金属材料展示图铜箔、镀金铜、铝合金等金属材料,凭借优异的导电、导热及机械性能,在半导体、消费电子等众多领域扮演着关键角色。然而,随着器件向轻薄化、集成化方向加速演进,传统金属加工工艺在精度、效率及质量等方面逐渐显露…

2026/10/12 9:31:27 阅读更多 →

最新新闻

大一新生年入百万?拆解流量造神背后的真实玩法与路径

大一新生年入百万?拆解流量造神背后的真实玩法与路径

大一新生,年入100万,别人都叫他“天才小火炬”。这种标题这两年刷到过不止一次,第一反应基本都是“又一个流量造神”。但把这些案例里的账号内容、产品路径、粉丝互动记录翻了很多遍之后,有一个结论我越来越确定:这里面…

2026/10/12 13:02:11 阅读更多 →
召合消除算法求解:从状态编码到A*搜索的工程实践

召合消除算法求解:从状态编码到A*搜索的工程实践

1. 从“消消乐”到算法题:召合消除问题的本质拆解第一次看到“召合消除”这个词,很多人会愣一下——这到底是个什么玩意儿?其实说白了,它就是一类在网格棋盘上通过交换相邻元素、凑齐三个或以上相同元素来触发消除的益智玩法的统称…

2026/10/12 13:02:11 阅读更多 →
Spring Boot接入MCP:让Java服务无缝对接AI应用的实战指南

Spring Boot接入MCP:让Java服务无缝对接AI应用的实战指南

1. 为什么每个Spring Boot开发者都该认真看看MCP 1.1 我遇到的服务接入难题 这两年我陆续接到好几个"把内部服务开放给AI使用"的需求,每次都被折腾得够呛。服务本身不难写,难的是对方AI应用的接入方式千奇百怪:有的要求按插件规范…

2026/10/12 13:02:11 阅读更多 →
C++从零实现分数类:精确四则运算与边界处理

C++从零实现分数类:精确四则运算与边界处理

1. 从整数除法的一个坑说起:为什么需要自己实现分数类很多刚接触C的朋友都写过类似这样的代码:计算1/2 1/3,然后发现结果输出是0。原因很简单,1/2在C里执行的是整数除法,结果是0而不是0.5。就算换成double&#xff0c…

2026/10/12 13:02:11 阅读更多 →
图片翻译六步流程:从OCR识别到译文回填的完整实战指南

图片翻译六步流程:从OCR识别到译文回填的完整实战指南

图片里夹着文字这件事,放在几年前还不太折腾人,大不了照着键盘一个字母一个字母敲。可现在谁手机里没有几十张截图、合同照片、PPT翻拍?尤其做外贸、读论文、追外漫、玩外服游戏的时候,满屏蝌蚪文真是让人头大。你去搜“图片翻译”…

2026/10/12 13:02:10 阅读更多 →
6.4k Stars!用Claude Code写论文的全套流水线,有人打包开源了:TaoToken统一Key接入实操

6.4k Stars!用Claude Code写论文的全套流水线,有人打包开源了:TaoToken统一Key接入实操

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 13:01:10 阅读更多 →

日新闻

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

在数码相机、高清显示屏与现代矢量图形技术高度发达的今天,画面可以做到绝对的锐利、平滑与无瑕。然而,当一张秋日手账插画或拍立得照片过于“平整无瑕”时,往往会散发出一种冰冷生硬的“数码塑料感(Digital Plasticity&#xff0…

2026/10/12 0:00:59 阅读更多 →
活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

在现代网页与移动端设计中,横排(Horizontal Layout)早已经成为了绝对的主流。然而,当我们翻开泛黄的线装古籍、宋版木刻诗集,或是欣赏一张茶道雅集的手写便签时,那种**自上而下纵向书写、自右向左逐列铺展&…

2026/10/12 0:00:59 阅读更多 →
周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

每到周日的晚上八点到十点,很多人心里都会悄悄亮起一盏警示灯。 在心理学上,这种现象有一个专门的称谓——“周日夜晚焦虑症(Sunday Scaries)”。明天又是周一,闹钟又要重新在七点响彻卧房;脑海里仿佛有一个…

2026/10/12 0:00:59 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/12 0:16:30 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/12 0:16:43 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 10:45:37 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:53 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:54 阅读更多 →