T1:三倍数

本题难度较大,“三倍数”的位数一定是 \(3\) 的倍数。

\(M = 10^6\),则答案为?

\[100 \big|00\big|00 \geqslant 99 \big|99 \big| 99 \]

答案为 \(99\)

\(M\) 的位数 \(|M|\) 满足 \(|M| = 3n+r\)\(1 \leqslant r \lt 3\) ,则

\[1\big| 1\big| 1,~2\big| 2\big| 2, ~\cdots~, ~\underbrace{99\cdots 9}_{n \times 9} \big| 99\cdots 9\big| 99\cdots 9 \]

都是不超过 \(M\) 的三倍数。答案为 \(\underbrace{99\cdots 9}_{n \times 9}\)

考虑 \(|M|\)\(3\) 的倍数的情形
\(M\) 分别为 \(123789456\)\(987654321\),则答案为?

\[123 \big| 789 \big| 456 \geqslant 123 \big| 123 \big| 123 \]

\[986 \big| 986 \big| 986 \leqslant 987 \big| 654 \big| 321 \leqslant 987 \big| 987 \big| 987 \]

答案分别为 \(123\)\(986\)

\(M\) 写成三等分的形式 \(M_1M_2M_3\),若 \({M_1} \lt {M_2}\) ,或 \(M_1 = M_2\)\(M_2 \leqslant M_3\),则答案即为 \(M_1\)
否则,答案应为 \(M_1 - 1\)

不难看出,本题需要考察高精度减法。
由于减数仅仅为 \(1\),所以无需实现真正意义上的高精度减法。
只需实现好退位、错位即可。

代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)

using namespace std;

int a[400], cnt;

int main() {
    string m;
    cin >> m;
    int n = m.size();
    
    if (n%3) {
        cout << string(n/3, '9') << '\n';
        return 0;
    }
    
    n /= 3;
    if (m.substr(0, n) < m.substr(n, n) or (m.substr(0, n) == m.substr(n, n) and m.substr(n, n) <= m.substr(2*n))) {
        cout << m.substr(0, n) << '\n';
        return 0;
    } 
    
    for (int i = n-1; i >= 0; --i) {
        a[++cnt] = m[i]-'0';
    }
    --a[1];
    for (int i = 1; i <= cnt and a[i] < 0; ++i) {
        a[i] += 10;
        a[i+1]--;
    }
    while (cnt > 1 and !a[cnt]) --cnt;
    for (int i = cnt; i >= 1; --i) {
        cout << a[i];
    }
    
    return 0;
}

T2:固定和的倍数序列

\(a_i = k_ia_{i-1}\),某一倍数序列长度为 \(n\),则元素总和可以写作:

\( \begin{aligned} \sum_{i=1}^n a_i &= a_1 + k_2a_1 + k_3k_2a_1 + \cdots + k_nk_{n-1}\cdots k_2a_1\\ &= a_1(1 + k_2 + k_3k_2 + \cdots + k_nk_{n-1} \cdots k_2) = M \end{aligned} \)

可得 \(k_2 + k_3k_2 + \cdots + k_nk_{n-1} \cdots k_2 = \frac{M}{a_1} - 1\),这一式子是求出状态转移方程的关键。
其表明:固定 \(a_1\),则一个元素总和为 \(M\) 的倍数序列,与一个元素总和为 \(\frac{M}{a_1} - 1\) 的倍数序列一一对应。

状态:

  • 要区分出两种倍数序列的元素总和。
  • \(dp[i]\):和为 \(i\) 的倍数序列的数量。

转移:

  • 任取 \(i\) 的约数 \(j\),则 \(j\) 可以作为倍数序列的第一项 \(a_1\)
  • 所有元素总和为 \(i\) 的倍数序列,按照 \(a_1\) 分类,该分类方法不重复、不遗漏。

方程:

\[dp[i] = \sum_{j \big| i} dp[\frac{i}{j}-1] \]

细节:

  • 初始条件:\(dp[0] = 1\)
  • 状态转移方程中的 \(j\)\(i\) 的约数,而约数是成对出现的,因此没有必要枚举到底。
  • 要注意一对两个约数都是某一完全平方数的平方根的情形,不要重复计数。

时间复杂度:\(O(n\sqrt{n})\)

代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)

using namespace std;
using ll = long long;

//const int mod = 998244353;
const int mod = 1000000007;
struct mint {
    ll x;
    mint(ll x=0):x((x%mod+mod)%mod) {}
    mint operator-() const {
        return mint(-x);
    }
    mint& operator+=(const mint a) {
        if ((x += a.x) >= mod) x -= mod;
        return *this;
    }
    mint& operator-=(const mint a) {
        if ((x += mod-a.x) >= mod) x -= mod;
        return *this;
    }
    mint& operator*=(const mint a) {
        (x *= a.x) %= mod;
        return *this;
    }
    mint operator+(const mint a) const {
        return mint(*this) += a;
    }
    mint operator-(const mint a) const {
        return mint(*this) -= a;
    }
    mint operator*(const mint a) const {
        return mint(*this) *= a;
    }
    mint pow(ll t) const {
        if (!t) return 1;
        mint a = pow(t>>1);
        a *= a;
        if (t&1) a *= *this;
        return a;
    }

    // for prime mod
    mint inv() const {
        return pow(mod-2);
    }
    mint& operator/=(const mint a) {
        return *this *= a.inv();
    }
    mint operator/(const mint a) const {
        return mint(*this) /= a;
    }
};
istream& operator>>(istream& is, mint& a) {
    return is >> a.x;
}
ostream& operator<<(ostream& os, const mint& a) {
    return os << a.x;
}

mint dp[100005];

int main() {
    int m;
    cin >> m;
    
    dp[0] = 1;
    for (int i = 1; i <= m; ++i) {
        for (int j = 1; j*j <= i; ++j) {
            if (i%j) continue;
            dp[i] += dp[i/j-1];
            if (j !=  i/j) dp[i] += dp[j-1];
        }
    }
    
    cout << dp[m] << '\n';
    
    return 0;
}

递推/填表法 \(\to\) 递拉/刷表法

  • 不是对于等号左边的项,将右边所有的项加起来
  • 而是对于等号右边的项,考虑它要加到左边的哪些项上。

观察方程 \(dp[i] = \sum_{j \big| i} dp[\frac{i}{j}-1]\),以 \(\frac{i}{j}\) 为主元,可以改写成:

for (int i = 1; i <= m; ++i) {
      for (int j = 1; j <= m/i; ++j) {
        dp[i*j] += dp[i-1];
    }
}

统计双重循环操作次数,有:

\[M(1 + \frac{1}{2} + \cdots + \frac{1}{M}) = M\sum_{i=1}^M \frac{1}{i} \]

在数学上,\(\sum\limits_{i=1}^{+\infty} \frac{1}{i}\) 称为调和级数。
由阶的估计的知识,\(\sum\limits_{i=1}^M \frac{1}{i} = \mathcal{O}(\log M)\)