プログラミング雑感  新JavaScript入門  JavaScript,Neo-Generation  掲示板  表紙
19. 多項式クラス(12) 
プログラミング雑感
Written 3/16/03
20. 多項式クラス(13)
途中で整数チェックする
n-1行目をもうちょっと詳しく見てみましょう。
    bn-1 = an-1 0 c0 + ... + an-1 n cn
    an-1i = (-1)n-1-in-1Ci/(n-1)! (i < n)
 
一般的にはこうなりますが、今やってる
    2 + 2x + x16
 
で最後に8次式で割れるなら、
    b7 = (-c0 + 7c1 - 21c2 + 35c3 - 35c4 + 21c5 - 7c6 + c7) / 5040 + 12c8
 
が整数になる必要があります。
これをちょっと変形してみましょう。
    b7 = (-c0 + c7 + 7(c1 - 3c2 + 5c3 - 5c4 + 3c5 - c6)) / 5040 + 12c8
 
これが整数ならば、
    -c0 + c7
 
が7で割れなければなりません。 すなわち、b7 を計算するまでもなく、 c0 と c7 だけで整数でないと判定できることがあることになります。 しかもこれは結構確率が高いです。
しかし、これはいつも成り立つわけではありません。 n-1が素数のときはうまくいくことがすぐに分かりますが、 例えば、7次式で割れるかどうか判定するときは、
    b6 = (c0 + c6 + (-6c1 + 15c2 - 20c3 + 15c4 - 6c5)) / 720
 
が整数であるかチェックするわけですが、 (6, 15, 20)の最大公約数は1なので、8次式のときのようなことはできません。
このように全然括れないことはあまりないようですが、 この手はあきらめて反対側から考えましょう。 すなわち、
    b6 = ((c0 + c6 - 6c1 - 6c5 + 15c2 + 15c4) - 20c3) / 720
 
としてみると、
    c0 + c6 - 6c1 - 6c5 + 15c2 + 15c4
 
が20で割れなければならないことが分かります。
これならどんなときも必ず成り立ちます。 b6 をあと少しで計算できるので効果は小さそうに見えますが、 効果は確実にあります。 しかも、20で割れる確率は単純に考えれば1/20なので、割と効果があります。
ちなみに、8次式で割る場合は、
    b7 = ((-c0 + c7 + 7c1 - 7c6 - 21c2 + 21c5) + 35(c3 - c4)) / 5040 + 12c8
 
となり、
    -c0 + c7 + 7c1 - 7c6 - 21c2 + 21c5
 
が35で割れる必要があります。
これをコードに組み入れたものが次です。
    factorize1l.cpp
 
これで、
    2 + 2x + x16
 
を因数分解したところ、1.2秒となりました。
もう少し次元を上げてみましょう。
    2 + 2x + x18
 
で、48.4秒かかりました。
先ほどの8次式で割る場合の式をもうちょっと変形してみましょう。
    b7 = (-c0 + c7 + 7c1 - 7c6 - 21c2 + 21c5 + 35(c3 - c4)) / 5040 + 12c8
       = (-c0 + c7 + 7c1 - 7c6 + 7(-3c2 + 3c5 + 5c3 - 5c4)) / 5040 + 12c8
       = (-c0 + c7 + 7(c1 - c6 - 3c2 + 3c5 + 5c3 - 5c4)) / 5040 + 12c8
 
これは下から順に、
    -c0 + c7 は7で割れなければならない
    -c0 + c7 + 7c1 - 7c6 は7で割れなければならない
    -c0 + c7 + 7c1 - 7c6 - 21c2 + 21c5 は35で割れなければならない
 
ことを表しています。2番目は実質的に意味はありませんが。
この数は、係数の最大公約数を次々に求めることにより得られます。 すなわち、係数は、
    7 21 35
 
ですが、まず右から35、次にこれと21の最大公約数を取り7、 これと7の最大公約数を取り7、となります。
これをコードに組み入れたものが次です。
    factorize1m.cpp
 
これで、
    2 + 2x + x18
 
で、12.0秒かかりました。意外と速くなりますね。
素因数分解を避ける
上の例では結構素因数分解に時間がかかっています。
以下ではこの素因数分解を避けられるときは避けるようにしてみます。
まず、上の例でどういう整数を素因数分解しているか調べます。
    f(-8) = 18014398509481970   64
    f(-7) = 1628413597910437    16
    f(-6) = 101559956668406     16
    f(-5) = 3814697265617       64
    f(-4) = 68719476730         16
    f(-3) = 387420485           32
    f(-2) = 262142               8
    f(-1) = 1                    2
    f(0) =  2                    2
    f(1) =  5                    4
    f(2) =  262150              72
    f(3) =  387420497           16
    f(4) =  68719476746         16
    f(5) =  3814697265637        4
    f(6) =  101559956668430    384
    f(7) =  1628413597910465    64
    f(8) =  18014398509482002   32
 
右端はそれぞれ割り切れる整数の数です。 ただし、f(0) だけは正の値だけ取ることにしているので半分になっています。
このうち9つを選んで基本的な考え方の p の組み合わせが最小になるようしていて、この場合は、
    f(-3), f(-2), ... , f(5)
 
を選んでいて、組み合わせの数は、
    301,989,888
 
となります。
ところで、この場合 f(-8) の素因数分解は計算する必要がないことが分かります。 f(0) が0,1,-1のときは素因数分解するまでもないし、 そうでないときは、割り切れる整数の数は4以上です。 仮に4として、f(-8) を選んだとき、すなわち
    f(-8), f(-7), ... , f(0)
 
を選んだときの組み合わせの数は、
    1,073,741,824
 
となり、素因数分解するまでもなくこの組み合わせは取り得ないことになります。 同様の理由で f(7), f(8) も素因数分解する必要がありません。
これを組み込んだものが、次です。
    factorize1n.cpp
 
これで、
    2 + 2x + x18
 
が11.6秒になりました。
この例はあまり速くなりません。もっといい例は次です。
    f(x) = 1 - x17
 
これは確か正17角形の作図と関係あったと記憶していますが、
    f(-7) = 29,078,814,248,401
 
が素数で、一つ前のプログラムだと、 これの素因数分解にほとんどの時間を費やしていて、1.4秒かかってしまいます。 これが今回のプログラムならこの素因数分解を避けられて、0.04秒となりました。
素因数分解を速くする
素因数分解が速ければ上のようなことはする必要はないのですが、 これがなかなか見つかりません。
因子を見つけるアルゴリズムはたくさんあるようですが、 1つρ法というのを試してみたところかなり遅くなってしまいました。 上の例では効果が出てこないというのと、プログラムが悪いってことでしょう。
素数かどうかの判定は、今まで確率的な判定方法が使われてきたようですが、 いくらその確率が1に極めて近いから実用上問題無いと言われても、 だったらこんなプログラムなんか書いてないでどっかからいいソースを拾って来い、 という話になってしまいます。
そんな中、去年の8月に 多項式時間で判定できる決定的な素数判定法 が発表されたそうです。 論文を読んでみると、 意外と簡単そうで、ちょっと組んでみようかな、という感じです。 ただしすごく遅いようで、全く実用的ではないようです。 けれど、こういうものは一つ突破口が見つかれば どんどん速くなっていくものなのでしょう。
さて、そんな夢のある話はおいといて、 お手軽に素因数分解を速くしましょう。
今までは、n の因数を求めるとき、 ひたすら n1/2 までの奇数で割っていました。 もうちょっと工夫して、2と3で割り切れないなら、 5からは6k+5と6k+1で表される整数で割っていけばよいです。 これだと、割とコードが増えないではっきり効果が現れます。
    factorize1o.cpp
 
これで11.4秒になりました。
以上を組み込んで、かつ、入力と出力をもうちょっとましにしたものを作りました。
    polynomial1.h
    ---------------------------------------------------------------------
    class Polynomial : public vector<int> {
        vector<Polynomial>    factors;
    public:
        Polynomial() { }
        Polynomial(const int *a, int size);
        Polynomial(const vector<int>& a);
        Polynomial(const string& s);
        bool operator ==(const Polynomial& a);
        bool operator ==(int a);
        bool operator !=(const Polynomial& a);
        bool operator !=(int a);
        Polynomial& operator +=(const Polynomial& a);
        Polynomial& operator -=(const Polynomial& a);
        Polynomial& operator *=(const Polynomial& a);
        Polynomial& operator /=(const Polynomial& a);
        Polynomial& operator %=(const Polynomial& a);
        friend bool operator <(const Polynomial& a, const Polynomial& b);
        void factorize();
        myint value(int x) const;
        void print() const;
    private:
        void _add(int a, unsigned int dim);
    };
 
これは次のように使います。
    polynomial1.h
    -----------------------------------------------------------------
    #include "polynomial1.h"
    
    void main(void) {
        Polynomial  p("2+2x+x^18");
        try {
            p.factorize();
        }
        catch(int e) {
            if(e == 1)
                cerr << "オーバーフローのため因数分解できず" << endl;
            else
                cerr << "メモリエラー" << endl;
        }
        p.print(); cout << endl;
    }
 
この連載は長くなってしまいました。
まだネタ(高速化の手法)はあるのですが、 本来の目的は多変数多項式の因数分解だったはずですし、 他のページの更新も全然してないので、 このへんで休みたいと思います。
first, prev, next, exit