T1:三倍数

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

M=106,则答案为?

100|00|0099|99|99

答案为 99

M 的位数 |M| 满足 |M|=3n+r1r<3 ,则

1|1|1, 2|2|2,  , 999n×9|999|999

都是不超过 M 的三倍数。答案为 999n×9

考虑 |M|3 的倍数的情形
M 分别为 123789456987654321,则答案为?

123|789|456123|123|123

986|986|986987|654|321987|987|987

答案分别为 123986

M 写成三等分的形式 M1M2M3,若 M1<M2 ,或 M1=M2M2M3,则答案即为 M1
否则,答案应为 M11

不难看出,本题需要考察高精度减法。
由于减数仅仅为 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:固定和的倍数序列

ai=kiai1,某一倍数序列长度为 n,则元素总和可以写作:

i=1nai=a1+k2a1+k3k2a1++knkn1k2a1=a1(1+k2+k3k2++knkn1k2)=M

可得 k2+k3k2++knkn1k2=Ma11,这一式子是求出状态转移方程的关键。
其表明:固定 a1,则一个元素总和为 M 的倍数序列,与一个元素总和为 Ma11 的倍数序列一一对应。

状态:

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

转移:

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

方程:

dp[i]=j|idp[ij1]

细节:

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

时间复杂度:O(nn)

代码实现
#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;
}

递推/填表法 递拉/刷表法

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

观察方程 dp[i]=j|idp[ij1],以 ij 为主元,可以改写成:

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

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

M(1+12++1M)=Mi=1M1i

在数学上,i=1+1i 称为调和级数。
由阶的估计的知识,i=1M1i=O(logM)