Project Euler Problem 23 - 2つの過剰数の和ではない自然数の和

自分自身を除く約数の和が自分自身に等しい自然数完全数と呼ばれる(e.g. 6 = 1 + 2 + 3)が、そうでないものについて、不足数過剰数という概念がある。

  • 不足数: 自分自身を除く約数の和が自分自身より小さい自然数
  • 過剰数: 自分自身を除く約数の和が自分自身より大きい自然数

ある自然数を2つの過剰数の和で表すことを考える。例えば12は最小の過剰数(1 + 2 + 3 + 4 + 6 = 16)なので、2つの過剰数で表せる最小の自然数は24だ。

ところで、このような自然数は無限にある。それどころか、28123より大きな自然数全て2つの過剰数の和として表すことができることが知られている。

では、過剰数の和として表すことができない自然数の総和を求めよ、というのがこの問題。

最初bruteforceでやって約80秒だった。forum覗いて少し良いやり方を見つけたものの、劇的な改善はせず38秒。そしてやり方はわかるもののなぜこれで正しい答えが出るのかも理解しきれていない。計算時間については、他の言語の様子を見ると普通にネストしたfor回して200msとかになってるし、まあ仕方ないかなという気持ちもある。

ところで、28123という上限は20161まで下げられる。Wikipediaにも書いてある(過剰数 - Wikipedia)。ループがネストしているので計算量は2/3より大きく減らせて、計算時間は20秒になった。

以下解答。

# limit = 20161
limit = 28123
abdnumlist <- (1:limit)[sapply(1:limit, function(n) sum((1:n)[n %% (1:n) == 0]) - n) > 1:limit]

sumofadb <- function(n){
  low = 1
  high = length(abdnumlist)
  sum = abdnumlist[low] + abdnumlist[high]
  while(low < high && sum != n){
    if(sum > n){
      high <- high - 1
    } else {
      low <- low + 1
    }
    sum = abdnumlist[low] + abdnumlist[high]
  }
  return(sum == n)
}
sum((1:limit)[!sapply(1:limit, sumofadb)])
end = Sys.time()

追記

python3

python3でやったら28123までで34秒、20161までで19秒でRと大差なし(書き方悪いのかもしれないけど)。sympy使わないともっと遅い。

# coding: utf-8
import time
import sympy as sym

limit = 20161
#limit = 28123
#limit = 1000
start = time.time()

def is_adbnum(n):
    if(sum(sym.divisors(n)) > 2*n):
        return True
    else:
        return False

adbnumlist = [i for i in range(1, limit+1) if is_adbnum(i)]

def is_sumofadb(n):
    low = 0
    high = len(adbnumlist)-1
    s = adbnumlist[low] + adbnumlist[high]
    while low < high and s != n:
        if s > n:
            high -= 1
        else:
            low += 1
        s = adbnumlist[low] + adbnumlist[high]
    return s == n

print(sum([i for i in range(1, limit+1) if not is_sumofadb(i)]))

print(time.time() - start)

julia

試しにjuliaで書いてみたけど28123までで33秒、20161までで17秒。書き方が悪いのかもと思って色々試してみたけど改善できず。定数にconst宣言つけるだけで28123までで5秒になった。処理を関数にすると早いという話も聞いたが、今回の例ではconst宣言が最も効果があった。

const limit = 28123
# limit = 20161

function divsum(n)
  return sum([i for i in 1:(n-1) if n % i == 0])
end

function make_abdnumlist(lim)
  return [i for i in 1:lim if divsum(i) > i]
end

const abdnumlist = make_abdnumlist(limit)

function is_not_sumobadb(n)::Bool
  low = 1
  high = length(abdnumlist)
  s = abdnumlist[low] + abdnumlist[high]
  while low < high && s != n
    if s > n
      high -= 1
    else
      low += 1
    end
    s = abdnumlist[low] + abdnumlist[high]
  end
  return s != n
end

function solve_problem(lim)
  sum([i for i in 1:lim if is_not_sumobadb(i)])
end

@show solve_problem(limit)

C++

C++で書いてみたら28123までで1.8秒、20161までで0.9秒だった…

#include <cstdio>

using namespace std;

const int LIMIT = 28123;

int divsum(int n){
  int result = 0;

  for (int i = 1; i < n; i++) {
    if (n % i == 0){
      result += i;
    }
  }

  return result;
}


bool is_not_sumofadb(int n, int *adbnumlist, int index){
  int low = 0;
  int high = index;
  int s = adbnumlist[low] + adbnumlist[high-1];
  while(low < high & s != n){
    if(s > n){
      high--;
    } else {
      low++;
    }
    s = adbnumlist[low] + adbnumlist[high];
  }
  return s != n;
}

int main(){
  int n = 12;
  int ans = 0;
  int adbnumlist[LIMIT];
  int index = 0;

  for(int i = 1; i <= LIMIT; i++){
    if(divsum(i) > i){
      adbnumlist[index] = i;
      index++;
    }
  }

  for(int i = 1; i <= LIMIT; i++){
    if(is_not_sumofadb(i, adbnumlist, index)){
      ans += i;
    }
  }

  printf("%d\n", ans);

}

さらに追記

コメントで教えていただいた方法に少し手を加えたら0.5秒程度にまで短縮できた。

limit <- 28123

#temp[i]がiの約数の総和になるようにする
temp <- rep(0, limit)
for(i in 1:limit){
  temp[(1:(limit %/% i)) * i] <- temp[(1:(limit %/% i)) * i] + i
}
abdnumlist <- (1:limit)[temp > 2 * (1:limit)]

sieve = logical(limit*2)
# ちょうど2つの過剰数で表せる自然数のindexをTRUEに
for(i in seq_along(abdnumlist)){
  sieve[abdnumlist[1:i] + abdnumlist[i]] <- TRUE
}
sieve <- sieve[1:limit]
sum((1:limit)[!sieve])