T1:三倍数
本题难度较大,“三倍数”的位数一定是 \(3\) 的倍数。
若 \(M = 10^6\),则答案为?
答案为 \(99\) 。
若 \(M\) 的位数 \(|M|\) 满足 \(|M| = 3n+r\),\(1 \leqslant r \lt 3\) ,则
都是不超过 \(M\) 的三倍数。答案为 \(\underbrace{99\cdots 9}_{n \times 9}\) 。
考虑 \(|M|\) 为 \(3\) 的倍数的情形
若 \(M\) 分别为 \(123789456\),\(987654321\),则答案为?
答案分别为 \(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[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];
}
}
统计双重循环操作次数,有:
在数学上,\(\sum\limits_{i=1}^{+\infty} \frac{1}{i}\) 称为调和级数。
由阶的估计的知识,\(\sum\limits_{i=1}^M \frac{1}{i} = \mathcal{O}(\log M)\) 。