今日は素因数分解のプログラムを紹介します。言語はC++です。素因数分解を行っていて、再帰プログラムでかけるのではないかと考えて、苦手だった再帰プログラムの練習として作ってみました。
通常、人間が手で素因数分解をやるようなアルゴリズムでコンピュータに素因数分解を行って行きます。例えば、90という数字を素因数分解をするとき、
2| 90 3| 45 3| 15 5
となり、90=233*5と計算できます。つまり、ある数の最小の素因数を見つけ、それを見つけたら、その数をその素因数で割り、その割った値に対して同様な作業を繰り返しています。
それをプログラムにしていきましょう。
<キーワード> 素因数分解, C++, アルゴリズム, 再帰処理, プログラム
まず、素数のリストを作ります。素数のリストは適当に作ります。最悪、手打ちでもいいでしょうw
// the number of primes which is less than 1000 const int PRIME_NUM = 168; //primes less than 1000 const int PRIME[PRIME_NUM] = { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, 827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 997};
それでは、最初の素因数を手に入れる函数を作りましょう。
素因数は素因数分解される数より大きくなることはないので、num<PRIME[i]になったとき、0を返します。
もし、num>PRIME[i]のとき、numが割り切れるかどうかを確認し、割り切れれば、その値を返し、そうでなければ、次の素数に対して同様の操作を繰り返します。
int firstFact(int num){ /* the least prime which is a factor of number */ for (int i=0; i<PRIME_NUM ; i++) { if (num > PRIME[i] ) { if (num%PRIME[i] == 0) { return PRIME[i]; } }else{ return 0; } } }
実際に素因数分解する処理です。
factという変数に素因数分解される数の最小の素因数を入れます。
ここで、素因数分解できなかったときは0が返ってくるので、fact=0のとき、このプログラムは終了します。
fact>0のときはrsltという素因数を保存しておく配列に入れます。
その後、num/factに対して再帰的に素因数分解を行います。
void factoring(int num, vector<int> * rslt){ /* factoring num and the factors into rslt */ int fact = firstFact(num); if (fact>0) { (*rslt).push_back(fact); }else{ return; } factoring(num/fact, rslt); return; }
これは表示させる函数です。
rsltをもらって、例えば、その配列が{2,3,3,5}であれば、
90=2*3*3*5 end
と表示されます。
void printFactors(vector<int> rslt){ /* print result of factoring num */ int num = 1; for (vector<int>::iterator it=rslt.begin(); it!=rslt.end();it++){ num *= (*it); } cout << num << " = "; for (vector<int>;::iterator it = rslt.begin(); it != rslt.end(); it++) { cout << (*it); if(it+1 != rslt.end()){ cout << "*"; }else { cout << endl; } } printf("endn"); }
実際に動かすmain函数は適当にこんなふうに作ればテストできます。
void main(){ int number; vectorrslt; cout << "1000以下の自然数を入力してください。" << endl; cin << number; factoring(number,&rslt); printFactors(rslt); }
今回はそのまま単純にプログラムを作ってみました。
しかし、数nの素因数をpとすると、必ず、p^2<=nが成り立つことを利用して、冗長さを減らし、もう少し計算量を少なくすることができます。
もっと早い、あるいはわかりやすいアルゴリズムがあれば、是非教えて下さい。
こんにちは、前DSLに行た人です。話してないですが。
firstFact で、PRIME[i] と「PRIME[i]で何回割ることが出来るか?」という情報が戻る様にすれば無駄が減るかと思います。
90=2*3*3*5 なら
の素因数分解を考えると、多分 firstFact を5回実行して
1回目: 2 をみつける
2回目: 3 をみつける
3回目: 3 をみつける
4回目: 5 をみつける
5回目: おわり
となると思うのですが、
2,3,4 回目で、やる必要の無い「2で割れるかな?」って処理をしてまっていると思います
(同様に4回目も「3で割れるかな?」をやっていると思います)
あとちょっと残念なお知らせなのですが、この再帰の使い方だと効果としてループと変らないです。
おそらく僕自信にも理解不足が点があるせいで、簡単に説明するのが難しいですが、factoring の一番最後で factoring を呼出していますよね。
これは要するに、factoring 自体がループだという事です。
では逆に、どういう場合に再帰が有効か? というと、factoring の途中で factoring を呼出す場合や、factoring の中で複数回 factoring を呼出す場合です(細かい事は省略しましたが)。
あと p^2<n でなくて p^2<=n ですね。
3^2<=9 とか p,n 両方整数なら = になると思います
長々とすみませんね。
赤木さん
冗長な点のご指摘ありがとうございます。このことを踏まえた上で再度プログラムを組み直してみます。
言われて見れば確かに、この再帰はループと変りないですね。。。冷静に考えてみると再帰プログラムの例でよくある階乗の計算もループでできることを再帰プログラムにしていることに気づきました。
あとp^2<=nの部分に対するご指摘もありがとうございます。修正しました。
> 再帰プログラムの例でよくある階乗の計算もループでできることを再帰プログラムにしていることに気づきました。
そうなんですよねぇ
これまでループを使っていたのに、階乗になるとなぜか突然再帰で説明というパターンはよく見る気がします(再帰でも考えてみるのは良い事だと思いますが)。
僕も昔先生に、「再帰じゃないと階乗は簡単に書けない、もし書けたら成績を5にしてくれる」とか言われたので即刻ループで書き直した思い出が。
自分の頭で考えないから、変なものが伝承されていくパターンは色々あると思いますが、先生とか本書く人にはやって欲しくないなぁと思いますね。