题解: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,可以尝试加强这道题目了。

posted @ 2025-04-23 16:53  caijianhong  阅读(56)  评论(0)    收藏  举报