改了一下标题,希望能引一波流(手动狗头
好吧我承认,这玩意可能不是全网最快,但它确实很快,看一下文章末尾的数据就知道了
前言:这个OIer原本以为各位都会,但翻了一下博客发现各位的做法千奇百怪,目前看到的唯一一个貌似是正解的做法用的是python库函数(用其他语言的人要怎么办啊喂),遂写了这篇博客。
假设我们现在要求 n 位自幂数,直接枚举 [ 1 0 n − 1 , 1 0 n ) [10^{n-1},10^n) [10n−1,10n) 区间内的所有数并判断的写法想必不用我讲,那么我们考虑优化。
通过百度,我们可以得到自幂数的性质
如果在一个固定的进制中,一个n位自然数等于自身各个数位上数字的n次幂之和,则称此数为自幂数。
那么,我们从这个性质入手,可以快速想出一个算法:
搜索,在 [ 0 , 9 ] [0,9] [0,9] 内任意选 n n n 个可重复的数,然后计算出这 n n n 个数的 n n n 次幂之和,再判断这个和是否是一个由这 n n n 个数组成的数,如果是那么它就是一个自幂数。
那这和暴力枚举有什么区别?
当然有区别,因为这个算法可以优化:
我们顺序搜索 [ 0 , 9 ] [0,9] [0,9] 内每个数被 重复选择 了几次,然后计算出这 n n n 个数的 n n n 次幂之和,再判断 这个幂之和 是否是一个由这 n n n 个数组成的数,如果是那么它就是一个自幂数。
考虑到这是一篇面向大众的博客,那么还是写得详细点比较好
我们设 搜索状态为 ( p , s u m , l e s s ) (p,sum,less) (p,sum,less),
p p p 为当前搜索到了 [ 1 , 9 ] [1,9] [1,9] 内第 p p p 个数,
s u m sum sum 为当前被选择的数的 n n n 次幂之和 (比如之前 9 9 9 选 1 1 1 个, 8 8 8 选 2 2 2 个, 5 5 5 选 1 1 1 个 ,那么 s u m = 9 n + 2 ⋅ 8 n + 5 n sum = 9^n + 2 \cdot 8^n + 5^n sum=9n+2⋅8n+5n),
l e s s less less 为还可以选几个数
设初始状态为 ( 9 , 0 , n ) (9,0,n) (9,0,n) ( p = 9 p = 9 p=9 从大往小搜是有意义的,在接下来的剪枝部分中会提到)
那么对于 ( p , s u m , l e s s ) (p,sum,less) (p,sum,less),我们接下来肯定是要搜索 ( p − 1 , s u m + k ⋅ p n , l e s s − k ) (p - 1,sum + k \cdot p^{n},less - k) (p−1,sum+k⋅pn,less−k) ( 0 ≤ k ≤ l e s s 0 \leq k \leq less 0≤k≤less)
显然,当 p = 0 p = 0 p=0 或 l e s s = 0 less = 0 less=0 时我们就可以开始判断当前搜索的是否为自幂数 并 终止搜索了。
我们可以证明这个算法的时间复杂度为 O ( n C n + 9 10 ) O(nC^{10}_{n+9}) O(nCn+910)
虽然这个算法时间复杂度较优,但面对 n ≥ 30 n \geq 30 n≥30 的情况,就算无视递归自带的常数,期望运行时间也超过了2分钟
Q:这么慢,怎么办?
A:剪枝!大力剪枝!
我们发现,在搜索过程中搜索了大量超过 n n n 位数的 s u m sum sum,这部分肯定要剪枝!
那么如果搜索到的 s u m sum sum 大于等于 1 0 n 10^n 10n,我们就直接返回。
既然剪掉了超过 n n n 位数的 s u m sum sum,那么 s u m sum sum 低于 n n n 位数,也就是 s u m < 1 0 n − 1 sum < 10^{n-1} sum<10n−1的状态也要剪掉。
那么这里又出现了一个小问题,我们怎么判断在当前搜索状态下继续搜索的所有 s u m sum sum 都小于 1 0 n − 1 10^{n-1} 10n−1?
那么就体现出我们之前把 p p p 从大往小搜的重要性了。
对于状态 ( p , s u m , l e s s ) (p,sum,less) (p,sum,less),它向下搜索的 s u m sum sum ,肯定不大于当前状态下的 s u m + l e s s ⋅ p n sum + less \cdot p^n sum+less⋅pn
那么如果 s u m + l e s s ⋅ p n < 1 0 n − 1 sum + less \cdot p^n < 10^{n-1} sum+less⋅pn<10n−1 ,那么我们也直接返回。
由于原本的代码压行严重可能会引起部分人的不适,所以我格式化了一下。
以及如果输入一个数 n n n 输出 − 1 -1 −1 代表没有长度为 n n n 的自幂数。
#include
#pragma GCC optimize(2)
using namespace std;
typedef __int128 ull;
constexpr int N = 1e6 + 7;
ull fn[10], lowerlimit, upperlimit, ans[N];
int n, flg[17], acnt, flg2[17];
bool check(ull num)
{
for(int i = 0; i <= 9; ++ i) flg2[i] = flg[i];
while(num)
{
if(flg2[num % 10] <= 0)
{
return false;
}
-- flg2[num % 10], num /= 10;
}
return true;
}
void dfs(ull num, int nowp, int less)
{
if(nowp == 0) flg[0] = less, less = 0;
if(num + less * fn[nowp] < lowerlimit) return ;
if(less == 0)
{
if(check(num))
{
ans[++ acnt] = num;
}
return ;
}
ull res = 0;
for(int *i = &flg[nowp]; *i <= less; ++ *i)
{
if(num + res > upperlimit) break;
dfs(num + res, nowp - 1, less - *i);
res += fn[nowp];
}
flg[nowp] = 0;
}
void print128(__int128 x)
{
if(x < 0) putchar('-'), x = -x;
if(x == 0) return ;
print128(x / 10);
putchar(x % 10 + '0');
}
int main()
{
clock_t start,end;
scanf("%d", &n);
start = clock();
for(ull i = 1; i < 10; ++ i)
{
fn[i] = 1;
for(int j = 1; j <= n; ++ j)
{
fn[i] *= i;
}
}
lowerlimit = 1;
for(int i = 1; i < n; ++ i)
{
lowerlimit *= 10llu;
}
upperlimit = 10 * lowerlimit - 1;
dfs(0, 9, n);
sort(ans + 1, ans + acnt + 1);
if(acnt > 0) {
for(int i = 1; i <= acnt; ++ i) {
print128(ans[i]), putchar(' ');
}
}
else printf("-1");
putchar('\n');
end = clock();
double totaltime = (double)(end - start) / CLOCKS_PER_SEC;
printf("耗时%.4lfs\n", totaltime);
system("pause");
return 0;
}
这份代码在本机耗时 90.701 s 90.701s 90.701s 运行完了 n = 38 n = 38 n=38 的情况。
n = 39 n = 39 n=39 是特殊的,由于 unsinged __int128 的大小限制,这份代码并不能完全搜完所有的 39 39 39 位数,所以我并不确定这份代码在 n = 39 n = 39 n=39 时的正确性。
至于 n > 39 n > 39 n>39 …… 万能的百度百科告诉我们 十进制中最大的自幂数只有 39 39 39 位。