1 条题解
-
0
思路与算法
先给出一个完整可行性判定,再从左向右决定问号。每次按逗号、0到9的顺序尝试字符,选择第一个仍能补成合法完整字符串的字符。因为判定会考虑整个剩余串,这个选择不会把后缀逼入无解状态。
可行性用前缀分割DP。到达某个新整数的起点i时,只保留已经完成的前缀中能够达到的最小最后整数best[i],初始best[0]=0。枚举下一段结束位置j,段中不能经过固定逗号;若不是串尾,下一字符必须能作为逗号,逗号之后还必须留有字符。
对数字段模式,求严格大于best[i]的最小匹配正整数。用十进制字符串把best[i]加1作为下界:位数不足时下界改成该长度的最小正数,位数过长则失败。按位从小到大选数字,用“当前是否仍等于下界前缀”的状态判断后缀能否完成;首位不得为0。失败状态可记忆,成功时得到最小后继。由此更新下一起点的最小最后整数,若合法数字段结束在全串末尾就判定可行。
所有数最多50位,用长度和同长度字典序比较数值,不用long long解析这些整数。
正确性说明
固定分割起点i,两个匹配同一前缀模式的方案只会通过最后一个整数影响后续严格递增约束。较小最后整数允许的后继集合包含较大的,因此保留最小best[i]不会丢失任何完整方案。每个下一段的起止与分隔字符都被枚举,其最小匹配后继又支配同一段更大的后继;按起点递增归纳,DP可达当且仅当存在匹配该模式的合法分割。
匹配后继以prev+1为数值下界,位数判断保证比较等价;同位数逐位按数字升序尝试,并以tight状态记录是否仍贴着下界。一个状态只依赖段内位置和tight,不依赖其他前缀细节,因此失败记忆有效。首位非0且所有固定数字均被尊重,所得第一个成功数字串恰为大于prev的最小正整数。
最后证明外层贪心。假设已固定前缀仍有合法完整补全。当前选择第一个使可行性判定成功的字符c,就确实存在以新前缀开头的合法补全。任何更小字符均已被完整判定为无解;任何采用更大字符的合法答案,在首次不同处都比当前前缀大,可以替换成一个当前可行后缀而得到更小答案。因此逐位交换不会增大字典序,完成后是最小合法串。初始无解时输出impossible,反之可行性不变量保证每一步都能选到字符。
复杂度
令L为串长,最大50。一次可行性至多枚举O(L²)个数字段,每个后继的按位状态O(L)、每状态至多10个数字,连同截取与比较为O(10L³)。每个问号至多11次判定,含最初判定单串时间O(110L^4),多组对各组求和。这是保守上界;代码遇到可行串尾立即返回,固定逗号和长度限制会提前停止。DP保存O(L)个至多L位数字串,辅助空间O(L²),后继递归和失败状态O(L)。完整T=10、长度50模式已实际执行核验。
易错点
- 逗号比任何数字都小,但不能出现在首尾,也不能连用。
- “数字”在递增规则里指一个正整数,10是合法单项,不是每个十进制字符都要隔开。
- 0和前导0均不合法,递增是严格大于。
- 数值比较先比较长度;同长度才比较字典序。整个字符串的字典序则按逗号及数字字符比较,二者不能混用。
- 分隔位置若在最后一格,就会留下空整数,必须排除;不能把它当作到达串尾的合法状态。
- 覆盖固定非法串、50位整数、50个问号、重复或下降数字、长后缀可行性及全部公开样例。
参考代码(C++11)
#include <algorithm> #include <functional> #include <iostream> #include <string> #include <vector> using namespace std; bool lessNumber(const string &a, const string &b) { return a.size() != b.size() ? a.size() < b.size() : a < b; } string successor(const string &pat, const string &prev) { string low = prev; int i = (int)low.size() - 1; while (i >= 0 && low[i] == '9') { low[i] = '0'; --i; } if (i < 0) low = '1' + low; else ++low[i]; int n = pat.size(); if (low.size() > pat.size()) return ""; if (low.size() < pat.size()) low = '1' + string(n - 1, '0'); bool dead[51][2] = {}; string out; function<bool(int, bool)> dfs = [&](int at, bool tight) { if (at == n) return true; if (dead[at][tight]) return false; int first = at == 0 ? 1 : 0; if (tight) first = max(first, low[at] - '0'); for (int d = first; d <= 9; ++d) { if (pat[at] != '?' && pat[at] != char('0' + d)) continue; out.push_back('0' + d); if (dfs(at + 1, tight && d == low[at] - '0')) return true; out.pop_back(); } dead[at][tight] = true; return false; }; return dfs(0, true) ? out : ""; } bool feasible(const string &s) { int n = s.size(); vector<string> best(n + 1); vector<char> ok(n + 1); ok[0] = 1; best[0] = "0"; for (int i = 0; i < n; ++i) if (ok[i]) for (int j = i; j < n; ++j) { if (s[j] == ',') break; bool end = j + 1 == n; if (!end && ((s[j + 1] != ',' && s[j + 1] != '?') || j + 2 >= n)) continue; string v = successor(s.substr(i, j - i + 1), best[i]); if (v.empty()) continue; if (end) return true; int k = j + 2; if (!ok[k] || lessNumber(v, best[k])) { ok[k] = 1; best[k] = v; } } return false; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int T; cin >> T; const string alphabet = ",0123456789"; while (T--) { string s; cin >> s; if (!feasible(s)) { cout << "impossible\n"; continue; } for (int i = 0; i < (int)s.size(); ++i) if (s[i] == '?') { for (char c : alphabet) { s[i] = c; if (feasible(s)) break; } } cout << s << '\n'; } }
- 1