[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"consumer-news-detail-1023":3,"consumer-news-interaction-1023":40,"consumer-news-related-1023":43},{"detail":4,"item":35},{"card":5,"schemaVersion":22,"fields":23,"content":29},{"id":6,"kind":7,"targetType":8,"targetId":9,"subtype":7,"typeLabel":10,"title":11,"subtitle":12,"summary":13,"coverUrl":14,"badgeText":14,"href":15,"sourceName":12,"meta":16,"metrics":19,"tags":20,"resolved":21},"NEWS_ARTICLE:1023","news","NEWS_ARTICLE",1023,"资讯","数位 DP 进阶","博客园","数位 DP 进阶 日期：2026-07-14 关键词：动态规划、数位 DP、记忆化搜索 涉及题目：P2602 [ZJOI2010] 数字计数 | P4124 [CQOI2016] 手机号码 | P3286 [SCOI2014] 方伯伯的商场之旅 数位 DP 梳理一下数位 DP 的核心思想。 数位 D","","\u002Fnews\u002F1023",[17,18],"2026","综合技术",{},[18],true,"consumer-content-detail-v1",{"sourceName":12,"authorName":24,"categoryName":18,"summary":13,"description":13,"publishTime":25,"updateTime":26,"sourceUrl":27,"language":28},"lvwangshu","2026-09-06T16:47","2026-09-11T15:22:03","https:\u002F\u002Fwww.cnblogs.com\u002FlvwangshuOI\u002Fp\u002F22863645","中文",{"format":30,"policy":31,"normalized":21,"html":32,"text":33,"wordCount":34,"hasBody":21},"HTML","NEWS_CONTENT_V1","数位 DP 进阶\n\u003Cblockquote>\n \u003Cp>\u003Cstrong>日期\u003C\u002Fstrong>：2026-07-14\u003Cbr>\u003Cstrong>关键词\u003C\u002Fstrong>：动态规划、数位 DP、记忆化搜索\u003Cbr>\u003Cstrong>涉及题目\u003C\u002Fstrong>：P2602 [ZJOI2010] 数字计数 | P4124 [CQOI2016] 手机号码 | P3286 [SCOI2014] 方伯伯的商场之旅\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch2>数位 DP\u003C\u002Fh2>\n\u003Cp>梳理一下\u003Cstrong>数位 DP\u003C\u002Fstrong> 的核心思想。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>数位 DP\u003C\u002Fstrong>，顾名思义，是对数字的“位”进行动态规划。它专门用于解决一类问题：\u003C\u002Fp>\n\u003Cblockquote>\n \u003Cp>求区间 \u003Cspan>\\([L, R]\\)\u003C\u002Fspan> 内满足某种特定性质的数的数量（或最值、总和等）。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>当 \u003Cspan>\\(L\\)\u003C\u002Fspan> 和 \u003Cspan>\\(R\\)\u003C\u002Fspan> 的范围达到 \u003Cspan>\\(10^{18}\\)\u003C\u002Fspan>（即 \u003Cspan>\\(10^{18}\\)\u003C\u002Fspan> 级别）时，传统的线性扫描完全失效，数位 DP 便成为了唯一的解法。\u003C\u002Fp>\n\u003Ch3>核心思想：逐位确定与记忆化\u003C\u002Fh3>\n\u003Cp>数位 DP 的本质是将数字按位拆分，通过 DFS（深度优先搜索）枚举每一位的可能取值，并利用记忆化来避免重复计算。\u003C\u002Fp>\n\u003Cp>主流写法通常采用 \u003Cstrong>DFS + 记忆化\u003C\u002Fstrong> 的形式，相比递推，它逻辑更清晰，边界处理更自然。\u003C\u002Fp>\n\u003Cp>一个标准的 DFS 函数通常包含以下核心参数：\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Ccode>pos\u003C\u002Fcode>：当前处理到第几位（通常从高位向低位递归）。\u003C\u002Fli>\n \u003Cli>\u003Ccode>state\u003C\u002Fcode>：当前的状态（这是解题的核心，例如前一位数字是什么、是否出现了某个特定数字等）。\u003C\u002Fli>\n \u003Cli>\u003Ccode>limit\u003C\u002Fcode>：是否受到上界的限制（这是数位 DP 最重要的标志之一）。\u003C\u002Fli>\n \u003Cli>\u003Ccode>lead\u003C\u002Fcode>：是否存在前导零。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch4>深入理解 \u003Ccode>limit\u003C\u002Fcode>（限制位）\u003C\u002Fh4>\n\u003Cul>\n \u003Cli>当我们构造数字时，如果之前填的每一位都和 \u003Cspan>\\(R\\)\u003C\u002Fspan> 的对应位一模一样，那么当前位就不能随意填写 0~9，最大值只能填 \u003Cspan>\\(R\\)\u003C\u002Fspan> 在这一位上的值。此时 \u003Ccode>limit = true\u003C\u002Fcode>。\u003C\u002Fli>\n \u003Cli>反之，如果之前某一位已经填得比 \u003Cspan>\\(R\\)\u003C\u002Fspan> 小了，那么后面的位就可以自由填写 0~9，\u003Ccode>limit = false\u003C\u002Fcode>。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>关键结论\u003C\u002Fstrong>：只有 \u003Ccode>limit = false\u003C\u002Fcode> 的状态才具有通用性，才能进行记忆化存储；\u003Ccode>limit = true\u003C\u002Fcode> 的状态是唯一的、临时的，不能存入 DP 数组。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch4>深入理解 \u003Ccode>lead\u003C\u002Fcode>（前导零）\u003C\u002Fh4>\n\u003Cp>前导零指的是数字开头的 0。例如 00123 实际是 123。\u003C\u002Fp>\n\u003Cul>\n \u003Cli>在统计数字 0 的出现次数时，前导零不能算作有效的 0。\u003C\u002Fli>\n \u003Cli>在判断数字性质（如单调性）时，前导零通常不影响判断逻辑。\u003C\u002Fli>\n \u003Cli>我们通常使用 \u003Ccode>lead\u003C\u002Fcode> 标记来区分当前是否处于前缀的零中。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Chr>\n\u003Ch2>一、 基础：P2602 [ZJOI2010] 数字计数\u003C\u002Fh2>\n\u003Ch3>题目大意\u003C\u002Fh3>\n\u003Cp>求区间 \u003Cspan>\\([a, b]\\)\u003C\u002Fspan> 中 0~9 每个数字分别出现的总次数。\u003C\u002Fp>\n\u003Ch3>解题思路\u003C\u002Fh3>\n\u003Cp>这道题是数位 DP 的经典入门题。它不直接询问满足条件的数的个数，而是询问\u003Cstrong>数字出现的总频次\u003C\u002Fstrong>。这要求我们不仅要统计“有多少个”，还要统计“贡献了多少次”。利用 \u003Cstrong>差分\u003C\u002Fstrong> 思想 ： \u003Ccode>ans = count(b, d) - count(a - 1, d)\u003C\u002Fcode>\u003C\u002Fp>\n\u003Cp>\u003Cstrong>状态设计\u003C\u002Fstrong>：\u003Cbr>\n  我们需要同时维护两个维度的信息：\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>个数 (\u003Cspan>\\(cnt\\)\u003C\u002Fspan>)\u003C\u002Fstrong>：从当前位置开始，往后能构造出多少个合法的数。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>和 (\u003Cspan>\\(sum\\)\u003C\u002Fspan>)\u003C\u002Fstrong>：从当前位置开始，往后目标数字一共出现了多少次。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>因此，DFS 函数的返回值设计为 \u003Ccode>pair&lt;ll, ll&gt;\u003C\u002Fcode>。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>状态转移\u003C\u002Fstrong>：\u003Cbr>\n  假设当前位于第 \u003Ccode>pos\u003C\u002Fcode> 位，准备填入数字 \u003Ccode>i\u003C\u002Fcode>。递归调用下一位后，获得返回值 \u003Ccode>{cnt, sum}\u003C\u002Fcode>。\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Ccode>tot_cnt += cnt\u003C\u002Fcode>：累加低位返回的合法数字个数。\u003C\u002Fli>\n \u003Cli>\u003Ccode>tot_sum += sum\u003C\u002Fcode>：累加低位返回的目标数字出现次数。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>核心逻辑\u003C\u002Fstrong>：如果当前填入的数字 \u003Ccode>i\u003C\u002Fcode> 正好是我们统计的目标数字 \u003Ccode>target\u003C\u002Fcode>，那么低位的每一个合法数（\u003Ccode>cnt\u003C\u002Fcode> 个），其最高位都是 \u003Ccode>target\u003C\u002Fcode>。因此，我们需要额外加上 \u003Ccode>cnt\u003C\u002Fcode>。\n  \u003Cdiv>\n   \\[tot\\_sum \\leftarrow tot\\_sum + sum + (i == target ? cnt : 0) \\]\n  \u003C\u002Fdiv>\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>\u003Cstrong>代码实现\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cpre>\u003Ccode>#include &lt;bits\u002Fstdc++.h&gt;\nusing namespace std;\n\nusing ll = long long;\nusing pll = pair&lt;ll, ll&gt;;\n\nconst int N = 20;\n\nll a,b;\n\nll dp_cnt[N][2][2];\nll dp_sum[N][2][2];\n\nvector&lt;int&gt; dig;\n\nint target;\n\npll dfs(int pos, bool lead, bool limit) {\n\tif (pos == dig.size()) return { 1, 0 };\n\tif (dp_cnt[pos][lead][limit] != -1)\n\t\treturn { dp_cnt[pos][lead][limit], dp_sum[pos][lead][limit] };\n\t\n\tint up = 0;\n\tif (limit) up = dig[pos];\n\telse up = 9;\n\t\n\tll tot_cnt = 0, tot_sum = 0;\n\tfor (int i = 0; i &lt;= up; i ++) {\n\t\tbool n_lead, n_limit;\n\t\tif (lead == true &amp;&amp; i == 0) n_lead = true;\n\t\telse n_lead = false;\n\t\t\n\t\tif (limit ==true &amp;&amp; i == up) n_limit = true;\n\t\telse n_limit = false;\n\t\t\n\t\tauto [cnt, sum] = dfs(pos + 1, n_lead, n_limit);\n\t\ttot_cnt += cnt;\n\t\ttot_sum += sum;\n\t\t\n\t\tif (n_lead == false &amp;&amp; i == target) tot_sum += cnt;\n\t}\n\t\n\tdp_cnt[pos][lead][limit] = tot_cnt;\n\tdp_sum[pos][lead][limit] = tot_sum;\n\t\n\treturn { tot_cnt, tot_sum };\n\t\n}\n\n\nll count(ll x, int d) {\n\tif (x &lt; 1) return 0;\n\t\n\tdig.clear();\n\twhile (x &gt; 0) {\n\t\tdig.push_back(x % 10);\n\t\tx \u002F= 10;\n\t}\n\treverse(dig.begin(), dig.end());\n\ttarget = d;\n\t\n\tmemset(dp_cnt, -1, sizeof(dp_cnt));\n\tmemset(dp_sum, -1, sizeof(dp_sum));\n\t\n\tauto [cnt, sum] = dfs(0, true, true);\n\t\n\treturn sum;\n}\n\n\n\nint main () {\n\tios::sync_with_stdio(false);\n\tcin.tie(0);\n\t\n\tcin &gt;&gt; a &gt;&gt; b;\n\tfor (int d = 0; d &lt;= 9; d ++) {\n\t\tcout &lt;&lt; count(b, d) - count(a - 1, d) &lt;&lt; ' ';\n\t}\n\t\n\treturn 0;\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Chr>\n\u003Ch2>二、 进阶：P4124 [CQOI2016] 手机号码\u003C\u002Fh2>\n\u003Ch3>题目大意\u003C\u002Fh3>\n\u003Cp>找出区间 \u003Cspan>\\([L, R]\\)\u003C\u002Fspan> 内满足条件的 11 位手机号码：\u003C\u002Fp>\n\u003Col>\n \u003Cli>必须包含连续的三个相同数字。\u003C\u002Fli>\n \u003Cli>不能同时包含数字 4 和数字 8。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch3>解题思路\u003C\u002Fh3>\n\u003Cp>这道题引入了\u003Cstrong>复杂的状态约束\u003C\u002Fstrong>。我们需要在 DFS 的过程中，实时记录当前构造的数字所具有的特征，以便进行合法性判断。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>状态设计\u003C\u002Fstrong>：\u003Cbr>\n  为了涵盖所有约束，我们需要扩充 DFS 的参数列表：\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Ccode>pos\u003C\u002Fcode>: 当前位。\u003C\u002Fli>\n \u003Cli>\u003Ccode>pre1\u003C\u002Fcode>: 上一位数字（用于判断连续）。\u003C\u002Fli>\n \u003Cli>\u003Ccode>len\u003C\u002Fcode>: 当前连续的长度。\u003C\u002Fli>\n \u003Cli>\u003Ccode>has3\u003C\u002Fcode>: 是否已经出现连续三位相同的数字。\u003C\u002Fli>\n \u003Cli>\u003Ccode>has4\u003C\u002Fcode>, \u003Ccode>has8\u003C\u002Fcode>: 是否出现过 4 或 8。\u003C\u002Fli>\n \u003Cli>\u003Ccode>limit\u003C\u002Fcode>, \u003Ccode>lead\u003C\u002Fcode>: 常规参数。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>\u003Cstrong>剪枝优化\u003C\u002Fstrong>：\u003Cbr>\n  这是提升效率的关键。如果在递归过程中发现 \u003Ccode>has4\u003C\u002Fcode> 和 \u003Ccode>has8\u003C\u002Fcode> 同时为 \u003Ccode>true\u003C\u002Fcode>，意味着这个数字已经违反了规则，可以直接返回 0，终止该分支的搜索。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>前导零处理\u003C\u002Fstrong>：\u003Cbr>\n  特别需要注意的是，前导零阶段不应参与连续性和数字出现的判断。只有当 \u003Ccode>lead\u003C\u002Fcode> 变为 \u003Ccode>false\u003C\u002Fcode> 时，才开始正式计数。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>代码逻辑\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cpre>\u003Ccode>#include &lt;bits\u002Fstdc++.h&gt;\nusing namespace std;\n\nusing ll = long long;\n\nconst int N = 13;\n\nll L, R;\nll dp[N][11][10][10][2][2];\n\nvector&lt;int&gt; dig;\n\nll dfs(ll pos, ll pre1, bool len, bool has3, bool has4, bool has8, bool limit, bool lead) {\n\tif (pos == dig.size()) {\n\t\tif (has3 &amp;&amp; !(has4 &amp;&amp; has8)) return 1;\n\t\telse return 0;\n\t}\n\t\t\n\tif (!limit &amp;&amp; !lead &amp;&amp; dp[pos][pre1][len][has3][has4][has8] != -1)\n\t\treturn dp[pos][pre1][len][has3][has4][has8];\n\n\tint up = 0;\n\tif (limit) up = dig[pos];\n\telse up = 9;\n\n\tll res = 0;\n\tfor (int i = 0; i &lt;= up; i ++) {\n\t\tbool n_lead = lead &amp;&amp; (i == 0);\n\t\tbool n_limit = limit &amp;&amp; (i == up);\n\n\t\tint n_pre = i, n_len = len;\n\t\tbool n_has3 = has3;\n\t\tbool n_has8 = has8 || (i == 8);\n\t\tbool n_has4 = has4 || (i == 4);\n\n\t\tif (!n_lead) {\n\t\t\tif (i == pre1) {\n\t\t\t\tn_len = len + 1;\n\t\t\t\tif (n_len &gt;= 3) n_has3 = true;\n\t\t\t}\n\t\t\telse n_len = 1;\n\t\t}\n\t\telse {\n\t\t\tn_pre = 10;\n\t\t\tn_len = 0;\n\t\t}\n\n\t\tres += dfs(pos + 1, n_pre, n_len, n_has3, n_has4, n_has8, n_limit, n_lead);\n\t}\n\t\n\tif (!limit &amp;&amp; !lead) dp[pos][pre1][len][has3][has4][has8] = res;\n\t\n\treturn res;\n}\n\nll get(ll x) {\n\tdig.clear();\n\twhile (x) {\n\t\tdig.push_back(x % 10);\n\t\tx \u002F= 10;\n\t}\n\treverse(dig.begin(), dig.end());\n\n\tmemset(dp, -1, sizeof(dp));\n\n\tll ans = dfs(0, 10, 0, 0, 0, 0, 1, 1);\n\n\treturn ans;\n}\n\n\nint main () {\n\tios::sync_with_stdio(false);\n\tcin.tie(0);\n\n\tcin &gt;&gt; L &gt;&gt; R;\n\n\tcout &lt;&lt; get(R) - get(L - 1) &lt;&lt; '\\n';\n\n\treturn 0;\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>小结\u003C\u002Fh3>\n\u003Cp>这道题展示了如何处理\u003Cstrong>多维度的 bool 约束\u003C\u002Fstrong>。核心技巧在于：\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>状态参数化\u003C\u002Fstrong>：将所有约束条件转化为 DFS 的参数。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>及时剪枝\u003C\u002Fstrong>：一旦发现非法状态，立即停止搜索。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>严谨的前导零处理\u003C\u002Fstrong>：确保前导零不影响数字属性的判断。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Chr>\n\u003Ch2>三、 提高：P3286 [SCOI2014] 方伯伯的商场之旅\u003C\u002Fh2>\n\u003Ch3>题目大意\u003C\u002Fh3>\n\u003Cp>将区间 \u003Cspan>\\([l, r]\\)\u003C\u002Fspan> 内的所有数转换为 \u003Cspan>\\(k\\)\u003C\u002Fspan> 进制，然后计算将所有数位上的石子移动到同一个位置的代价（代价定义为权重乘以距离），求最小总代价。\u003C\u002Fp>\n\u003Ch3>解题思路\u003C\u002Fh3>\n\u003Cp>这是一道\u003Cstrong>带权值的最优化问题\u003C\u002Fstrong>，也是数位 DP 的难点。暴力枚举每个数的最优集合点是不可行的。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>核心思想：贡献法与二次 DP\u003C\u002Fstrong>\u003Cbr>\n  我们不关心最终的最优位置具体在哪，而是关心\u003Cstrong>移动集合点带来的代价变化\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>基准状态\u003C\u002Fstrong>：假设所有数字都先移动到第 1 个位置，计算出总代价 \u003Cspan>\\(S_1\\)\u003C\u002Fspan>。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>增量调整\u003C\u002Fstrong>：考虑将集合点从位置 \u003Cspan>\\(p\\)\u003C\u002Fspan> 移动到 \u003Cspan>\\(p+1\\)\u003C\u002Fspan>。 \n  \u003Cul>\n   \u003Cli>对于位置 \u003Cspan>\\(&lt; p+1\\)\u003C\u002Fspan> 的数字，距离增加了 1，代价增加 \u003Cspan>\\(w\\)\u003C\u002Fspan>。\u003C\u002Fli>\n   \u003Cli>对于位置 \u003Cspan>\\(\\ge p+1\\)\u003C\u002Fspan> 的数字，距离减少了 1，代价减少 \u003Cspan>\\(w\\)\u003C\u002Fspan>。\u003C\u002Fli>\n  \u003C\u002Ful>\u003C\u002Fli>\n \u003Cli>\u003Cstrong>决策\u003C\u002Fstrong>：如果移动后代价变小（\u003Cspan>\\(\\Delta &lt; 0\\)\u003C\u002Fspan>），则说明移动是有益的。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>\u003Cstrong>代码实现\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cpre>\u003Ccode>#include &lt;bits\u002Fstdc++.h&gt;\nusing namespace std;\n\nusing ll = long long;\n\nconst int N = 1e4 + 5, M = 105;\n\nll l, r, k;\n\nll dig[M];\nll dp[M][N];\n\nll dfs(ll now, ll sum, ll p, bool limit) {\n\tif (!now) return max(sum, ll(0));\n\tif (!limit &amp;&amp; dp[now][sum] != -1) return dp[now][sum];\n\n\tll ans = 0;\n\tint up = 0;\n\t\n\tif (limit) up = dig[now];\n\telse up = k - 1;\n\n\tfor (int i = 0; i &lt;= up; i ++) {\n\t\tint delta = 0;\n\t\tif (p == 1) delta = i * (now - 1);\n\t\telse {\n\t\t\tif (now &lt; p) delta = -i;\n\t\t\telse delta = i;\n\t\t}\n\t\t\n\t\tans += dfs(now - 1, sum + delta, p, limit &amp;&amp; (i == up));\n\t}\n\n\tif (!limit) dp[now][sum] = ans;\n\treturn ans;\n}\n\n\nll get(ll x) {\n\tint t = 0;\n\twhile (x) {\n\t\tdig[++ t] = x % k;\n\t\tx \u002F= k;\n\t}\n\n\tll ans = 0;\n\tfor (int i = 1; i &lt;= t; i ++) {\n\t\tmemset(dp, -1, sizeof(dp));\n\t\tif (i == 1) ans += dfs(t, 0, i, 1);\n\t\telse ans -= dfs(t, 0, i, 1);\n\t}\n\n\treturn ans;\n}\n\n\nint main () {\n\tios::sync_with_stdio(false);\n\tcin.tie(0);\n\n\tcin &gt;&gt; l &gt;&gt; r &gt;&gt; k;\n\tcout &lt;&lt; get(r) - get(l - 1) &lt;&lt; '\\n';\n\n\n\treturn 0;\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>小结\u003C\u002Fh3>\n\u003Cp>这道题代表了数位 DP 的高级应用：\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>化整为零\u003C\u002Fstrong>：不求解绝对最优解，而是求解相对变化量。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>二次 DP\u003C\u002Fstrong>：通过多次 DP 来模拟过程的推进。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>贡献思维\u003C\u002Fstrong>：关注每一步决策对全局代价的边际影响。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Chr>\n\u003Cp>\u003Cstrong>总结\u003C\u002Fstrong>\u003Cbr>\n  这三道题完美诠释了数位 DP 的进化路线：\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>P2602\u003C\u002Fstrong> 教会我们如何统计\u003Cstrong>贡献\u003C\u002Fstrong>。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>P4124\u003C\u002Fstrong> 教会我们如何管理\u003Cstrong>复杂约束\u003C\u002Fstrong>。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>P3286\u003C\u002Fstrong> 教会我们如何解决\u003Cstrong>带权最优化\u003C\u002Fstrong>问题。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>掌握这三类模板，基本能应对绝大多数数位 DP 的题目。\u003C\u002Fp>\n\u003Chr>\n\u003Cblockquote>\n \u003Cp>注：部分由 AI 润色\u003C\u002Fp>\n\u003C\u002Fblockquote>","数位 DP 进阶 日期：2026-07-14 关键词：动态规划、数位 DP、记忆化搜索 涉及题目：P2602 [ZJOI2010] 数字计数 | P4124 [CQOI2016] 手机号码 | P3286 [SCOI2014] 方伯伯的商场之旅 数位 DP 梳理一下数位 DP 的核心思想。 数位 DP，顾名思义，是对数字的“位”进行动态规划。它专门用于解决一类问题： 求区间 \\([L, R]\\) 内满足某种特定性质的数的数量（或最值、总和等）。 当 \\(L\\) 和 \\(R\\) 的范围达到 \\(10^{18}\\)（即 \\(10^{18}\\) 级别）时，传统的线性扫描完全失效，数位 DP 便成为了唯一的解法。 核心思想：逐位确定与记忆化 数位 DP 的本质是将数字按位拆分，通过 DFS（深度优先搜索）枚举每一位的可能取值，并利用记忆化来避免重复计算。 主流写法通常采用 DFS + 记忆化 的形式，相比递推，它逻辑更清晰，边界处理更自然。 一个标准的 DFS 函数通常包含以下核心参数： pos：当前处理到第几位（通常从高位向低位递归）。 state：当前的状态（这是解题的核心，例如前一位数字是什么、是否出现了某个特定数字等）。 limit：是否受到上界的限制（这是数位 DP 最重要的标志之一）。 lead：是否存在前导零。 深入理解 limit（限制位） 当我们构造数字时，如果之前填的每一位都和 \\(R\\) 的对应位一模一样，那么当前位就不能随意填写 0~9，最大值只能填 \\(R\\) 在这一位上的值。此时 limit = true。 反之，如果之前某一位已经填得比 \\(R\\) 小了，那么后面的位就可以自由填写 0~9，limit = false。 关键结论：只有 limit = false 的状态才具有通用性，才能进行记忆化存储；limit = true 的状态是唯一的、临时的，不能存入 DP 数组。 深入理解 lead（前导零） 前导零指的是数字开头的 0。例如 00123 实际是 123。 在统计数字 0 的出现次数时，前导零不能算作有效的 0。 在判断数字性质（如单调性）时，前导零通常不影响判断逻辑。 我们通常使用 lead 标记来区分当前是否处于前缀的零中。 一、 基础：P2602 [ZJOI2010] 数字计数 题目大意 求区间 \\([a, b]\\) 中 0~9 每个数字分别出现的总次数。 解题思路 这道题是数位 DP 的经典入门题。它不直接询问满足条件的数的个数，而是询问数字出现的总频次。这要求我们不仅要统计“有多少个”，还要统计“贡献了多少次”。利用 差分 思想 ： ans = count(b, d) - count(a - 1, d) 状态设计： 我们需要同时维护两个维度的信息： 个数 (\\(cnt\\))：从当前位置开始，往后能构造出多少个合法的数。 和 (\\(sum\\))：从当前位置开始，往后目标数字一共出现了多少次。 因此，DFS 函数的返回值设计为 pair\u003Cll, ll>。 状态转移： 假设当前位于第 pos 位，准备填入数字 i。递归调用下一位后，获得返回值 {cnt, sum}。 tot_cnt += cnt：累加低位返回的合法数字个数。 tot_sum += sum：累加低位返回的目标数字出现次数。 核心逻辑：如果当前填入的数字 i 正好是我们统计的目标数字 target，那么低位的每一个合法数（cnt 个），其最高位都是 target。因此，我们需要额外加上 cnt。 \\[tot\\_sum \\leftarrow tot\\_sum + sum + (i == target ? cnt : 0) \\] 代码实现 #include \u003Cbits\u002Fstdc++.h> using namespace std; using ll = long long; using pll = pair\u003Cll, ll>; const int N = 20; ll a,b; ll dp_cnt[N][2][2]; ll dp_sum[N][2][2]; vector\u003Cint> dig; int target; pll dfs(int pos, bool lead, bool limit) { if (pos == dig.size()) return { 1, 0 }; if (dp_cnt[pos][lead][limit] != -1) return { dp_cnt[pos][lead][limit], dp_sum[pos][lead][limit] }; int up = 0; if (limit) up = dig[pos]; else up = 9; ll tot_cnt = 0, tot_sum = 0; for (int i = 0; i \u003C= up; i ++) { bool n_lead, n_limit; if (lead == true && i == 0) n_lead = true; else n_lead = false; if (limit ==true && i == up) n_limit = true; else n_limit = false; auto [cnt, sum] = dfs(pos + 1, n_lead, n_limit); tot_cnt += cnt; tot_sum += sum; if (n_lead == false && i == target) tot_sum += cnt; } dp_cnt[pos][lead][limit] = tot_cnt; dp_sum[pos][lead][limit] = tot_sum; return { tot_cnt, tot_sum }; } ll count(ll x, int d) { if (x \u003C 1) return 0; dig.clear(); while (x > 0) { dig.push_back(x % 10); x \u002F= 10; } reverse(dig.begin(), dig.end()); target = d; memset(dp_cnt, -1, sizeof(dp_cnt)); memset(dp_sum, -1, sizeof(dp_sum)); auto [cnt, sum] = dfs(0, true, true); return sum; } int main () { ios::sync_with_stdio(false); cin.tie(0); cin >> a >> b; for (int d = 0; d \u003C= 9; d ++) { cout \u003C\u003C count(b, d) - count(a - 1, d) \u003C\u003C ' '; } return 0; } 二、 进阶：P4124 [CQOI2016] 手机号码 题目大意 找出区间 \\([L, R]\\) 内满足条件的 11 位手机号码： 必须包含连续的三个相同数字。 不能同时包含数字 4 和数字 8。 解题思路 这道题引入了复杂的状态约束。我们需要在 DFS 的过程中，实时记录当前构造的数字所具有的特征，以便进行合法性判断。 状态设计： 为了涵盖所有约束，我们需要扩充 DFS 的参数列表： pos: 当前位。 pre1: 上一位数字（用于判断连续）。 len: 当前连续的长度。 has3: 是否已经出现连续三位相同的数字。 has4, has8: 是否出现过 4 或 8。 limit, lead: 常规参数。 剪枝优化： 这是提升效率的关键。如果在递归过程中发现 has4 和 has8 同时为 true，意味着这个数字已经违反了规则，可以直接返回 0，终止该分支的搜索。 前导零处理： 特别需要注意的是，前导零阶段不应参与连续性和数字出现的判断。只有当 lead 变为 false 时，才开始正式计数。 代码逻辑 #include \u003Cbits\u002Fstdc++.h> using namespace std; using ll = long long; const int N = 13; ll L, R; ll dp[N][11][10][10][2][2]; vector\u003Cint> dig; ll dfs(ll pos, ll pre1, bool len, bool has3, bool has4, bool has8, bool limit, bool lead) { if (pos == dig.size()) { if (has3 && !(has4 && has8)) return 1; else return 0; } if (!limit && !lead && dp[pos][pre1][len][has3][has4][has8] != -1) return dp[pos][pre1][len][has3][has4][has8]; int up = 0; if (limit) up = dig[pos]; else up = 9; ll res = 0; for (int i = 0; i \u003C= up; i ++) { bool n_lead = lead && (i == 0); bool n_limit = limit && (i == up); int n_pre = i, n_len = len; bool n_has3 = has3; bool n_has8 = has8 || (i == 8); bool n_has4 = has4 || (i == 4); if (!n_lead) { if (i == pre1) { n_len = len + 1; if (n_len >= 3) n_has3 = true; } else n_len = 1; } else { n_pre = 10; n_len = 0; } res += dfs(pos + 1, n_pre, n_len, n_has3, n_has4, n_has8, n_limit, n_lead); } if (!limit && !lead) dp[pos][pre1][len][has3][has4][has8] = res; return res; } ll get(ll x) { dig.clear(); while (x) { dig.push_back(x % 10); x \u002F= 10; } reverse(dig.begin(), dig.end()); memset(dp, -1, sizeof(dp)); ll ans = dfs(0, 10, 0, 0, 0, 0, 1, 1); return ans; } int main () { ios::sync_with_stdio(false); cin.tie(0); cin >> L >> R; cout \u003C\u003C get(R) - get(L - 1) \u003C\u003C '\\n'; return 0; } 小结 这道题展示了如何处理多维度的 bool 约束。核心技巧在于： 状态参数化：将所有约束条件转化为 DFS 的参数。 及时剪枝：一旦发现非法状态，立即停止搜索。 严谨的前导零处理：确保前导零不影响数字属性的判断。 三、 提高：P3286 [SCOI2014] 方伯伯的商场之旅 题目大意 将区间 \\([l, r]\\) 内的所有数转换为 \\(k\\) 进制，然后计算将所有数位上的石子移动到同一个位置的代价（代价定义为权重乘以距离），求最小总代价。 解题思路 这是一道带权值的最优化问题，也是数位 DP 的难点。暴力枚举每个数的最优集合点是不可行的。 核心思想：贡献法与二次 DP 我们不关心最终的最优位置具体在哪，而是关心移动集合点带来的代价变化。 基准状态：假设所有数字都先移动到第 1 个位置，计算出总代价 \\(S_1\\)。 增量调整：考虑将集合点从位置 \\(p\\) 移动到 \\(p+1\\)。 对于位置 \\(\u003C p+1\\) 的数字，距离增加了 1，代价增加 \\(w\\)。 对于位置 \\(\\ge p+1\\) 的数字，距离减少了 1，代价减少 \\(w\\)。 决策：如果移动后代价变小（\\(\\Delta \u003C 0\\)），则说明移动是有益的。 代码实现 #include \u003Cbits\u002Fstdc++.h> using namespace std; using ll = long long; const int N = 1e4 + 5, M = 105; ll l, r, k; ll dig[M]; ll dp[M][N]; ll dfs(ll now, ll sum, ll p, bool limit) { if (!now) return max(sum, ll(0)); if (!limit && dp[now][sum] != -1) return dp[now][sum]; ll ans = 0; int up = 0; if (limit) up = dig[now]; else up = k - 1; for (int i = 0; i \u003C= up; i ++) { int delta = 0; if (p == 1) delta = i * (now - 1); else { if (now \u003C p) delta = -i; else delta = i; } ans += dfs(now - 1, sum + delta, p, limit && (i == up)); } if (!limit) dp[now][sum] = ans; return ans; } ll get(ll x) { int t = 0; while (x) { dig[++ t] = x % k; x \u002F= k; } ll ans = 0; for (int i = 1; i \u003C= t; i ++) { memset(dp, -1, sizeof(dp)); if (i == 1) ans += dfs(t, 0, i, 1); else ans -= dfs(t, 0, i, 1); } return ans; } int main () { ios::sync_with_stdio(false); cin.tie(0); cin >> l >> r >> k; cout \u003C\u003C get(r) - get(l - 1) \u003C\u003C '\\n'; return 0; } 小结 这道题代表了数位 DP 的高级应用： 化整为零：不求解绝对最优解，而是求解相对变化量。 二次 DP：通过多次 DP 来模拟过程的推进。 贡献思维：关注每一步决策对全局代价的边际影响。 总结 这三道题完美诠释了数位 DP 的进化路线： P2602 教会我们如何统计贡献。 P4124 教会我们如何管理复杂约束。 P3286 教会我们如何解决带权最优化问题。 掌握这三类模板，基本能应对绝大多数数位 DP 的题目。 注：部分由 AI 润色",5259,{"id":6,"kind":7,"title":11,"summary":13,"image":14,"href":15,"meta":36,"badge":10,"author":12,"stats":-1,"accent":37,"coverRatio":38,"tags":39},"2026 · 综合技术","#2563eb","16 \u002F 10",[18],{"targetType":8,"targetId":9,"likedByMe":41,"likeCount":42,"commentCount":42,"contentLikeCount":42,"contentCommentCount":42,"sourceLikeCount":42,"sourceCommentCount":42},false,0,[44,51,57,64,71,78,84,91],{"id":45,"kind":7,"title":46,"summary":47,"image":48,"href":49,"meta":36,"badge":10,"author":12,"stats":-1,"accent":37,"coverRatio":38,"tags":50},"NEWS_ARTICLE:962","基于增强QUIC协议优化弱网下的直播观看体验","如今，直播已成为电商、教育、娱乐等各领域信息传递与线上消费的重要载体，而网络质量则是决定直播体验的核心命脉。然而，当用户一旦置身于地铁、户外等弱网环境中，画面卡顿、画质模糊、音画错位等问题便接踵而至，原本精彩的直播瞬间变得支离破碎，用户观看体验随之直线下降。 基于此，HarmonyOS SDK远场通","https:\u002F\u002Foscimg.oschina.net\u002F\u002FAiCreationDetail\u002Fup-d5a5690fbed726571a684c85eec1789e.png","\u002Fnews\u002F962",[18],{"id":52,"kind":7,"title":53,"summary":54,"image":14,"href":55,"meta":36,"badge":10,"author":12,"stats":-1,"accent":37,"coverRatio":38,"tags":56},"NEWS_ARTICLE:1011","上周热点回顾（8.31-9.6）","热点随笔： &#183; 被罚了500后，整个人都变老实了 (欢醉) &#183; 2016已经是十年前了 (三范式) &#183; 都是 AI 写代码，为什么 C# 比 Java 快半拍 (张善友) &#183; OpenClaw 2.0 发布：史上最大更新，8 大新功能 + 2 个破坏性变更必看","\u002Fnews\u002F1011",[18],{"id":58,"kind":7,"title":59,"summary":60,"image":61,"href":62,"meta":36,"badge":10,"author":12,"stats":-1,"accent":37,"coverRatio":38,"tags":63},"NEWS_ARTICLE:1025","Ninja 使用笔记","title: Ninja 使用笔记 author: 凌杰 date: 2026-08-10 tags: 自动化构建 categories: 软件使用经验 [!NOTE] 笔记说明 这篇笔记是《[[Makefile 使用笔记]]》的姊妹篇，将用于记录本人在使用 Ninja 这款项目构建工具过程中所记录","https:\u002F\u002Fimg2024.cnblogs.com\u002Fblog\u002F691082\u002F202609\u002F691082-20260906155027127-1498408465.png","\u002Fnews\u002F1025",[18],{"id":65,"kind":7,"title":66,"summary":67,"image":68,"href":69,"meta":36,"badge":10,"author":12,"stats":-1,"accent":37,"coverRatio":38,"tags":70},"NEWS_ARTICLE:838","AI 学习笔记：LLM 的微调实验","title: LLM 的微调实验 author: 凌杰 date: 2026-08-21 tags: LoRA, LLaMA-Factory, Qwen categories: 人工智能 [!NOTE] 笔记说明 这篇笔记对应的是《[[关于 AI 的学习路线图]]》一文中所规划的第三个学习阶段。其中","https:\u002F\u002Fimg2024.cnblogs.com\u002Fblog\u002F691082\u002F202609\u002F691082-20260902122904258-1285666483.png","\u002Fnews\u002F838",[18],{"id":72,"kind":7,"title":73,"summary":74,"image":75,"href":76,"meta":36,"badge":10,"author":12,"stats":-1,"accent":37,"coverRatio":38,"tags":77},"NEWS_ARTICLE:843","介绍一下常用的Token鉴权方案","本文梳理 Session‑Cookie、JWT、OAuth2.0、SSO 主流 Token 鉴权方案，对比各方案适用场景。重点讲解生产级 JWT 双 Token 架构，给出 RS256 非对称加密、Redis 黑名单、网关统一鉴权等 Java 实战代码。剖析 JWT 注销、并发刷新、令牌泄露等落地痛","https:\u002F\u002Fimg2024.cnblogs.com\u002Fblog\u002F739056\u002F202609\u002F739056-20260902114155245-1924311389.png","\u002Fnews\u002F843",[18],{"id":79,"kind":7,"title":80,"summary":81,"image":14,"href":82,"meta":36,"badge":10,"author":12,"stats":-1,"accent":37,"coverRatio":38,"tags":83},"NEWS_ARTICLE:854","分治：序列分治（CDQ）与点分治","分治：序列分治（CDQ）与点分治 一、分治思想概述 分治（Divide and Conquer）是算法设计中最核心的思想之一。它的基本策略是： 分（Divide）：将原问题划分为规模更小的子问题。 治（Conquer）：递归地求解子问题（若子问题足够小则直接求解）。 合（Combine）：将子问题的","\u002Fnews\u002F854",[18],{"id":85,"kind":7,"title":86,"summary":87,"image":88,"href":89,"meta":36,"badge":10,"author":12,"stats":-1,"accent":37,"coverRatio":38,"tags":90},"NEWS_ARTICLE:863","弱模型不能裸奔：Agent Harness 凭什么真实有效","Harness 不是给弱模型贴的创可贴。它是把工程纪律——验证、门禁、不变量、路由——变成架构里一等公民的方式。模型每半年换一代，今天省钱的 Flash 明天可能就过时了，但那套'默认怀疑、机器校验、按决策密度调度'的流程会留下来，并且越跑越值钱。\n弱模型不能裸奔。给它穿上 harness，便宜才真","https:\u002F\u002Fimg2024.cnblogs.com\u002Fblog\u002F510\u002F202608\u002F510-20260830181633785-1171146572.jpg","\u002Fnews\u002F863",[18],{"id":92,"kind":7,"title":93,"summary":94,"image":95,"href":96,"meta":36,"badge":10,"author":12,"stats":-1,"accent":37,"coverRatio":38,"tags":97},"NEWS_ARTICLE:865","焕新鸿蒙应用权限管理方案，应用授权体验再升级","作为用户或应用开发者，或许经历过类似的体验场景：使用应用的过程中，触发应用某些功能会需要访问你的位置、麦克风、相机等常用权限，若为了保护隐私拒绝授权后，想要使用功能时，再次打开却找不到设置入口；或开启流程繁琐，需要经过频繁跳转和设置。这一问题不仅影响用户体验，还可能造成应用功能不可用、用户流失，也制","https:\u002F\u002Fimg2024.cnblogs.com\u002Fblog\u002F2396482\u002F202609\u002F2396482-20260901170248450-744043562.png","\u002Fnews\u002F865",[18]]