UOJ Logo tangjz的博客

博客

BZOJ 3452 Tyvj1955 Lunatic & ProjectEuler 368 A Kempner-like series

2018-02-27 21:51:39 By tangjz

这是一道数学分析背景的数位 DP 经典题……

为什么 BZOJ 上没人做呢……

题意

BZOJ 3452:给定 $n$ 个不含前导零的数字串 $s_1, s_2, \cdots, s_n$,对于所有满足十进制表示中不含任何一个给定数字串的正整数 $x$,求它们的倒数和,四舍五入精确到小数点后第四位。保证 $n, |s_i| \leq 3~(i = 1, 2, \cdots, n)$。

ProjectEuler 368:对于所有满足十进制表示中不含连续三个相同数位的正整数 $x$,求它们的倒数和,四舍五入精确到小数点后第十位。

注意:Tyvj 上的数据是错的

分析

对于任意一个正整数 $x$,我们可以利用字符串匹配检查它是否对答案产生贡献,而且可以发现这个正整数的任意前缀数字都是可以产生贡献的,因此可以考虑数位 DP 套字符串匹配,按照数位划分状态的同时将字符串匹配的信息存储在状态中。

由于题目涉及到多个字符串间的匹配,因此对于 BZOJ 3452 我们采用 AC 自动机进行字符串匹配,而对于 ProjectEuler 368 我们则只需要记录最后一个数位以及它是否与倒数第二个数位相同。这里假设读者已经具备相关知识储备。

在数位 DP 时,我们需要考虑在一些不含前导 $0$ 的数字 $x~(x \in S)$ 后添加一个数位 $d$ 并维护倒数和,注意到有 $$\frac{1}{10 x + d} = \sum_{k \geq 1}{\frac{(-d)^{k - 1}}{(10 x)^k}},$$ 于是有 $$\sum_{x \in S}{\frac{1}{(10 x + d)^n}} = \sum_{k \geq n}{{k - 1 \choose n - 1} \frac{(-d)^{k - n}}{10^k} \sum_{x \in S}{\frac{1}{x^k}}},$$ 那么当 $x$ 和 $k$ 足够大时, $\sum\limits_{x \in S}{\dfrac{1}{x^k}}$ 是极小的,因此可以为 $x$ 确定长度下界、为 $k$ 确定上界,对答案进行近似计算。

具体来说,注意到 $x = 1$ 时 $k$ 无法取到足够好的上界,因此我们可以设定阈值 $L$,对于 $x < 10^L$ 的情况采用枚举的方法,对于 $x \geq 10^L$ 的情况采用数位 DP 近似。设定 $L$ 后,可以直观地设定 $k$ 的上限 $K$,只需让 $\sum\limits_{x \in S}{\dfrac{1}{x^K}}$ 的最大值也远远小于答案允许的误差即可。

现在继续考虑如何用数位 DP 计算 $x \geq 10^L$ 的贡献,我们定义 $f(i, j, k)$ 表示长度为 $i$、匹配状态为 $j$ 的所有能够产生贡献的数字的 $k$ 次方倒数和,若可行的匹配状态集合为 $P$,则对答案的贡献为 $$\sum_{i > L} \sum_{j \in P}{f(i, j, 1)},$$ 可以再次设定长度的上界进行近似计算,不过这里为了保证精度更好可以采用更好的方法。

设 $M = |P|$,数位 DP 的初始状态 $f(L, j, k)$ 可以写成一个长度为 $M K$ 的行向量 $A$,而 $f(i + 1, j, k)$ 与 $f(i, j, k)$ 之间的转移也可以写成一个大小为 $M K \times M K$ 的矩阵 $B$,则 $x \geq 10^L$ 的全部 DP 信息为 $$\sum_{k \geq 1}{A B^k} = A B (I - B)^{-1},$$ 其中 $I$ 是大小为 $M K \times M K$ 的单位矩阵,所求即结果矩阵的 $M$ 个特定元素之和。

于是本题就在 $\mathcal{O}(10^L + M^3 K^3)$ 的时间复杂度中解决了。

代码

贴一个 ProjectEuler 368 的代码仅供交流学习。

#include <bits/stdc++.h>
typedef double DB;
const int maxd = 10, maxs = 101, maxl = 6;
int m, e, sz;
DB lpw[maxd + 1][maxs], c[maxs][maxs];
DB f[maxs], mat[maxs][maxs << 1 | 1], ans;
void dfs(int dep, int val, int msk) {
    if(dep == maxl) {
        DB prd = 1;
        for(int i = 0; i < e; ++i)
            f[i * m + msk] += 1 / (prd *= val);
        return;
    }
    ans += 1.0 / val;
    for(int i = 0; i < maxd; ++i) {
        if(msk & 1 && (msk >> 1) == i)
            continue;
        dfs(dep + 1, val * maxd + i, i << 1 | ((msk >> 1) == i));
    }
}
int main() {
    m = maxd << 1;
    e = (maxs - 1) / m;
    sz = e * m;
    lpw[0][0] = 0;
    for(int i = 1; i <= maxd; ++i) {
        lpw[i][0] = 0;
        lpw[i][1] = log(i);
        for(int j = 2; j < e; ++j)
            lpw[i][j] = lpw[i][j - 1] + lpw[i][1];
    }
    lpw[maxd][e] = lpw[maxd][e - 1] + lpw[maxd][1];
    for(int i = 0; i < e; ++i) {
        c[i][0] = c[i][i] = 1;
        for(int j = 1; j < i; ++j)
            c[i][j] = c[i - 1][j - 1] + c[i - 1][j];
    }
    for(int i = 0; i < sz; ++i)
        mat[i][i] = mat[i][sz + i] = 1;
    for(int i = 0; i < m; ++i)
        for(int d = 0; d < maxd; ++d) {
            if(i & 1 && (i >> 1) == d)
                continue;
            int ii = d << 1 | ((i >> 1) == d);
            for(int j = 0; j < e; ++j)
                for(int jj = 0; jj <= j; ++jj) {
                    if(!d && jj < j)
                        continue;
                    mat[j * m + i][jj * m + ii] -= ((j - jj) & 1 ? -1 : 1) * exp(log(c[j][jj]) + lpw[d][j - jj] - lpw[maxd][j + 1]);
                }
        }
    for(int i = 0, j, k; i < sz; ++i) {
        for(j = i, k = j + 1; k < sz; ++k)
            if(fabs(mat[k][i]) > fabs(mat[j][i]))
                j = k;
//        assert(fabs(mat[j][i]) > 1e-20);
        DB prd = mat[j][i];
        for(k = i; k < sz << 1; ++k) {
            std::swap(mat[j][k], mat[i][k]);
            mat[i][k] /= prd;
        }
        for(j = 0; j < sz; ++j) {
            if(i == j)
                continue;
            DB prd = mat[j][i];
            for(int k = i; k < sz << 1; ++k)
                mat[j][k] -= prd * mat[i][k];
        }
    }
    for(int i = 1; i < maxd; ++i)
        dfs(1, i, i << 1);
    for(int i = 0; i < sz; ++i)
        for(int j = 0; j < m; ++j)
            ans += f[i] * mat[i][sz + j];
    printf("%.10f\n", (double)ans);
    return 0;
}

Written with StackEdit.

评论

暂无评论

发表评论

可以用@mike来提到mike这个用户,mike会被高亮显示。如果你真的想打“@”这个字符,请用“@@”。