1. 什么是重复单位可整除性
在数论中,我们可以定义两个正整数$a, b$,如果它们的单位重复周期相等,那么我们称两数具有重复单位可整除性。换句话说,如果$a$的周期是$x$,$b$的周期是$y$,那么当$x|y$时,$a$可以整除$b$。
下面我们来看一个例子,假设$a=0.\overline{217391}$,$b=0.\overline{891304}$。那么我们可以发现,$a$的周期为7,$b$的周期为6,且6是7的因子,因此$a$可以整除$b$。
2. 一个简单的实现
下面是一个使用字符串模拟的重复单位可整除性的判断方法:
bool repeat_divisible(string s1, string s2) {
int len1 = s1.length();
int len2 = s2.length();
int pos1 = s1.find('.');
int pos2 = s2.find('.');
int x = len1 - pos1 - 1, y = len2 - pos2 - 1;
if (x > y) swap(x,y), swap(pos1,pos2), swap(len1,len2), swap(s1,s2);
string t = s2.substr(pos2+1, y-x);
for (int i = 0; i < x; i++) {
if (t != s1.substr(pos1+1+i, y-x)) return false;
}
return true;
}
上述代码中,我们通过$find$函数找到小数点的位置,$substr$函数获取小数点后的数列,最后进行比较即可。然而,上述方法存在的问题是重复计算了许多数列,其时间复杂度为$O(n^2)$,无法通过大量测试数据。
3. 优化算法
3.1 线性同余方程组
我们发现,以上方法的时间复杂度主要来自于重复匹配数字串。我们可以想到,如果能找到一个简单的方式,使用一些预处理来将字符串匹配的时间复杂度降至$O(1)$,那么我们的时间复杂度就能从$O(n^2)$降至$O(n)$,这对于大量数据场景的优化至关重要。
我们可以通过解出线性同余方程组的方式,来快速匹配数字串。
先考虑一个例子:$a=0.\overline{137}$、$b=0.\overline{913}$。我们将它们写成分数形式:
$$a=\frac{137}{999}, b=\frac{913}{999}$$
接下来,我们将$a$、$b$循环展开,得到:
$$a=\frac{137}{999}=0.\overline{137}=\frac{1}{10^3} \times 137 + \frac{1}{10^6} \times 137 + \frac{1}{10^9} \times 137 + \cdots$$
$$b=\frac{913}{999}=0.\overline{913}=\frac{1}{10^3} \times 913 + \frac{1}{10^6} \times 913 + \frac{1}{10^9} \times 913 + \cdots$$
然后,我们对于每一个数位,将$a$、$b$的相应倍数相减得到一个新的方程,也就是这个方程组:
$$\begin{cases}
13x-87y=0 \\
137x-913y=0 \\
1370x-9132y=0 \\
13700x-91319y=0 \\
137000x-913192y=0 \\
\vdots \\
\end{cases}$$
我们可以将方程组写成矩阵形式:
$$\begin{bmatrix}
13 & -87 \\
137 & -913 \\
1370 & -9132 \\
13700 & -91319 \\
137000 & -913192 \\
\vdots & \vdots \\
\end{bmatrix} \begin{bmatrix}
x \\
y \\
\end{bmatrix} = \begin{bmatrix}
0 \\
0 \\
0 \\
0 \\
0 \\
\vdots \\
\end{bmatrix}$$
然后,我们使用高斯消元
来求得$x$、$y$每一位的系数,即可通过求$x$与$y$的最小公倍数来得到重复的最小循环长度。
下面是优化算法的部分代码:
ll gcd(ll a, ll b) { return b == 0 ? a : gcd(b, a%b); }
ll lcm(ll a, ll b) { return a / gcd(a, b) * b; }
ll matrix[N<<1][N], n;
void gaussian() {
// omit
}
ll get_len(string s) {
// 单位长度
int len = s.length() - s.find('.') - 1;
// 字符串转化为数字
ll x, p, q;
sscanf(s.c_str(), "%lld.%lld(%lld)", &x, &p, &q);
ll t = x * pow(10, len) + p * (pow(10, len) - 1) / q;
// 构造系数矩阵及常量矩阵
ll b = pow(10, len), l = lcm(b, q);
memset(matrix, 0, sizeof(matrix));
n = len << 1;
for (int i = 0; i < len; i++) {
matrix[i][i] = b;
matrix[i][i+len] = -q;
b /= 10, q /= 10;
}
// 高斯消元求解系数矩阵
gaussian();
// 计算最小循环节长度
for (int i = len; i < n; i++) {
if (~matrix[i][len]) continue;
ll t = 0;
for (int j = 0; j < len; j++) t += matrix[i][j] * (l / pow(10, j+1));
return l / t;
}
return 0;
}
bool repeat_divisible(string s1, string s2) {
// 首先比较小数点后长度是否相等
int len1 = s1.length() - s1.find('.') - 1;
int len2 = s2.length() - s2.find('.') - 1;
int len = gcd(len1, len2);
if (len1 != len2) {
ll t1 = get_len(s1), t2 = get_len(s2);
if (t1 % t2 != 0 && t2 % t1 != 0) return false;
len = lcm(t1, t2) * len1 / len / t1;
}
// 比较长度相等的部分
int pos1 = s1.find('.'), pos2 = s2.find('.');
s1 = s1.substr(pos1+1, len);
s2 = s2.substr(pos2+1, len);
return s1 == s2;
}
3.2 倍增求循环节长度
除了线性同余方程组方法外,我们还可以通过倍增的方式快速计算循环节长度。
我们考虑套路的模仿快速幂,使用如下的判断方式:
$$a^m \equiv a^{m + k\cdot\lambda} (mod\ p)$$
然而,我们可以发现这种方法存在一些问题。因为我们不能保证在乘法过程中的数字串是完整的。
我们可以通过标记是否已经出现过某一个模数的方式来判断是否出现了不完整情况。
ll b, q, x, y, sum = 0;
bool vis[N];
ll get_len(string s) {
int len = s.length() - s.find('.') - 1;
x = pow(10, len) * 1e9 + atoi(s.substr(s.find('.')+1).c_str());
b = pow(10, len), q = 1, sum = 0;
memset(vis, 0, sizeof(vis));
while (!vis[q]) {
vis[q] = true;
q *= b;
y = q / x;
q %= x;
sum += y, sum %= x;
}
return sum;
}
4. 总结
本篇文章介绍了重复单位可整除性的定义及常见的两种优化算法:线性同余方程组和倍增求循环节长度。
代码实现难度较低,算法思想较为简单,但是对于求最小公倍数、高斯消元等数论基础问题的掌握及手写快读等输入输出技巧的熟练使用都有一定的要求,因此具有一定的锻炼价值。