这可能是目前全网最快的自幂数计算方法(C++)

改了一下标题,希望能引一波流(手动狗头

好吧我承认,这玩意可能不是全网最快,但它确实很快,看一下文章末尾的数据就知道了


前言:这个OIer原本以为各位都会,但翻了一下博客发现各位的做法千奇百怪,目前看到的唯一一个貌似是正解的做法用的是python库函数(用其他语言的人要怎么办啊喂),遂写了这篇博客。

假设我们现在要求 n 位自幂数,直接枚举 [ 1 0 n − 1 , 1 0 n ) [10^{n-1},10^n) [10n1,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+28n+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) (p1,sum+kpn,lessk) 0 ≤ k ≤ l e s s 0 \leq k \leq less 0kless)

显然,当 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 n30 的情况,就算无视递归自带的常数,期望运行时间也超过了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<10n1的状态也要剪掉。

那么这里又出现了一个小问题,我们怎么判断在当前搜索状态下继续搜索的所有 s u m sum sum 都小于 1 0 n − 1 10^{n-1} 10n1

那么就体现出我们之前把 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+lesspn

那么如果 s u m + l e s s ⋅ p n < 1 0 n − 1 sum + less \cdot p^n < 10^{n-1} sum+lesspn<10n1 ,那么我们也直接返回。

code

由于原本的代码压行严重可能会引起部分人的不适,所以我格式化了一下。

以及如果输入一个数 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 位。

你可能感兴趣的:(搜索,算法,c++,dfs)