数位 DP
数位 DP 在我的理解中和普通 DP 并没有什么区别,由于自身的特性还可以很好的使用搜索处理,但是需要注意需要某些剪枝才能保证正确性。
难点一在于数位 DP 的理解,有些剪枝不仅仅是剪枝,确保的还是算法正确性。
难点二在于边界处理,这也与数位 DP 的理解有关。
DP 似乎主要靠直接写题,不需要非常多的前置知识。
一 数位间可转移,字典序 且最小的 。位数为 。
方案: dfs
例题 1.1 :https://www.luogu.com.cn/problem/CF1560F2
题意:
给一个
题解:
设
首先想到
考虑
容易想到只需要再维护一个状态,即当前搜索到的数位总共有
路径答案如果不方便维护,可以选择记录在
只需要搜
dfs 的第一个一定是字典序最小,但是除了特定算法比如欧拉路,dfs 不要考虑维护回溯后的答案。bool dfs 是典型的处理。
view
std::vector<int> d; while (n) { d.push_back(n % 10); n /= 10; } std::reverse(d.begin(), d.end()); ll ans = 0; int vis[10] = {0}; std::function<int(int, int, int)> dfs = [&](int x, int larger, int cnt) { if (x == d.size()) { return true; } for (int i = larger ? 0 : d[x]; i <= 9; i++) { vis[i] += 1; // 是剪枝,也是正确性 ans = ans * 10 + i; if (cnt + (vis[i] == 1) <= k && dfs(x + 1, larger | (i > d[x]), cnt + (vis[i] == 1))) return true; ans /= 10; vis[i] -= 1; } return false; }; dfs(0, 0, 0);
边界维护:
由于是找
显然首位一般不会出现
算法正确性与复杂度分析:
- 假设
,不妨假设前缀是 ,那么此时的构造就是 , 往后的构造可以任选,搜下去一定存在答案。 - 假设
,不妨假设前缀是 ,此时 往后的构造不能任选,搜下去不一定存在答案。
时间:
- 由
,只有 才有可能会回溯。由 ,最多只会回溯 次,可以使后面的数可以被任意构造。 - 如果不回溯,时间复杂度等于树的高度。回溯如果只有常数次,都可以认为时间复杂度就是
。
正确性:同时可以观察 cnt + (vis[i] == 1) <= k ,似乎是个剪枝。实际上如果不加上去,不会影响时间复杂度,但由
例题 1.2:例题 1.1 的变种
题意:
给一个
题解:
设
可以想到
考虑
显然也只需要多一个状态,当前数位的种类有
view
std::vector<int> d; while (n) { d.push_back(n % 10); n /= 10; } std::reverse(d.begin(), d.end()); ll ans = 0; int vis[10] = {0}; std::function<int(int, int, int)> dfs = [&](int x, int larger, int cnt) { if (x == d.size()) { return true; } for (int i = larger ? 0 : d[x]; i <= 9; i++) { vis[i] += 1; ans = ans * 10 + i; if (cnt + (vis[i] == 1) + d.size() - (x + 1) >= k && dfs(x + 1, larger | (i > d[x]), cnt + (vis[i] == 1))) return true; ans /= 10; vis[i] -= 1; } return false; }; int m = d.size(); while (!dfs(0, 0, 0)) { d.resize((int)d.size() + 1); d.assign((int)d.size(), 0); d[0] = 1; assert((int)d.size() <= (int)std::max(k, m + 1)); }
可以看出 dfs 的变动只有 cnt + (vis[i]) <= k 变为了 cnt + (vis[i] == 1) + d.szie() - (x + 1) >= k 。
由于位数可能会更多(但只会多常数次)。原本的直接 dfs 变成了 while(dfs) 。
例题 1.3 https://atcoder.jp/contests/abc042/tasks/arc058_a
题意:
给一个数
显然
但是若
考虑数位 DP 。dfs 的初始状态为:搜了 x 位。前缀比
甚至不需要增加状态。只用把不合法的数位跳过即可。
view
std::string str; std::cin >> str; int n =str.size(); std::vector<int> d(n); for (int i = 0; i < n; i++) { d[i] = str[i] - '0'; } int k; std::cin >> k; int v[10] = {0}; for (int i = 1; i <= k; i++) { int x; std::cin >> x; v[x] += 1; } std::set<int> legal; for (int i = 0; i < 10; i++) if (!v[i]) legal.insert(i); std::string ans = ""; std::function<bool(int, int)> dfs = [&] (int x, int larger) { if (x == d.size()) { return true; } for (int i = larger ? 0 : d[x]; i <= 9; i++) if (legal.count(i)) { ans.push_back((char)(i + '0')); if (dfs(x + 1, larger |= i > d[x])) return true; ans.pop_back(); } return false; }; int m = d.size(); while (!dfs(0, 0)) { d.resize((int)d.size() + 1); d.assign((int)d.size(), 0); d[0] = 1; assert((int)d.size() <= (int)std::max(m + 1, 10)); } std::cout << ans << "\n";
二 数位间不可转移,但能按数位分类
方案: 按数位分类,并构造。
2.1 例题
询问
思路
区间
于是问题转化为,统计
首先将
分类的好处是方便在相同数位下处理。
例如
更一般的,若
我们对
这里显然是这
而对于某一类,我们考虑
一:考虑第
-
前
位是固定的 。对每个 ,固定要选 。剩下的可选位只有从 位开始往后的位置。第 位有 共 种选择,后 位每位都有 种选择。则 的方案数是 。 -
第
位可选 共 种,我们枚举这个数 ,前 位固定,后 位可以任选,于是 的贡献是 。 -
第
位往右的数,可以是 的任意数,我们枚举这个数 ,在后 个位置种选择一个位置放入 。第 位有 种选择,其他位可以任选。于是 的贡献是 。
最终一类中存在
二:最后考虑第
计算方式是:枚举第一个非零位的位置。
- 如果第一个非零位是第
位,这一位有 种选择, 剩下 位可以任选。这 位存在 的方案数,即存在一个 的方案数,答案为 。 - 如果第一个非零位是第
位,这一位有 种选择( 以外的 个数),剩下 位可以任选。这 位存在 的方案数是 。
三:第
三 数位间不可转移,但依赖数位计数
方案: 按数位分类,并计数
四 数位间可转移,方案数问题
方案: 数位 DP
五 数位间可转移,非方案数问题
似乎没见过,实际出现了也很难做,可以考虑不进行 DP 。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现