連立方程式演算プログラム(2)

JavaScriptで連立方程式の解を自動生成

概要

2005/10/09

あくまでプログラム重視なので行列の詳しい解説は省く。単に解だけが欲しいならページの一番下を参照してください。

前回のプログラムは、手元で計算した解をプログラムに実装する静止的なものであったが、今回n元連立1次方程式の行列をプログラム自身が解いて答えを求める。プログラムの流れは次のようになる。

  1. 連立方程式の未知数の数(元数)を入力
  2. 指定された元数の行列式を立てる
  3. クラメル公式でn個の解を行列式で表す
  4. 行列を展開
    1. 考えられる組み合わせを全て洗い出す
    2. 偶順列と奇順列に分けそれぞれに符合をつける
  5. 展開した行列から解を求める式を生成し文字列型データとして格納
  6. n元連立1次方程式の計算フォームを出力
  7. 変数にフォームからの値を代入
  8. 文字列型データを実行スクリプトに復元し計算
  9. 計算結果を出力

最も重要なアルゴリズムは行列の展開にある。一般にn元連立1次方程式の場合、解の項数はn!で示される。nの値し対しての項数は以下のとおり。

nの値し対しての項数表
n 項数 変数の使用回数
2 2 4
3 6 18
4 24 96
5 120 600
6 720 4320
7 5040 35280
8 40320 322560
9 362880 3265920
10 3628800 36288000
11 39916800 439084800
12 479001600 5748019200
13 6227020800 80951270400

計算量は階乗的に増加するので、JavaScriptの計算速度ではn=7でさえ厳しいことになる。数字を書いただけでは実感が湧かないのであれば前回手動で計算した4元連立1次方程式の解を見てほしい。24項であれなのだから、120項ある5はもはや想像がつかない世界だろう。ちなみに項数はこうなるが、代入を繰り返す変数の総使用回数はこれのn倍ある。いかに少ないステップで全ての項をコンプリートするかが最終的な計算速度になる。また、n=4の時点で計算式全体の文字数が2300文字以上ある。5元の場合2万字、6元では20万に膨れ上がる。

クラメル公式

2005/10/09

クラメル公式は行列で表された連立方程式の解を示す公式だ。2元、3元の場合は以下のようになる。

  1. a11x+a12y=b1
  2. a21x+a22y=b2

xのクラメル公式  yのクラメル公式

  1. a11x+a12y+a13z=b1
  2. a21x+a22y+a23z=b2
  3. a31x+a32y+a33z=b3

xのクラメル公式 yのクラメル公式 zのクラメル公式

行列式だと連立方程式の解を簡単に表すことができる。

クラメル公式の展開

2005/10/09

次はクラメル公式で得られた行列を通常の式に変換する。これに莫大な計算が必要になる。最初に行列式の横列の項の組み合わせを全て洗い出す。2元なら展開すべき列は2つで各列の長さも2なので2回。3元の場合6回、4元では24回。最初の表を見れば分かるが回数は階乗的に増加する。

例えば3元の分母の行列を展開すると次のようになる。

3!個の項ができることになる。ここで例えば何次でもおうようできそうな3次の行列式を展開するJavaScriptを考えてみる。

var r = 3;
document.writeln("<pre>");
for(i=Math.pow(r,r-2);i<Math.pow(r,r);i++){
        var str = i.toString(r); if(str.length<r)str = "0"+str;
        var ans = 0;
        for(j=0;j<r;j++){
                if(str.substr(j+1,r-j).match(str.charAt(j)))break;
                        else ans++;
        }
        if(ans==r){
                for(j=0;j<r-1;j++){
                        document.write("a",(j+1),(eval(str.charAt(j))+1),"*");
                }
                document.writeln("a",r,(eval(str.charAt(r-1))+1));
        }
}

再帰関数を利用して超高速に求める方法を模索するのは次回にして、とりあえずrに値を代入すると0〜r-1までの数字を組み合わせた全パターンが出力される。n進数を使っているので最大9が限界であるが、そもそもr=7の段階で相当な時間を要するのでn=9なんてJavaScriptでは計算不可能に等しい(r=7のとき5040パターンあるしね)。

ここにr=3を代入すると最初に書いた組み合わせと全く同じものが出力される。

a11*a22*a33
a11*a23*a32
a12*a21*a33
a12*a23*a31
a13*a21*a32
a13*a22*a31

これで1つ目の関門はクリアされた。だがまだ符合が足りない。次は符合を決定する偶順列/奇順列の判別関数を設計する。

偶順列/奇順列とはそれぞれの順列の一番右の添字の数字が1,2,3...と順番になるように並び替える手順数で決まる。例えば、a11*a22*a33は初めから1,2,3が並んでいる。手順は0回で偶数だから偶順列。a11*a23*a32は2番目と3番目を入れ替えて1,2,3になるから手順は1で奇数だから奇順列、という具合だ。つまり、数列をソートするときの移動回数が2で割り切れるかどうかで符合が決定される。

var r = 3;
document.writeln("<pre>");
for(i=Math.pow(r,r-2);i<Math.pow(r,r);i++){
        var str = i.toString(r); if(str.length<r)str = "0"+str;
        var ans = 0;
        for(j=0;j<r;j++){
                if(str.substr(j+1,r-j).match(str.charAt(j)))break;
                        else ans++;
        }
        if(ans==r){
                document.write(math_plus(str));
                for(j=0;j<r-1;j++){
                        document.write("a",(j+1),(eval(str.charAt(j))+1),"*");
                }
                document.writeln("a",r,(eval(str.charAt(r-1))+1));
        }
}

先ほどのプログラムに追加。math_plus関数が目標とする関数だ。早速作ってみる。

math_plus = function(str){
        var cnt = 0;
        var pass_p = new Array();
        var move = true;
        for(k=0;k<str.length;k++)pass_p[k] = str.charAt(k);
        while(move){
                move = false;
                for(l=0;l<str.length-1;l++){
                        var ex = [pass_p[l],pass_p[l+1]];
                        if(ex[0]>ex[1]){
                                pass_p[l] = ex[1];
                                pass_p[l+1] = ex[0];
                                cnt++;
                                move = true;
                        }
                }
        }
        if(cnt%2==0)return "+";
        else return "-";
}

これがそのプログラム。連続する2数を比べ、前者のほうが大きければ2数を入れ替えることによって配列をソートする。これをくわえてr=3を出力すると…

+a11*a22*a33
-a11*a23*a32
-a12*a21*a33
+a12*a23*a31
+a13*a21*a32
-a13*a22*a31

正しく符合がつけられている。これで行列の展開は成功だ。最後にこれらの関数を使いやすい形式にまとめておく。最初のa1やa2にはbが入ることもあるからだ。

math_plus = function(str){
        var cnt = 0;
        var pass_p = new Array();
        var move = true;
        for(k=0;k<str.length;k++)pass_p[k] = str.charAt(k);
        while(move){
                move = false;
                for(l=0;l<str.length-1;l++){
                        var ex = [pass_p[l],pass_p[l+1]];
                        if(ex[0]>ex[1]){
                                pass_p[l] = ex[1];
                                pass_p[l+1] = ex[0];
                                cnt++;
                                move = true;
                        }
                }
        }
        if(cnt%2==0)return "+";
        else return "-";
}
open_square = function(r,b){
        var clae = "";
        for(i=Math.pow(r,r-2);i<Math.pow(r,r);i++){
                var str = i.toString(r); if(str.length<r)str = "0"+str;
                var ans = 0;
                for(j=0;j<r;j++){
                        if(str.substr(j+1,r-j).match(str.charAt(j)))break;
                                else ans++;
                }
                if(ans==r){
                        clae+=math_plus(str);
                        for(j=0;j<r;j++){
                                if(eval(str.charAt(j))+1==b)clae+="b"+(j+1);
                                else clae+="a"+(j+1)+(eval(str.charAt(j))+1);
                                if(j!=r-1)clae+="*";
                        }
                }
        }
        return clae;
}

rは元数、bはbの位置。0のときはbは入らない。ここまで準備があれば展開なんてたった数行。

open_square(3,0)
open_square(3,1)
open_square(3,2)
open_square(3,3)

+a11*a22*a33-a11*a23*a32-a12*a21*a33+a12*a23*a31+a13*a21*a32-a13*a22*a31
+b1*a22*a33-b1*a23*a32-a12*b2*a33+a12*a23*b3+a13*b2*a32-a13*a22*b3
+a11*b2*a33-a11*a23*b3-b1*a21*a33+b1*a23*a31+a13*a21*b3-a13*b2*a31
+a11*a22*b3-a11*b2*a32-a12*a21*b3+a12*b2*a31+b1*a21*a32-b1*a22*a31

一気に2,3,4の工程が終了した。(上)これらの式は最初のクラメル公式の各行列を展開したものなので、あとはクラメル公式通りにこれらの組み合わせを除算すれば連立方程式を解くことができる。

また、上の4個の計算結果のうち前半の処理は全て同じなのでさらに高速化ができる。(下)rを代入すると4つの式を全て生成する。

math_plus = function(str){
        var cnt = 0;
        var pass_p = new Array();
        var move = true;
        for(k=0;k<str.length;k++)pass_p[k] = str.charAt(k);
        while(move){
                move = false;
                for(l=0;l<str.length-1;l++){
                        var ex = [pass_p[l],pass_p[l+1]];
                        if(ex[0]>ex[1]){
                                pass_p[l] = ex[1];
                                pass_p[l+1] = ex[0];
                                cnt++;
                                move = true;
                        }
                }
        }
        if(cnt%2==0)return "+";
        else return "-";
}
open_square_r = function(r){
        var clae = new Array();
        var clae_c = 0;
        for(i=Math.pow(r,r-2);i<Math.pow(r,r);i++){
                var str = i.toString(r); if(str.length<r)str = "0"+str;
                var ans = 0;
                for(j=0;j<r;j++){
                        if(str.substr(j+1,r-j).match(str.charAt(j)))break;
                                else ans++;
                }
                if(ans==r){
                        clae[clae_c] = new Array(math_plus(str),str);
                        clae_c++;
                }
        }
        return clae;
}
open_square = function(r){
        var claex = new Array();
        var d = open_square_r(r);
        for(i=0;i<=r;i++){
                var b = i;
                claex[i] = "";
                for(j=0;j<d.length;j++){
                        claex[i]+= d[j][0];
                        var str = d[j][1];
                        for(k=0;k<r;k++){
                                if(eval(str.charAt(k))+1==b)claex[i]+="b"+(k+1);
                                else claex[i]+="a"+(k+1)+(eval(str.charAt(k))+1);
                                if(k!=r-1)claex[i]+="*";
                        }
                }
        }
        return claex;
}
open_square(3)
↓
+a11*a22*a33-a11*a23*a32-a12*a21*a33+a12*a23*a31+a13*a21*a32-a13*a22*a31
+b1*a22*a33-b1*a23*a32-a12*b2*a33+a12*a23*b3+a13*b2*a32-a13*a22*b3
+a11*b2*a33-a11*a23*b3-b1*a21*a33+b1*a23*a31+a13*a21*b3-a13*b2*a31
+a11*a22*b3-a11*b2*a32-a12*a21*b3+a12*b2*a31+b1*a21*a32-b1*a22*a31

連立方程式演算プログラム

2005/10/09-2007/09/09

式ができてしまえばプログラム全体の9割が完成したも同然。残り1割を完成させた。n=7以上は時間がかかりすぎて危険。

(2007/09/09)圧倒的に高速化された連立方程式演算プログラム(3)が完成。

連立方程式の解計算プログラム

2005/10/09

解を計算して出力。5元/6元連立方程式の恐怖を体感すべし(何

7元連立1次方程式の解計算結果

2005/10/11

20〜30分程度かけて計算した7元連立1次方程式の解。数式だけで2MB。

関連記事

2008/08/04

ページ情報

作成日時
2005/10/09
最終更新日時
2008/08/04
HTML4.01版
index.html
XHTML1.1版
index.xhtml
XML原本
index.xml