寻找幸运数字的算法

28
我发现了这个问题。一个数字被称为“幸运数”,当它的各位数字之和以及各位数字的平方之和都是质数时。在A和B之间有多少个幸运数?1 <= A <= B <= 10^18。我尝试了以下步骤。

  • 首先我生成了在1到可以通过求平方和得到的数字之间的所有可能的质数(81 * 18 = 1458)。

  • 我读入A和B,找出可以通过加总各位数字得出的最大数字。如果B是两位数,则最大数字为18(由99生成)。

  • 对于1到最大数字之间的每个质数,我应用了整数分割算法。

  • 对于每个可能的分割方式,我检查它们各位数字的平方和是否形成一个质数。如果是,就会生成该分割的可能排列,并且如果它们在范围内,它们就是幸运数。

这是实现:

#include<stdio.h>
#include<malloc.h>
#include<math.h>
#include <stdlib.h>
#include<string.h>
long long luckynumbers;
int primelist[1500];

int checklucky(long long possible,long long a,long long b){
    int prime =0;
    while(possible>0){
            prime+=pow((possible%10),(float)2);
            possible/=10;
    }
        if(primelist[prime]) return 1;
        else return 0;
}
long long getmax(int numdigits){
        if(numdigits == 0) return 1; 
        long long maxnum =10;
             while(numdigits>1){
                        maxnum = maxnum *10;
                        numdigits-=1;
             }
         return maxnum; 

}
void permuteandcheck(char *topermute,int d,long long a,long long b,int digits){
    if(d == strlen(topermute)){
            long long possible=atoll(topermute);
            if(possible >= getmax(strlen(topermute)-1)){  // to skip the case of getting already read numbers like 21 and 021(permuted-210

                if(possible >= a && possible <= b){

                    luckynumbers++;
                }
            }
    }
    else{
        char lastswap ='\0';
        int i;
        char temp;
        for(i=d;i<strlen(topermute);i++){
            if(lastswap == topermute[i])
                continue;
            else
                lastswap = topermute[i];
            temp = topermute[d];
            topermute[d] = topermute[i];
            topermute[i] = temp;

            permuteandcheck(topermute,d+1,a,b,digits);

            temp = topermute[d];
            topermute[d] = topermute[i];
            topermute[i] = temp;
        }

    }

}


void findlucky(long long possible,long long a,long long b,int digits){
    int i =0;
    if(checklucky(possible,a,b)){
        char topermute[18];
        sprintf(topermute,"%lld",possible);
        permuteandcheck(topermute,0,a,b,digits);
    }
}


void  partitiongenerator(int k,int n,int numdigits,long long  possible,long long a,long long b,int digits){
    if(k > n || numdigits > digits-1 || k > 9) return;
    if(k == n){

        possible+=(k*getmax(numdigits));

        findlucky(possible,a,b,digits);
        return;
    }
    partitiongenerator(k,n-k,numdigits+1,(possible + k*getmax(numdigits)),a,b,digits);
    partitiongenerator(k+1,n,numdigits,possible,a,b,digits);

}


void calcluckynumbers(long long a,long long b){
    int i;
    int numdigits = 0;
    long long temp = b;
    while(temp > 0){
        numdigits++;
        temp/=10;
    }

    long long maxnum =getmax(numdigits)-1;
    int maxprime=0,minprime =0;
    temp = maxnum;
    while(temp>0){
        maxprime+=(temp%10);
        temp/=10;
    }
    int start = 2;
    for(;start <= maxprime ;start++){
            if(primelist[start]) {
                partitiongenerator(0,start,0,0,a,b,numdigits);
            }
    }   

}   
void generateprime(){
    int i = 0;
    for(i=0;i<1500;i++)
        primelist[i] = 1;
    primelist[0] = 0;
    primelist[1] = 0;
    int candidate = 2;
    int topCandidate = 1499;
    int thisFactor = 2;
    while(thisFactor * thisFactor <= topCandidate){
        int  mark = thisFactor + thisFactor;
        while(mark <= topCandidate){
            *(primelist + mark) = 0;
            mark += thisFactor;
        }
        thisFactor++;
        while(thisFactor <= topCandidate && *(primelist+thisFactor) == 0) thisFactor++;
    }

}
int main(){
        char input[100];
        int cases=0,casedone=0;
    long long a,b;
    generateprime();
        fscanf(stdin,"%d",&cases);
        while(casedone < cases){
        luckynumbers = 0;
                fscanf(stdin,"%lld %lld",&a,&b);
        int i =0;
               calcluckynumbers(a,b);
                casedone++;
        }

}
算法速度太慢了。我认为可以根据数字的属性找到答案。请分享你的想法。谢谢。

算法速度太慢了。我认为可以根据数字的属性找到答案。请分享你的想法。谢谢。


1
错误:primelist的维度为1400,但您将其视为具有1500个维度。 - Paul R
我认为这个问题应该移动到http://codereview.stackexchange.com/。 - Renan Greinert
@Paul R,我认为这不是什么大问题。 - batbaatar
4
@batbaatar:你认为数组越界写入“不是什么大问题”??? - Paul R
2
@Muad'Dib:这不是作业。但它来自InterviewStreet.com网站。解决问题是一回事,而在他们分配的时间内解决问题则完全是另一回事。 - The111
显示剩余2条评论
10个回答

15

很棒的解决方案OleGG,不过您的代码并没有优化。我对您的代码进行了以下更改:

  1. 在count_lucky函数中,不需要遍历9*9*i次k,因为对于10000个案例,它会运行那么多次,我通过start和end减少了这个值。

  2. 我使用ans数组存储中间结果。虽然看起来不起眼,但在10000个案例中,这是降低时间的主要因素。

我已经测试了这段代码,并且它通过了所有的测试用例。以下是修改后的代码:

    #include <stdio.h>

    const int MAX_LENGTH = 18;
    const int MAX_SUM = 162;
    const int MAX_SQUARE_SUM = 1458;
    int primes[1460];
    unsigned long long dyn_table[20][164][1460];
    //changed here.......1
    unsigned long long ans[19][10][164][1460];  //about 45 MB

    int start[19][163];
    int end[19][163];
    //upto here.........1
    void gen_primes() {
        for (int i = 0; i <= MAX_SQUARE_SUM; ++i) {
            primes[i] = 1;
        }
        primes[0] = primes[1] = 0;

        for (int i = 2; i * i <= MAX_SQUARE_SUM; ++i) {
            if (!primes[i]) {
                continue;
            }
            for (int j = 2; i * j <= MAX_SQUARE_SUM; ++j) {
                primes[i*j] = 0;
            }
        }
    }

    void gen_table() {
        for (int i = 0; i <= MAX_LENGTH; ++i) {
            for (int j = 0; j <= MAX_SUM; ++j) {
                for (int k = 0; k <= MAX_SQUARE_SUM; ++k) {
                    dyn_table[i][j][k] = 0;
                }
            }
        }
        dyn_table[0][0][0] = 1;

        for (int i = 0; i < MAX_LENGTH; ++i) {
            for (int j = 0; j <= 9 * i; ++j) {
                for (int k = 0; k <= 9 * 9 * i; ++k) {
                    for (int l = 0; l < 10; ++l) {
                        dyn_table[i + 1][j + l][k + l*l] += dyn_table[i][j][k];
                    }
                }
            }
        }
    }

    unsigned long long count_lucky (unsigned long long maxp) {
        unsigned long long result = 0;
        int len = 0;
        int split_max[MAX_LENGTH];
        while (maxp) {
            split_max[len] = maxp % 10;
            maxp /= 10;
            ++len;
        }
        int sum = 0;
        int sq_sum = 0;
        unsigned long long step_result;
        unsigned long long step_;
        for (int i = len-1; i >= 0; --i) {
            step_result = 0;
            int x1 = 9*i;
            for (int l = 0; l < split_max[i]; ++l) {
    //changed here........2
                step_ = 0;
                if(ans[i][l][sum][sq_sum]!=0)
                    {
                        step_result +=ans[i][l][sum][sq_sum];
                        continue;
                    }
                int y = l + sum;
                int x = l*l + sq_sum;
                for (int j = 0; j <= x1; ++j) {
                    if(primes[j + y])
                        for (int k=start[i][j]; k<=end[i][j]; ++k) {
                            if (primes[k + x]) {
                                step_result += dyn_table[i][j][k];
                                step_+=dyn_table[i][j][k];
                            }
                    }

                }
                 ans[i][l][sum][sq_sum] = step_;
    //upto here...............2
            }
            result += step_result;
            sum += split_max[i];
            sq_sum += split_max[i] * split_max[i];
        }

        if (primes[sum] && primes[sq_sum]) {
            ++result;
        }

        return result;
    }

    int main(int argc, char** argv) {
        gen_primes();
        gen_table();

    //changed here..........3
        for(int i=0;i<=18;i++)
            for(int j=0;j<=163;j++)
                {
                    for(int k=0;k<=1458;k++)
                            if(dyn_table[i][j][k]!=0ll)
                                {
                                    start[i][j] = k;
                                    break;                               
                                }

                    for(int k=1460;k>=0;k--)
                            if(dyn_table[i][j][k]!=0ll)
                                {
                                    end[i][j]=k;
                                    break;                               
                                }
                }
    //upto here..........3
        int cases = 0;
        scanf("%d",&cases);
        for (int i = 0; i < cases; ++i) {
            unsigned long long a, b;

            scanf("%lld %lld", &a, &b);
    //changed here......4
            if(b == 1000000000000000000ll)
                b--;
    //upto here.........4
            printf("%lld\n", count_lucky(b) - count_lucky(a-1));
        }
        return 0;

}

解释:

gen_primes()和gen_table()基本上是不言自明的。

count_lucky()的工作方式如下:

将数字分割为split_max[],仅存储个位数,十位数,百位数等位置的单个数字。思路是:假设split_map[2] = 7,则我们需要计算以下结果:

百位数为1,个位数和十位数从00到99。

百位数为2,个位数和十位数从00到99。

百位数为7,个位数和十位数从00到99。

实际上,这在l循环中完成,其中涉及到已经预先计算的出现数字总和和平方和。对于此示例:总和将从0到9 * i变化,而平方和将从0到9 * 9 * i变化...这在j和k循环中完成。这适用于i循环中的所有长度

这是OleGG的想法。

为了优化,考虑以下内容:

  1. 没有必要将平方和从0到9*9*i运行,因为对于特定的数字总和,它不会达到完整范围。例如,如果i = 3且总和为5,则平方和不会从0到9 * 9 * 3变化。使用预先计算的值将此部分存储在start[]和end[]数组中。

  2. 对于特定位数和最高位数字以及达到特定总和和特定平方和的情况,存储了记忆中的值。虽然太长但仍然约为45 MB。 我相信这可以进一步优化。


1
恭喜,你做到了。我知道这个技巧必须存储中间结果,而且我甚至对这些结果如何表示原始数字有正确的想法(请参见我的答案,其中包含“00000-54321”的示例)。然而,我没有像你一样用[19][10][164][1460]数组正确实现它。太棒了……这个答案值得打勾并获得更多的赞。顺便说一句:你在k上所做的诡计几乎没有什么作用……最大的收益来自于ans[](20倍的收益)和将if(primes[j + y])移出循环(2倍的收益)。总共约为40倍的收益。 - The111
请忽略我上面的“20x”和“2x”注释。那些只对我运行的某个特定测试用例有意义。显然,这取决于测试用例的总数。无论如何,这仍然是最大的两个改进。 - The111
1
InterviewStreet应该是一个个人挑战!给予建议和帮助走上正轨是一回事,发布简单的代码解决方案则是另外一回事... - Benny
1
我认为提供完整的解决方案可以使你的想法更加具体。无论如何,比赛已经结束了,所以没有什么损失。这可能不太合适,但肯定是有帮助的。 - pirate

14

你应该使用DP来完成这个任务。这是我的解决方案:

#include <stdio.h>

const int MAX_LENGTH = 18;
const int MAX_SUM = 162;
const int MAX_SQUARE_SUM = 1458;
int primes[1459];
long long dyn_table[19][163][1459];

void gen_primes() {
    for (int i = 0; i <= MAX_SQUARE_SUM; ++i) {
        primes[i] = 1;
    }
    primes[0] = primes[1] = 0;

    for (int i = 2; i * i <= MAX_SQUARE_SUM; ++i) {
        if (!primes[i]) {
            continue;
        }
        for (int j = 2; i * j <= MAX_SQUARE_SUM; ++j) {
            primes[i*j] = 0;
        }
    }
}

void gen_table() {
    for (int i = 0; i <= MAX_LENGTH; ++i) {
        for (int j = 0; j <= MAX_SUM; ++j) {
            for (int k = 0; k <= MAX_SQUARE_SUM; ++k) {
                dyn_table[i][j][k] = 0;
            }
        }
    }
    dyn_table[0][0][0] = 1;

    for (int i = 0; i < MAX_LENGTH; ++i) {
        for (int j = 0; j <= 9 * i; ++j) {
            for (int k = 0; k <= 9 * 9 * i; ++k) {
                for (int l = 0; l < 10; ++l) {
                    dyn_table[i + 1][j + l][k + l*l] += dyn_table[i][j][k];
                }
            }
        }
    }
}

long long count_lucky (long long max) {
            long long result = 0;
    int len = 0;
    int split_max[MAX_LENGTH];
    while (max) {
        split_max[len] = max % 10;
        max /= 10;
        ++len;
    }
    int sum = 0;
    int sq_sum = 0;
    for (int i = len-1; i >= 0; --i) {
        long long step_result = 0;
        for (int l = 0; l < split_max[i]; ++l) {
            for (int j = 0; j <= 9 * i; ++j) {
                for (int k = 0; k <= 9 * 9 * i; ++k) {
                    if (primes[j + l + sum] && primes[k + l*l + sq_sum]) {
                        step_result += dyn_table[i][j][k];
                    }
                }
            }
        }
        result += step_result;

        sum += split_max[i];
        sq_sum += split_max[i] * split_max[i];
    }

    if (primes[sum] && primes[sq_sum]) {
        ++result;
    }

    return result;
}

int main(int argc, char** argv) {
    gen_primes();
    gen_table();

    int cases = 0;
    scanf("%d", &cases);
    for (int i = 0; i < cases; ++i) {
        long long a, b;
        scanf("%lld %lld", &a, &b);
        printf("%lld\n", count_lucky(b) - count_lucky(a-1));
    }
    return 0;
}

简要说明:

  • 使用埃拉托斯特尼筛选法来计算所有小于9 * 9 * MAX_LENGTH的素数;
  • 然后使用DP,构建table dyn_table,其中dyn_table[i][j][k]中的值为X,意味着我们恰好有X个长度为i,数字和等于j,平方和等于k的数字;
  • 然后我们可以很容易地计算从1到999..999(即9的len次方)的幸运数字的数量。对此,我们只需将所有dyn_table[len][j][k],其中jk都是素数的值相加。
  • 要计算从1到随机X的幸运数字的数量,我们将从1到X的区间分为长度为10^K的区间(参见*count_lucky*函数)。
  • 最后一步是从count_lucky(b)中减去count_lucky(a-1) (因为我们在区间中包含了a)

这就是全部内容。O(log(MAX_NUMBER)^3)的预处理工作,每个步骤的复杂度也是如此。

我已经测试过我的解决方案与线性直接的解决方案的结果相等。


1
这是一个非常聪明的解决方案。不幸的是,它并不能100%工作,因为你不能把每个数字都分解成10的幂。我有一个不同的解决方案,也需要类似的拆分,但找不到任何方法来拆分像1234567这样的数字,虽然1100111可以很好地分解成10的幂。大多数数字分解成10的幂的倍数,这在您的表格中并不完全适用。 - The111
1
谢谢您的评论,我发现了我的算法中的一个bug,所以我进行了更新。现在我已经在从1到一百万的所有数字上测试过它,并且结果与直接解决方案相同。循环for (int l = 0; l < split_max[i]; ++l)不仅允许我们处理像1100111这样的数字,而且可以处理所有数字-我考虑到拆分部分中的第一个数字不仅可以是0或1。 - OleGG
谢谢你的更新。我本来想自己设计一个类似的算法,但是你的更好。 :-) - The111
1
是的,我已经为单个查询的情况完成了我的解决方案,你的解决方案对于多个查询处理得更好,所以我给你加一分 =) - OleGG
1
但速度还不够快。我已经绞尽脑汁好几天了,试图找到更好的优化方法。这个谜题真是让人又沮丧又着迷! - The111
显示剩余2条评论

4

不要枚举数字的空间,而是枚举幸运数字的不同“签名”,然后打印所有这些组合。

可以使用简单的回溯算法来实现:

#define _GNU_SOURCE
#include <assert.h>
#include <limits.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>

#define bitsizeof(e)   (CHAR_BIT * sizeof(e))
#define countof(e)     (sizeof(e) / sizeof((e)[0]))
#define BITMASK_NTH(type_t, n) ( ((type_t)1) << ((n) & (bitsizeof(type_t) - 1)))
#define OP_BIT(bits, n, shift, op) \
    ((bits)[(unsigned)(n) / (shift)] op BITMASK_NTH(typeof(*(bits)), n))
#define TST_BIT(bits, n)    OP_BIT(bits, n, bitsizeof(*(bits)), &  )
#define SET_BIT(bits, n)    (void)OP_BIT(bits, n, bitsizeof(*(bits)), |= )

/* fast is_prime {{{ */

static uint32_t primes_below_1M[(1U << 20) / bitsizeof(uint32_t)];

static void compute_primes_below_1M(void)
{
    SET_BIT(primes_below_1M, 0);
    SET_BIT(primes_below_1M, 1);
    for (uint32_t i = 2; i < bitsizeof(primes_below_1M); i++) {
        if (TST_BIT(primes_below_1M, i))
            continue;
        for (uint32_t j = i * 2; j < bitsizeof(primes_below_1M); j += i) {
            SET_BIT(primes_below_1M, j);
        }
    }
}

static bool is_prime(uint64_t n)
{
    assert (n < bitsizeof(primes_below_1M));
    return !TST_BIT(primes_below_1M, n);
}

/* }}} */

static uint32_t prime_checks, found;

static char     sig[10];
static uint32_t sum, square_sum;

static void backtrack(int startdigit, int ndigits, int maxdigit)
{
    ndigits++;

    for (int i = startdigit; i <= maxdigit; i++) {
        sig[i]++;
        sum        += i;
        square_sum += i * i;
        prime_checks++;
        if (is_prime(sum) && is_prime(square_sum)) {
                found++;
        }
        if (ndigits < 18)
            backtrack(0, ndigits, i);
        sig[i]--;
        sum        -= i;
        square_sum -= i * i;
    }
}

int main(void)
{
    compute_primes_below_1M();
    backtrack(1, 0, 9);

    printf("did %d signature checks, found %d lucky signatures\n",
           prime_checks, found);
    return 0;
}

当我运行它时,它会执行以下操作:

$ time ./lucky
did 13123091 signature checks, found 933553 lucky signatures
./lucky  0.20s user 0.00s system 99% cpu 0.201 total

你需要生成由该数字构建的所有不同数字排列,而不是使用found ++。我还预先计算了前1M个质数。 我没有检查代码是否100%正确,您可能需要对其进行调试。但是大致思路在这里,我能够在0.2秒内生成所有幸运排列(即使没有错误,速度也不应超过两倍)。当然,你想生成满足A <= B条件的排列。你可能还想忽略生成具有比B更多或少于A位数的分区。无论如何,你可以从我的一般想法中提高。 (注意:开头的简介是因为我剪切和粘贴了我为项目欧拉编写的代码,因此对于N <= 1M有效的非常快的is_prime;))

是的,我想这就是我所说的。 - MK.

3
对于那些还不知道的人,这是InterviewStreet.com网站上的一个问题(在我看来,那里最难的一个)。我的方法起初与OleGG下面的方法类似(并且受到了他的启发)。然而,在创建了他所创建的第一个[19] [163] [1459]表(我将其称为table1)之后,我朝稍微不同的方向发展。我创建了第二个ragged长度为[19] [x] [3]的表格(table2),其中x是相应数量数字的唯一求和对数。对于表格的第三个维度,长度为3,第1个元素是具有由第2个和第3个元素保持的sum和squareSum值的唯一“sum pairs”的数量。
例如:
//pseudocode

table2[1] = new long[10][3]
table2[1] = {{1, 0, 0}, {1, 1, 1}, {1, 2, 4},
             {1, 3, 9}, {1, 4, 16}, {1, 5, 25},
             {1, 6, 36}, {1, 7, 49}, {1, 8, 64}, {1, 9, 81}}

table2[2] = new long[55][3]
table2[3] = new long[204][3]
table2[4] = new long[518][3]
    .
    .
    .
    .
table2[17] = new long[15552][3]
table2[18] = new long[17547][3]

数组的第二个维度长度(10、55、204、518、……、15552、17547)可以通过查询table1来验证,类似的方式可以填充table2。现在,使用table2,我们可以比OleGG发布的方法更快地解决大型“幸运数”查询,尽管仍然采用了类似于他的“分割”过程。例如,如果您需要查找lucky(00000-54321)(即0到54321之间的幸运数),它被拆分为以下5行的总和:

lucky(00000-54321) = {
    lucky(00000-49999) +
    lucky(50000-53999) +
    lucky(54000-54299) +
    lucky(54300-53319) +
    lucky(54320-54321)
}

进一步分解如下:

lucky(00000-49999) = {
    lucky(00000-09999) +
    lucky(10000-19999) +
    lucky(20000-29999) +
    lucky(30000-39999) +
    lucky(40000-49999)
}
    .
    .
lucky(54000-54299) = {
    lucky(54000-54099) +
    lucky(54100-54199) +
    lucky(54200-54299)
}
    .
    .
    .
    etc

通过查询table2表格,可以轻松地获取这些值。例如,lucky(40000-49999)是通过将第三个维度的table2表格的第二个和第三个元素相加得到4和16:

sum = 0
for (i = 0; i < 518; i++)
    if (isPrime[table2[4][i][1] + 4] && isPrime[table2[4][i][2] + 4*4])
        sum += table2[4][i][0]
return sum

或者对于幸运数字(54200-54299):

sum = 0
for (i = 0; i < 55; i++)
    if (isPrime[table2[2][i][1] + (5+4+2)]
    && isPrime[table2[2][i][2] + (5*5+4*4+2*2)])
        sum += table2[2][i][0]
return sum

现在,OleGG的解决方案比我之前尝试过的任何其他方案都要快得多,但是通过上述修改,它的性能甚至比以前更好(对于大型测试集,性能提高了约100倍)。然而,它仍然远远不够快,无法通过InterviewStreet给出的盲目测试用例。通过一些巧妙的技巧,我能够确定我当前运行的速度比完成他们的测试集所需的时间慢了约20倍。然而,我找不到更多的优化方法。明显,这里最耗费时间的是迭代table2的第二维,避免这种情况的唯一方法是制表这些和的结果。然而,在规定的时间(5秒)内计算所有可能性或将它们全部存储在给定的空间(256MB)中都是不可能的。例如,上面的lucky(54200-54299)循环可以预先计算并存储为单个值,但如果是这样,我们还需要预先计算lucky(123000200-123000299)和lucky(99999200-99999299),等等。我已经做过计算,预先计算的计算量太大了。

1

我刚刚解决了这个问题。

这只是一个动态规划的问题。将DP[n](sum-square_sum)作为DP函数,DP[n](sum-square_sum)表示数字位数小于等于n,数字的和为sum,平方和为square_sum的所有数字的计数。例如:

DP[1](1-1) = 1      # only 1 satisfies the condition                        
DP[2](1-1) = 2      # both 1 and 10 satisfies the condition                        
DP[3](1-1) = 3      # 1 10 100
DP[3](2-4) = 3      # 11 110 101

由于我们可以轻松地找出第一个DP状态DP [1] [..] [..],因此它是:

(0-0) => 1     (1-1) => 1    (2-4) => 1     (3-9) => 1     (4-16) => 1    
(5-25) => 1    (6-36) => 1   (7-49) => 1    (8-64) => 1    (9-81) => 1

然后我们可以从DP[1]推导出DP[2],再推导出DP[3]......DP[18]。 上述推导是基于每次n增加1时(例如从DP[1]到DP[2]),我们都会得到一个新的数字(0..9),并且(sum, square_sum)对(i.e. DP[n])必须更新。 最后,我们可以遍历DP[18]集合并计算幸运数字的数量。
那么,上述算法的时间和空间复杂度如何呢? 我们知道sum <= 18*9=162,square_sum <= 18*9*9 = 1458,因此(sum, square_sum)对的集合(i.e. DP[n])非常小,小于162*1458=236196,实际上比236196要小得多; 事实是:我的Ruby程序在不到1秒的时间内计算出0到10^18之间所有幸运数字的数量。
ruby lucky_numbers.rb  0.55s user 0.00s system 99% cpu 0.556 total

我通过编写一个使用暴力算法的测试函数来测试我的程序,在小于10^7的数字上测试结果正确。


0

根据需求,可以用不同的方式来完成。如果我要做这个,我会使用“埃拉托斯特尼筛法”计算所需范围内(A到(9*2)*B.length)的质数,并将其缓存(再次取决于您的设置,可以使用内存或磁盘缓存),以供下一次运行时使用。

我刚编写了一个快速解决方案(Java),如下所示(注意: 没有检查整数溢出。 这只是一个快速示例。 我的代码也没有经过优化。):

import java.util.ArrayList;
import java.util.Arrays;

public class LuckyNumbers {
    public static void main(String[] args) {
        int a = 0, b = 1000;
        LuckyNumbers luckyNums = new LuckyNumbers();
        ArrayList<Integer> luckyList = luckyNums.findLuckyNums(a, b);
        System.out.println(luckyList);
    }

    private ArrayList<Integer> findLuckyNums(int a, int b) {
        ArrayList<Integer> luckyList = new ArrayList<Integer>();
        int size = ("" + b).length();        
        int maxNum = 81 * 4; //9*2*b.length() - 9 is used, coz it's the max digit
        System.out.println("Size : " + size + " MaxNum : " + maxNum);
        boolean[] primeArray = sieve(maxNum);

        for(int i=a;i<=b;i++) {
            String num = "" + i;
            int sumDigits = 0;
            int sumSquareDigits = 0;

            for(int j=0;j<num.length();j++) {
                int digit = Integer.valueOf("" + num.charAt(j));
                sumDigits += digit;
                sumSquareDigits += Math.pow(digit, 2);
            }

            if(primeArray[sumDigits] && primeArray[sumSquareDigits]) {
                luckyList.add(i);
            }
        }

        return luckyList;
    }

    private boolean[] sieve(int n) {
        boolean[] prime = new boolean[n + 1];
        Arrays.fill(prime, true);
        prime[0] = false;
        prime[1] = false;
        int m = (int) Math.sqrt(n);

        for (int i = 2; i <= m; i++) {
            if (prime[i]) {
                for (int k = i * i; k <= n; k += i) {
                    prime[k] = false;
                }
            }
        }

        return prime;
    }
}

输出结果为:

[11、12、14、16、21、23、25、32、38、41、49、52、56、58、61、65、83、85、94、101、102、104、106、110、111、113、119、120、131、133、137、140、146、160、164、166、173、179、191、197、199、201、203、205、210、223、229、230、232、250、289、292、298、302、308、311、313、317、320、322、331、335、337、344、346、353、355、364、368、371、373、377、379、380、386、388、397、401、409、410、416、434、436、443、449、461、463、467、476、490、494、502、506、508、520、533、535、553、559、560、566、580、595、601、605、610、614、616、634、638、641、643、647、650、656、661、665、674、683、689、698、713、719、731、733、737、739、746、764、773、779、791、793、797、803、805、829、830、836、838、850、863、869、883、892、896、904、911、917、919、922、928、937、940、944、955、968、971、973、977、982、986、991]


3
很遗憾,当 (B - A) -> 10^18 时,你的算法仍然会很慢,因为它仍然是线性的。 - OleGG
1
在我的实现中,我使用了埃拉托色尼筛法。这很简单。需要优化的主要是遍历所有数字并计算它们的数字之和,因为数字范围从1到10^18。 - vgeta
质数的范围实际上要小得多。 - MK.
我同意这也比较慢。也许可以通过使用多台机器和MapReduce框架来加速?或者对于在单台机器上运行的程序,是否存在其他更快的解决方案? - bchetty
不,没有并行处理,只有一台机器。我相信秘密在于数字的属性。 - vgeta
秘密在于使用DP(动态规划)。我将在几分钟内展示我的解决方案,我认为它会比O(N)更快。 - OleGG

0
有时候最快的解决方案非常简单:
uint8_t precomputedBitField[] = {
    ...
};

bool is_lucky(int number) {
    return precomputedBitField[number >> 8] & (1 << (number & 7));
}

只需修改您现有的代码以生成“precomputedBitField”即可。

如果您担心大小,为了覆盖从0到999的所有数字,它只会花费您125个字节,因此这种方法可能比任何其他替代方法更小(而且速度更快)。


位运算在所有算法复杂度中几乎没有任何开销。此外,质数数组可以轻松地存储到处理器的缓存中,并且从中获取数字甚至比在数组上执行操作更快。 - OleGG
1
抱歉。我无法理解这个解决方案。你可以详细说明一下吗?谢谢。 - vgeta
这不是一个新的解决方案,而是对现有解决方案进行更改,使质数数组更好地适应内存。 对于我们的情况,这并不重要。 - OleGG

0
我正在尝试使用 Pierre 的枚举方法来解决方案,但是没有找到足够快速计算排列的方法。 OleGG 的计数方法非常聪明,而 pirate 的优化则是使其足够快的必要条件。 我想出了一个小改进和一种解决严重问题的方法。
首先,这个改进:您不必一个一个地遍历所有的和与平方和来检查 pirate 的 j 和 k 循环中的质数。您已经拥有(或可以轻松生成)质数列表。 如果您使用其他变量来确定哪些质数在范围内,您只需要遍历适合 sum 和 squaresum 的质数列表。 一个包含质数的数组和用于快速确定大于等于某个数字的质数索引的查找表是有帮助的。但是,这可能只是一个相对较小的改进。
最大的问题在于 pirate 的答案缓存数组。它并不像声称的那样仅占用 45MB;以 64 位条目为例,它约为 364MB。这超出了 C 和 Java 目前允许的内存限制。通过摆脱“l”维度,可以将其减少到 37MB,这是不必要的,并且会影响缓存性能。您真正感兴趣的是对 l + sum 和 l*l + squaresum 进行缓存计数,而不是单独缓存 l、sum 和 squaresum。

0
我还没有仔细分析你目前的解决方案,但这可能会对其进行改进:
由于数字的顺序并不重要,您应该遍历所有可能的数字0-9的长度为1到18的组合,跟踪数字和它们的平方的总和,并逐个添加一个数字,使用先前计算的结果。
因此,如果您知道对于12,数字的总和为3,平方和为5,那么请查看数字120、121、122...等,从数字12的3和5轻松地计算它们的总和。

整数分割算法可以实现这一点。而且每个分区也会被排列。 - vgeta
1
等等,你为什么需要排列?你应该能够使用公式计算排列的数量吧? - MK.
取11,和为2。110和101也产生相同的和。 - vgeta
3
你只需要看11和110,因为你可以在按顺序枚举数字时始终保留你的数字。如果你按顺序生成数字组合,那么就没有必要同时看110和101。然后一旦你知道110是好的,你将通过这3个数字可能的排列数量增加计数(注意排除具有前导0的排列)。 - MK.

-2
首先,我想说可以通过筛法计算幸运数字,关于筛法的解释可以在这里找到:http://en.wikipedia.org/wiki/Lucky_number 因此,您可以使用筛法来确定数字,从而提高解决方案的速度。

我认为筛法生成的“幸运数字”不是我要找的“幸运数字”。如果一个数字的各位数字之和以及各位数字的平方和都是质数,那么这个数字被称为“幸运数字”。 - vgeta

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接