题解:CF924F Minimal Subset Difference
题目描述
定义 \(f(n)\) 表示将十进制数 \(n\) 所有数码之间填入加号或者减号,最终得到的值的绝对值最小值。
\(T\) 组询问,给定 \(l,r,k\),求满足 \(l≤m≤r\) 且 \(f(m)≤k\) 的 \(m\) 的个数。
数据范围:\(1≤T≤5×10 ^4\) ,\(1≤l≤r≤10^{18}\) ,\(0≤k≤9\)。
solution
先考虑如何计算 \(f\),这里与其他题解是一样的,都是维护一个 0/1 背包 \(dp_{c}\) 表示总和为 \(c\) 能否凑出来。观察到:如果当前总和为负数,则使用加法,否则使用减法,这样总和就能控制在 \(-9\sim9\) 内,所以答案不超过 \(9\)。在此基础上,如果当前的和超过 \(90\),那么最后最多减到 \(90-(18\times 9-90)=9\),因此可以限定总和不超过 \(90\)。这样以后进行搜索,发现不同的 \(dp\) 只有不超过 \(2\times 10^4\) 种,这样就可以以所有可能的 \(dp\) 数组为状态节点,以加入的数字为字符集,建立 \(10\) 个 DFA,第 \(i\) 个 DFA 上的某个状态为接受状态,当且仅当它对应的答案 \(\leq k\)。至此可以做数位 dp,枚举某一段前缀贴着题目输入的数,下一位小于题目输入的数,后面的位任意,这样的信息可以表示为从 DFA 上某个节点开始走 \(k\) 步,能到达多少个接受状态,这也是可以预处理的。这样这题就做完了……吗?
题目中的 DFA 非常庞大,如果我们担心运行时不能建出这个 DFA(?),我们可以考虑在本地建出 DFA 并做 DFA 最小化,然后将新的 DFA 贴到代码中,这样就不用担心建立 DFA 会超时了。下面我们来尝试一下。
首先你需要找到正确的求最小 DFA 的方法,浅谈有限状态自动机及其应用 - 杭州学军中学 徐哲安 中的《4 DFA 的等价类与最小化》一节就是一个正确的 DFA 最小化,其中提到一个暴力划分等价类的算法,和另一个不那么暴力的基于启发式分裂的划分等价类算法 Hopcroft 算法(注意看清楚,这些算法都是用于划分等价类的)。我们可以实现一个 DFA 最小化的代码:
#!/bin/env python3
from collections import defaultdict, deque
def rebuild(tr, P, q0):
tot = 0
drn = {}
for S in P:
for nd in S:
drn[nd] = tot
tot += 1
newtr = {}
tot = 0
for S in P:
for nd in S:
acc, trs = tr[nd]
break
newtr[tot] = acc, tuple(map(lambda nd: drn[nd], trs))
tot += 1
return newtr, drn[q0]
def search_dfa(tr, st):
q = [st]
vis = {st}
l = 0
while l < len(q):
u = q[l]
l += 1
for v in tr[u][1]:
if v not in vis:
vis.add(v)
q.append(v)
return {nd: tr[nd] for nd in q}
def hopcroft(tr):
sgm = 0
for nd, (acc, trs) in tr.items():
sgm = len(trs)
break
Q = frozenset({nd for nd, (acc, trs) in tr.items()})
F = frozenset({nd for nd, (acc, trs) in tr.items() if acc})
P = {F, Q - F}
W = {F}
while W:
A = W.pop()
for c in range(sgm):
X = {nd for nd, (acc, trs) in tr.items() if trs[c] in A}
Ys = {(Y, U, V) for Y in P if (U := Y - X) and (V := Y & X)}
for Y, U, V in Ys:
P.remove(Y)
P.add(U)
P.add(V)
if Y in W:
W.remove(Y)
W.add(U)
W.add(V)
else:
if len(U) < len(V):
W.add(U)
else:
W.add(V)
return P
def solve(tr, q0):
tr = search_dfa(tr, q0)
P0 = [frozenset({nd}) for nd, (acc, trs) in tr.items()]
tr, q0 = rebuild(tr, P0, q0)
P = hopcroft(tr)
return rebuild(tr, P, q0)
def print_dfa(tr, q0):
F = []
print("{", end="")
for i in range(len(tr)):
acc, trs = tr[i]
if acc:
F.append(i)
print("{", end="")
print(*trs, sep=",", end="},")
print("}")
print("q0:", q0)
print("ACC:", end=" ")
print(*F, sep=",")
"""
一个 dfa 是一个 dict,键为结点,值是元组套元组 `(acc, (tr[0], tr[1], ...))`,acc 表示该状态是否是接受状态,tr[0], tr[1], ... 就是转移到的状态。
`solve(tr, q0)` 用于化简 dfa,q0 是初始状态。`print_dfa(tr, q0)` 输出一个被 solve 过的 dfa。
"""
注意,以上代码时间复杂度是错误的,但这个不妨碍我们在本地跑出结果,下面是生成原始 DFA 的代码:
#include <bits/stdc++.h>
using namespace std;
#ifdef LOCAL
#define debug(...) fprintf(stderr, ##__VA_ARGS__)
#else
#define debug(...) void(0)
#define endl "\n"
#endif
using LL = long long;
using bin = bitset<91>;
unordered_set<bin> vis;
void dfs(bin f) {
if (vis.find(f) != vis.end()) return ;
vis.insert(f);
for (int c = 1; c <= 9; c++) {
auto tmp = f << c | f >> c;
for (int i = 0; i < c; i++) if (f[i]) tmp[c - i] = true;
dfs(tmp);
}
}
int main() {
#ifndef LOCAL
cin.tie(nullptr)->sync_with_stdio(false);
#endif
dfs(1);
cerr << "searched" << endl;
for (int k = 0; k <= 9; k++) {
cout << "{";
for (auto f : vis) {
int pos = 0;
while (pos <= k && !f[pos]) ++pos;
cout << "'" << f << "':(" << (pos <= k ? "True" : "False") << ",(";
for (int c = 0; c <= 9; c++) {
auto tmp = f << c | f >> c;
for (int i = 0; i < c; i++) if (f[i]) tmp[c - i] = true;
cout << "'" << tmp << "',";
}
cout << ")),";
}
cout << "}";
cout << endl;
}
return 0;
}
以上代码输出了 10 个 DFA,只需要将它们喂给那份 DFA 最小化的代码,就能输出 10 个 DFA,花的时间为几分钟左右。注意由于 \(f(x)\leq 9\) 的 DFA 全部是接受状态,上面那个代码会运行时错误,但是我们特殊处理一下就好了。
到这里,我们就可以将 DFA 写入到代码里,获得一份答案正确的代码:
#include <bits/stdc++.h>
using namespace std;
#ifdef LOCAL
#define debug(...) fprintf(stderr, ##__VA_ARGS__)
#else
#define debug(...) void(0)
#define endl "\n"
#endif
using LL = long long;
struct DFA {
vector<array<int, 10>> tr;
int q0;
vector<int> acc;
vector<LL> f[19];
vector<int> ac;
void calc() {
int n = (int)tr.size();
for (int i = 0; i < 19; i++) f[i].resize(n);
ac = vector<int>(n);
for (int x : acc) ac[x] = true;
for (int st = 0; st < n; st++) {
vector<LL> pre(n);
pre[st] = 1;
f[0][st] = ac[st];
for (int i = 1; i < 19; i++) {
vector<LL> now(n);
for (int j = 0; j < n; j++) for (int c = 0; c <= 9; c++) now[tr[j][c]] += pre[j];
pre = now;
for (int j = 0; j < n; j++) if (ac[j]) f[i][st] += pre[j];
}
}
}
} dfa[9];
void init() {
dfa[0..8] = ???; // 9 个 DFA
for (int i = 0; i < 9; i++) dfa[i].calc();
}
LL fsolve(LL n, int d) {
if (!n) return 1;
if (d == 9) return n + 1;
vector<int> vec;
while (n) vec.push_back(n % 10), n /= 10;
reverse(vec.begin(), vec.end());
int u = dfa[d].q0;
LL res = 0;
for (int i = 0; i < (int)vec.size(); i++) {
for (int c = 0; c < vec[i]; c++) {
int v = dfa[d].tr[u][c];
res += dfa[d].f[(int)vec.size() - 1 - i][v];
}
u = dfa[d].tr[u][vec[i]];
}
return res + dfa[d].ac[u];
}
int main() {
#ifndef LOCAL
cin.tie(nullptr)->sync_with_stdio(false);
#endif
init();
int t;
cin >> t;
while (t--) {
LL l, r;
int k;
cin >> l >> r >> k;
cout << fsolve(r, k) - fsolve(l - 1, k) << endl;
}
return 0;
}
这里发现 CodeForces 的代码长度限制为 64K,以上代码超过 70K,即使答案正确,也无法提交。
考虑使用 base64 编码压缩第一个 DFA,这里是考虑到第一个 DFA 的大小为 \(715\),我们可以用 \(10\) 位二进制数存一个结点编号,这样只需要 \(10\times 10\times 715\) 个 bits 就能存下这个 DFA。然后使用 base64 编码将这些 bits 转写,存到代码中即可。这样代码长度达到 64K,可以恰好通过。
以下是 base64 编码与解码的一个参考代码。既然是自己用,可以不用关注正常的 base64 编码规则。
#include <bits/stdc++.h>
using namespace std;
#ifdef LOCAL
#define debug(...) fprintf(stderr, ##__VA_ARGS__)
#else
#define debug(...) void(0)
#define endl "\n"
#endif
using LL = long long;
constexpr const char* table = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_";
constexpr int buf[][10]
#include "in.txt"
;
int tot;
bool dat[10000010];
void write(int x, int d) {
while (d--) dat[tot++] = x >> d & 1;
}
int main() {
#ifndef LOCAL
cin.tie(nullptr)->sync_with_stdio(false);
#endif
for (auto&& arr : buf) for (auto x : arr) write(x, 10);
for (int i = 0; i < tot; i += 6) {
int x = 0;
for (int j = i; j < i + 6; j++) x = x << 1 | dat[j];
cout << table[x];
}
cout << endl;
return 0;
}
vector<array<int, 10>> decode(const string& buf) {
static bool dat[10000010];
static constexpr const char* table = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_";
int tot = 0, ptr = 0;
auto read = [&](int d) {
int x = 0;
for (int i = 0; i < d; i++) x = x << 1 | dat[ptr++];
return x;
};
for (char ch : buf) {
int x = find(table, table + 64, ch) - table;
for (int d = 5; d >= 0; d--) dat[tot++] = x >> d & 1;
}
vector<array<int, 10>> res(715);
for (int i = 0; i < 715; i++) {
for (int j = 0; j < 10; j++) res[i][j] = read(10);
}
return res;
}
以下是在 CodeForces 上的 AC 提交记录:https://codeforces.com/contest/924/submission/316781602。
至此本题就结束了。我们已经有了最小的 DFA,可以尝试加强这道题目了。
本文来自博客园,作者:caijianhong,转载请注明原文链接:https://www.cnblogs.com/caijianhong/p/18843050/solution-CF924F
浙公网安备 33010602011771号