トップページに戻る    次のC#のサンプルへ    前のC#のサンプルへ

Cマガ電脳クラブ(第024回) 数々の事情

問題

ここに10個の整数がある。仮に、
 23 32 45 51 53 70 98 116 296 332
としよう。この中の9個を選んで足すと、
 23+32+45+51+53+70+98+116+296=784
となり、この784は28の2乗、つまり平方数 (ある整数の2乗になっている整数) である。
では、ほかの9個ではどうだろうか。
 23+32+45+51+53+70+98+116+332=820
この820は平方数ではない。

さて、10個の正の整数のうち、
どの9個を選んで合計しても平方数になるような10個の数の組み合わせの中で、
10個の数の合計がいちばん小さくなるものを求めよ。
なお、この10個の数はすべて異なる数でなければならない。


ソース

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static int MaxSumOfAToJ = 8700; //10個の数の和の上限
    static int[] HeihouSuuArr; //MaxSumOfAToJ以下の平方数の配列
    static int[] HeihouDiffArr; //平方数同士の差の配列

    static void Main()
    {
        Console.WriteLine("10個の数の和を、{0}以下として検証します", MaxSumOfAToJ);
        var sw = System.Diagnostics.Stopwatch.StartNew();

        IntiSyori(); //初期処理

        for (int I = 10 * (10 + 1) / 2; I <= MaxSumOfAToJ; I++) {
            bool IsFoundAnswer;
            ExecKensyou(I, out IsFoundAnswer);
            if (IsFoundAnswer) break;
        }
        Console.WriteLine();
        Console.WriteLine("経過時間={0}", sw.Elapsed);
    }

    //初期処理
    static void IntiSyori()
    {
        var HeihouSuuList = new List<int>();
        var HeihouDiffList = new List<int>();
        for (int I = 1; I * I <= MaxSumOfAToJ; I++) {
            //平方数は、1+2+3+4+5+6+7+8+9以上
            if (I * I < 9 * (9 + 1) / 2) continue;
            HeihouSuuList.Add(I * I);

            for (int J = 0; J <= HeihouSuuList.Count - 1 - 1; J++) {
                HeihouDiffList.Add(I * I - HeihouSuuList[J]);
            }
        }
        HeihouSuuArr = HeihouSuuList.ToArray();
        HeihouDiffArr = HeihouDiffList.Distinct().OrderBy(X => X).ToArray();
    }

    struct JyoutaiDef
    {
        internal int CurrVal;
        internal List<int> ValList;
    }

    //SumOfAToJを引数として、解を検証
    static void ExecKensyou(int pSumOfAToJ, out bool pIsFoundAnswer)
    {
        pIsFoundAnswer = false;
        Console.WriteLine("10個の数の和を、{0}として検証中", pSumOfAToJ);

        var stk = new Stack<JyoutaiDef>();
        JyoutaiDef WillPush;

        for (int I = 1; I <= pSumOfAToJ; I++) {
            //等差数列の和 = (初項+末項)*項数/2
            int wkSum = (I + I + 9) * 10 / 2;
            if (pSumOfAToJ < wkSum) break;

            if (Array.BinarySearch(HeihouSuuArr, pSumOfAToJ - I) < 0) continue;

            WillPush.CurrVal = I;
            WillPush.ValList = new List<int>() { I };
            stk.Push(WillPush);
        }

        while (stk.Count > 0) {
            JyoutaiDef Popped = stk.Pop();

            int Cnt = Popped.ValList.Count;
            int CurrSum = Popped.ValList.Sum();

            if (Cnt == 10) {
                Console.WriteLine("解候補を発見");
                PrintAnswer(Popped.ValList.ToArray());
                pIsFoundAnswer = true;
                continue;
            }

            for (int I = 0; I <= HeihouDiffArr.GetUpperBound(0); I++) {
                int NextVal = Popped.CurrVal + HeihouDiffArr[I];
                if (Cnt == 9) {
                    if (CurrSum + NextVal < pSumOfAToJ) continue;
                    if (CurrSum + NextVal > pSumOfAToJ) break;
                }
                else {
                    //残りの数個を加算した場合に、和が上限を超えたら枝切り
                    int RestCnt = 10 - Cnt;
                    if (CurrSum + NextVal * RestCnt > pSumOfAToJ) break;
                }
                if (Array.BinarySearch(HeihouSuuArr, pSumOfAToJ - NextVal) < 0) continue;

                WillPush.CurrVal = NextVal;
                WillPush.ValList = new List<int>(Popped.ValList) { NextVal };
                stk.Push(WillPush);
            }
        }
    }

    //解を出力
    static void PrintAnswer(int[] pAToJ)
    {
        int wkSum = pAToJ.Sum();

        //平方根を求める
        Func<int, int> GetHeihouKon = (pVal) =>
        {
            return (int)Math.Sqrt(pVal);
        };

        int A = pAToJ[0];
        int B = pAToJ[1];
        int C = pAToJ[2];
        int D = pAToJ[3];
        int E = pAToJ[4];
        int F = pAToJ[5];
        int G = pAToJ[6];
        int H = pAToJ[7];
        int I = pAToJ[8];
        int J = pAToJ[9];

        string wkStr = "A={0},B={1},C={2},D={3},E={4},F={5},G={6},H={7},I={8},J={9}";
        Console.WriteLine(wkStr, A, B, C, D, E, F, G, H, I, J);
        Console.WriteLine("A+B+C+D+E+F+G+H+I   = {0} ({1}の2乗)", wkSum - J, GetHeihouKon(wkSum - J));
        Console.WriteLine("A+B+C+D+E+F+G+H  +J = {0} ({1}の2乗)", wkSum - I, GetHeihouKon(wkSum - I));
        Console.WriteLine("A+B+C+D+E+F+G  +I+J = {0} ({1}の2乗)", wkSum - H, GetHeihouKon(wkSum - H));
        Console.WriteLine("A+B+C+D+E+F  +H+I+J = {0} ({1}の2乗)", wkSum - G, GetHeihouKon(wkSum - G));
        Console.WriteLine("A+B+C+D+E  +G+H+I+J = {0} ({1}の2乗)", wkSum - F, GetHeihouKon(wkSum - F));
        Console.WriteLine("A+B+C+D  +F+G+H+I+J = {0} ({1}の2乗)", wkSum - E, GetHeihouKon(wkSum - E));
        Console.WriteLine("A+B+C  +E+F+G+H+I+J = {0} ({1}の2乗)", wkSum - D, GetHeihouKon(wkSum - D));
        Console.WriteLine("A+B  +D+E+F+G+H+I+J = {0} ({1}の2乗)", wkSum - C, GetHeihouKon(wkSum - C));
        Console.WriteLine("A  +C+D+E+F+G+H+I+J = {0} ({1}の2乗)", wkSum - B, GetHeihouKon(wkSum - B));
        Console.WriteLine("  B+C+D+E+F+G+H+I+J = {0} ({1}の2乗)", wkSum - A, GetHeihouKon(wkSum - A));
        Console.WriteLine();
        Console.WriteLine("A+B+C+D+E+F+G+H+I+J={0}", wkSum);
    }
}


実行結果

省略
10個の数の和を、8650として検証中
10個の数の和を、8651として検証中
10個の数の和を、8652として検証中
10個の数の和を、8653として検証中
10個の数の和を、8654として検証中
10個の数の和を、8655として検証中
10個の数の和を、8656として検証中
解候補を発見
A=7,B=192,C=375,D=556,E=735,F=912,G=1087,H=1260,I=1600,J=1932
A+B+C+D+E+F+G+H+I   = 6724 (82の2乗)
A+B+C+D+E+F+G+H  +J = 7056 (84の2乗)
A+B+C+D+E+F+G  +I+J = 7396 (86の2乗)
A+B+C+D+E+F  +H+I+J = 7569 (87の2乗)
A+B+C+D+E  +G+H+I+J = 7744 (88の2乗)
A+B+C+D  +F+G+H+I+J = 7921 (89の2乗)
A+B+C  +E+F+G+H+I+J = 8100 (90の2乗)
A+B  +D+E+F+G+H+I+J = 8281 (91の2乗)
A  +C+D+E+F+G+H+I+J = 8464 (92の2乗)
  B+C+D+E+F+G+H+I+J = 8649 (93の2乗)

A+B+C+D+E+F+G+H+I+J=8656

経過時間=00:00:20.9369539


解説

10個の数の和をSumOfAToJとすると
A+B+C+D+E+F+G+H+I+J = SumOfAToJ
SumOfAToJ-A = 平方数01
SumOfAToJ-B = 平方数02
SumOfAToJ-C = 平方数03
SumOfAToJ-D = 平方数04
SumOfAToJ-E = 平方数05
SumOfAToJ-F = 平方数06
SumOfAToJ-G = 平方数07
SumOfAToJ-H = 平方数08
SumOfAToJ-I = 平方数09
SumOfAToJ-J = 平方数10
となるので、AからJを(A < B < C < D < E < F < G < H < I < J として)
順に求めていってます。

10個の数の組み合わせを、枝切りなしで求めると、組み合わせ爆発が発生するので、
組み合わせ爆発を抑えるアルゴリズムを使ってます。