トップページに戻る
次のC#のサンプルへ
前のC#のサンプルへ
Cマガ電脳クラブ(第053回) 平方数の逆襲
問題
異なる平方数の逆数の和で、2分の1を作る。
平方数とは自然数を2乗したもので、1、4、9、16・・・のことである。式で表すと
n
シグマ( 1/ (a(I)の2乗) ) = 0.5
I=1
となる。ただし、a(I)は自然数で、
1 <= a(1) < a(2) < ・・・ a(n)
である。
これだけだと何通りもの表し方があるが、このうちでa(n)の値が最小となるものを求めてほしい。
ヒント:加える個数は11個以下。余裕のある人はヒントを利用せずに求めてください。
ソース
using System;
using System.Collections.Generic;
class Program
{
const int NJyougen = 35; //Nの上限
//解の下限と上限
const decimal AnswerKagen = 0.4999999999999999999M;
const decimal AnswerJyougen = 0.5000000000000000001M;
struct JyoutaiDef
{
internal int CurrP;
internal decimal SumVal;
internal List<int> BunboList;
}
static void Main()
{
var sw = System.Diagnostics.Stopwatch.StartNew();
Console.WriteLine("{0}の2乗をa(n)の上限として検証します", NJyougen);
int[] HeihouSuuArr = new int[NJyougen + 1];
for (int I = 2; I <= NJyougen; I++)
HeihouSuuArr[I] = I * I;
decimal[] GyakuSuuArr = new decimal[NJyougen + 1];
for (int I = 2; I <= NJyougen; I++)
GyakuSuuArr[I] = 1M / (decimal)HeihouSuuArr[I];
DeriveRunSum(GyakuSuuArr);
var Stk = new Stack<JyoutaiDef>();
JyoutaiDef WillPush;
for (int I = 2; I <= NJyougen; I++) {
//添字以降の逆数の総和が、解の下限未満ならBreak
if (mRunSumArr[I] < AnswerKagen) break;
WillPush.CurrP = I;
WillPush.SumVal = GyakuSuuArr[I];
WillPush.BunboList = new List<int>() { HeihouSuuArr[I] };
Stk.Push(WillPush);
}
while (Stk.Count > 0) {
JyoutaiDef Popped = Stk.Pop();
//解の下限以上、かつ、解の上限以下であることが必要条件
if (AnswerKagen <= Popped.SumVal && Popped.SumVal <= AnswerJyougen) {
//真の値が0.5であることが十分条件
if (IsAnswer(Popped.BunboList)) {
Console.WriteLine("解候補を発見");
PrintAnswer(Popped.BunboList);
}
}
//添字以降の逆数の総和を足しても、解の下限未満なら枝切り
int ValidMinInd = Popped.CurrP + 1;
for (int I = Popped.CurrP + 1; I <= NJyougen; I++) {
if (Popped.SumVal + mRunSumArr[I] < AnswerKagen) break;
ValidMinInd = I;
}
for (int I = NJyougen; I >= Popped.CurrP + 1; I--) {
if (ValidMinInd < I) continue;
WillPush.CurrP = I;
WillPush.SumVal = Popped.SumVal + GyakuSuuArr[I];
WillPush.BunboList = new List<int>(Popped.BunboList);
WillPush.BunboList.Add(HeihouSuuArr[I]);
//解の上限を超えたら枝切り
if (WillPush.SumVal > AnswerJyougen) break;
Stk.Push(WillPush);
}
}
Console.WriteLine("経過時間={0}", sw.Elapsed);
}
//単位分数の分子のListを引数として、真の値が0.5かを判定
static bool IsAnswer(List<int> pBunboList)
{
long CurrBunsi = 0;
long CurrBunbo = 1;
foreach (int EachTaniBunsuuBunbo in pBunboList) {
CurrBunsi = CurrBunsi * EachTaniBunsuuBunbo + CurrBunbo;
CurrBunbo *= EachTaniBunsuuBunbo;
}
if (CurrBunbo % CurrBunsi > 0) return false;
return CurrBunbo / CurrBunsi == 2;
}
//添字以降の逆数の総和を求める
static decimal[] mRunSumArr = new decimal[NJyougen + 1];
static void DeriveRunSum(decimal[] pGyakuSuuArr)
{
decimal RunSum = 0M;
for (int I = NJyougen; 2 <= I; I--) {
RunSum += pGyakuSuuArr[I];
mRunSumArr[I] = RunSum;
}
}
//平方根を求める
static int GetHeihouKon(int pVal)
{
return (int)Math.Sqrt(pVal);
}
//解を出力
static void PrintAnswer(List<int> pIntList)
{
var sb = new System.Text.StringBuilder();
sb.Append("1/2 = ");
for (int I = 0; I <= pIntList.Count - 1; I++) {
sb.AppendFormat("1/({0}の2乗)", GetHeihouKon(pIntList[I]));
if (I < pIntList.Count - 1) {
if (I % 5 == 4) {
sb.AppendLine();
sb.Append(" ");
}
sb.Append(" + ");
}
}
Console.WriteLine(sb.ToString());
}
}
実行結果
35の2乗をa(n)の上限として検証します
解候補を発見
1/2 = 1/(2の2乗) + 1/(3の2乗) + 1/(4の2乗) + 1/(5の2乗) + 1/(7の2乗)
+ 1/(12の2乗) + 1/(15の2乗) + 1/(20の2乗) + 1/(28の2乗) + 1/(35の2乗)
経過時間=00:00:53.1619881
解説
a(n)の上限を決めてから深さ優先探索してます。
Decimal型の有効桁は、28桁ですので
Decimal型の単位分数40個の加算で、真の値が0.5ならば、
0.5- (10の-28乗)*40 <= 計算結果 >= 0.5 + (10の-28乗)*40
を満たします。(加算において、累計誤差は加算されるため)
上記の不等式を満たせば、
0.4999999999999999999 <= 計算結果 >= 0.5000000000000000001
も満たすので、
真の値が0.5ならば、
0.4999999999999999999 <= 計算結果 >= 0.5000000000000000001
を導出でき、この命題の対偶は、真の値が0.5である必要条件となります。
そして、必要条件を満たした分母の組み合わせの、
真の値が0.5かを調べて、十分性を検証してます。