题解:AT_abc436_g [ABC436G] Linear Inequation

· · 题解

题解

初步解法

因此我们考虑选择一个大于 $1$ 的整数 $B$,并把 $x_i$ 和 $M$ 给做一个进制转换,那么我们的条件就变成了这样: $$\sum_{d\ge0}^{}B^d\sum_{i=1}^{N} (\lfloor{\frac{x_i}{B^d}}\rfloor\bmod B) A_i \le \sum_{d\ge0}^{}B^d(\lfloor{\frac{M}{B^d}}\rfloor\bmod B)$$ 这就相当于有 $N$ 个 $B$ 进制的乘法竖式,其中乘数是确定的(即 $A_i$),你需要做的就是问有多少种同时确定被乘数的方案,使得最后的乘法运算结果的总和小于等于 $M$。 我们就可以发现这个数位 DP 就很明显了,只需要知道每一个 $d$ 所对应的 $\displaystyle(\lfloor{\frac{x_i}{B^d}}\rfloor\bmod B) A_i$ 即可,而且也可以快速处理到下一位的进位,因为进位的总量是不超过 $(B-1)\sum A_i$ 的,而且我们不需要关心竖式每一个数字填了什么,我们只需要关心他们的总和即可,这完全可以塞进去一个 DP 状态里面。 因此,我们考虑从低位到高位 DP,设状态 $\operatorname{dp}(d, jw, lim)$ 表示已处理完低 $d$ 位(即第 $0$ 到 $d-1$ 位),此时向第 $d$ 位的进位值为 $jw$,且已处理部分与 $M$ 的低 $d$ 位的大小关系为 $lim$。 这里,我们用 $lim=0$ 表示小于 $M$,用 $lim=1$ 表示等于 $M$,$lim=2$ 表示大于 $M$。这样,可以接受的状态就是 $jw=0,lim\le 1$。 我们再来思考一下具体的转移,转移显然是直接枚举 $\displaystyle\sum_{i=1}^{N} (\lfloor{\frac{x_i}{B^d}}\rfloor\bmod B) A_i$ 的值,假设这个值是 $jw'$,那么我们就可以转移到 $\operatorname{dp}(d+1,\lfloor\dfrac{jw'+jw}{B}\rfloor, lim')$。 对于 $lim'$ 的求解,这个是好做的,如果当前位 $(jw'+jw)\bmod B$ 等于 $\displaystyle\lfloor{\frac{M}{B^d}}\rfloor \bmod B$ 那么 $lim'=lim$,如果大于那么 $lim'=2$,否则就是 $lim'=0$。 不过显然这个还是要有一些转移系数,我们令 $\operatorname{cnt}(i)$ 为满足以下所有条件的 $(x_1,x_2,x_3,\dots,x_N)$ 的个数: - $x_i< B$。 - $\displaystyle\sum x_iA_i=i$。 显然直接 $O(nB\sum A_i)$ 的大力背包是可以做的,这个非常简单,此处不赘述。 这样,我们最终的转移式子也就出来了: $$\operatorname{dp}(d,jw,lim)=\sum_{jw'=0}^{\sum A_i} \operatorname{dp}(d+1,\lfloor\dfrac{jw'+jw}{B}\rfloor,lim')\times \operatorname{cnt}(jw')$$ 其中: $$lim'=\left\{\begin{aligned} 2 \space ((jw'+jw)\bmod B > (\lfloor{\frac{M}{B^d}}\rfloor\bmod B)) \\ lim \space ((jw'+jw)\bmod B = (\lfloor{\frac{M}{B^d}}\rfloor\bmod B)) \\ 0 \space ((jw'+jw)\bmod B < (\lfloor{\frac{M}{B^d}}\rfloor\bmod B))\end{aligned}\right.$$ (即数位 DP 的标记转移)。 边界条件定义为 $\displaystyle\operatorname{dp}(\lceil\log_B M\rceil+1, 0, 0/1)=1$。 这样我们得到了一个 $O(\log_B M \times (\sum A_i)^2 + (nB\sum A_i))$ 的做法。 参考代码如下: :::info[代码] ```cpp int dfs2(int i, int m) { // cnt[x] = dfs2(1, x) if (i == n + 1) return m == 0; if (dp2[i][m] != -1) return dp2[i][m]; int ans = 0; for (int j = 0; j * a[i] <= m && j < B; ++j) { ans += dfs2(i + 1, m - j * a[i]); } return dp2[i][m] = ans % p; } int dfs(int i, int jw, int lim) { if (i == D) return lim <= 1 && jw == 0; if (dp[i][jw][lim] != -1) return dp[i][jw][lim]; int ans = 0; for (int njw = 0; njw <= n * 100; ++njw) { int nowdig = (njw + jw) % B; ans += dfs(i + 1, (njw + jw) / B, (nowdig > digs[i] ? 2 : (nowdig < digs[i] ? 0 : lim))) * dfs2(1, njw) % p; } return dp[i][jw][lim] = ans % p; } ``` ::: ### 转化 为了方便后续方便优化,我们不妨直接让 $B=2$,即直接在二进制下求解,这样就能方便奇偶性便于分离,对于其他的 $B$ 只会增加分类的数量,效率优化不明显。 然后我们发现这个式子实在是太过于繁琐了,于是我们考虑直接按照 $jw'+jw$ 的奇偶性进行分类讨论。 $$\operatorname{dp}(d,jw,lim)=\sum_{jw'=0}^{\sum A_i}[2\mid (jw'+jw)] \operatorname{dp}(d+1,\lfloor\dfrac{jw'+jw}{B}\rfloor,lim')\times \operatorname{cnt}(jw')\\ + \sum_{jw'=0}^{\sum A_i}[2\nmid( jw'+jw)] \operatorname{dp}(d+1,\lfloor\dfrac{jw'+jw}{B}\rfloor,lim')\times \operatorname{cnt}(jw')$$ 这样做有很多好处,$\lfloor\dfrac{jw'+jw}{B}\rfloor$ 和 $(jw'+jw)\bmod B$ 我们都可以很方便地表示出来,因此不妨假设在 $(jw'+jw)\bmod B=1$ 的时候的新标记为 $lim_1$,另一种情况的标记是 $lim_0$。式子就可以变成这个样子: $$\operatorname{dp}(d,jw,lim)=\sum_{jw'=0}^{\sum A_i}[2\mid (jw'+jw)] \operatorname{dp}(d+1,\dfrac{jw'+jw}{B},lim_0)\times \operatorname{cnt}(jw')\\ + \sum_{jw'=0}^{\sum A_i}[2\nmid( jw'+jw)] \operatorname{dp}(d+1,\dfrac{jw'+jw+1}{B},lim_1)\times \operatorname{cnt}(jw')$$ 然后分别对两个和式进行换元,只需要把艾弗森括号消除掉即可。 $$\operatorname{dp}(d,jw,lim)=\sum_{jw'=0}^{\frac{\sum A_i}{2}}\operatorname{dp}(d+1,jw'+\lceil \frac{jw}{2} \rceil,lim_0)\times \operatorname{cnt}(jw'\times2+(jw\bmod2))\\ + \sum_{jw'=0}^{\frac{\sum A_i}{2}}\operatorname{dp}(d+1,jw'+\lfloor \frac{jw}{2} \rfloor,lim_1)\times \operatorname{cnt}(jw'\times2+1-(jw\bmod2))$$ ### 卷积 我们首先让 $\operatorname{cnt_0}(x)=\operatorname{cnt}(x\times 2)$,$\operatorname{cnt_1}(x)=\operatorname{cnt}(x\times 2 + 1)$,后续转化就能变得好看一点。 容易发现,对于固定的 $(d,par, lim)$(其中中间的 $par$ 表示 $jw\bmod 2$,也就是 $jw$ 的奇偶性),我们都需要计算下面这个东西: $$f_t = \sum_{k=0}^{\frac{\sum A_i}{2}}\operatorname{cnt}_{par}(k)\operatorname{dp}(d+1, k+t, lim)$$ 注意我们需要求解所有的 $f_t$ 的结果,而 $t$ 的取值对应到上文就是 $\lfloor \dfrac{jw}{2}\rfloor$(奇数部分)或者是 $\lceil\dfrac{jw}{2}\rceil$(偶数部分)。对于每一个求解是耗费时间的,考虑用卷积优化,先将其转化成简单的形式。 不过很可惜 $k+k+t$ 并不是一个定值,我们考虑令 $\operatorname{cnt}'_{par}(k)=\operatorname{cnt}_{par}(\frac{\sum A_i}{2}-k)$,即直接把整个序列颠倒过来。那么式子就可以变成这样: $$f_t = \sum_{k=0}^{\frac{\sum A_i}{2}}\operatorname{cnt}'_{par}(\frac{\sum A_i}{2}-k)\operatorname{dp}(d+1, k+t, lim)$$ 然后再给 $k$ 换个元: $$f_t = \sum_{k=0}^{\frac{\sum A_i}{2}}\operatorname{cnt}'_{par}(k)\operatorname{dp}(d+1, \frac{\sum A_i}{2}+t-k, lim)$$ 于是: $$a(k) = \operatorname{cnt}'_{par}(k)\\ b(k) = \operatorname{dp}(d+1, k, lim)\\ f_t =(a * b)\left[\frac{\sum A_i}{2} + t\right]$$ 然后就可以直接卷积了。 时间复杂度 $O((n\sum A_i) \log (n\sum A_i)\log M)$,除去卷积带来的额外开销,有 $6$ 倍常数($jw$ 的奇偶性和数位 DP 的标记个数)。 ### 代码 这里卷积实现用的是 AC 库。 ```cpp #include <bits/stdc++.h> #include <atcoder/all> using namespace std; #define int long long const int B = 2, D = 62, V = 10000; // D > log M, V >= \sum A_i int n, m, a[103], dp[D+1][V+2][3], dp2[101][V+2]; // dp[d][jw][lim] 与题解阐述的相同,d 表示当前位(从小到大考虑),jw 表示进位,lim 表示大小关系 int res[3][2][V+2]; // 用于卷积存储结果临时使用的数组 const int p = 998244353; signed main() { cin >> n >> m; for (int i = 1; i <= n; ++i) cin >> a[i]; dp[D][0][0] = dp[D][0][1] = 1; dp2[0][0] = 1; for (int i = 1; i <= n; ++i) { for (int m = 0; m <= V; ++m) { dp2[i][m] = (dp2[i - 1][m] + (m >= a[i] ? dp2[i - 1][m - a[i]] : 0)) % p; // 处理 cnt 的值 } } for (int i = D - 1; i >= 0; --i) { int dig = (m >> i) & 1; // m 的第 i 位 for (int jw = 0; jw <= 1; ++jw) for (int lim = 0; lim <= 2; ++lim) { // 对于每一种 jw 的奇偶性(并非 jw 本身)和 lim 标记做卷积 vector<int> a(V/2+1), b(V+1), c; for (int j = 0; j <= V/2; ++j) a[V/2-j] = dp2[n][j*2+jw]; // 翻转序列 for (int j = 0; j <= V; ++j) b[j] = dp[i+1][j][lim]; c = atcoder::convolution(a, b); for (int j = 0; j <= V/2; ++j) res[lim][jw][j] = c[V/2 + j] % p; // 从结果提取需要的部分 } for (int jw = 0; jw <= V; ++jw) for (int lim = 0; lim <= 2; ++lim) { dp[i][jw][lim] = (res[dig?0:lim][jw&1][(jw+1)/2] + res[dig?lim:2][!(jw&1)][jw/2]) % p; // 这里的 dig?0:lim 之类的东西是数位 dp 的标记更新,省去了一些没必要的条件判断。 } } cout << dp[0][0][1] << "\n"; return 0; } ``` [AC-Link](https://atcoder.jp/contests/abc436/submissions/71740309) 不得不说 AC 库的板子就是快,赛时贺的一个模板要跑 800 多毫秒。 ## 总结 做法核心就是数位 DP 并且根据奇偶性化式子得到卷积形式。 这个数位 DP trick 也在 [Codeforces-1290F](https://codeforces.com/problemset/problem/1290/F) 一题中也有应用,只不过不需要卷积,而是需要一些奇妙的计算几何知识。感兴趣的读者可以去尝试。