• ベストアンサー

組み合わせ順列

nCrを求める関数combination(n,r)をC言語で作りたいのですが、どうすればよいか教えてください。また、参考となるようなサイトを教えてください。僕の作った関数だと、すぐに桁あふれになってしまいます。そのことを考慮して、桁あふれにになりにくい関数もつくりました。これは パスカルの三角形の関係を使ったnCr=n-1Cr +n-1Cr-1の関係を使っての再帰関数です。しかし、これだと結果を出すのに時間がかかってしまいます。僕がつくった関数をいくつか出しておきますのでいい考えがあれば教えてください。64C32が高速に正確に出れれば最高です。 long combination(int n,int r) { int i, a, b; long c; if(r>n/2) r=n-r; a= n; b= 1; c= 1L; for(i=0 ;i< r ;i++){ c= c* a/ b; a--; b++; } return c; } これはすぐに桁あふれになってしまう。 long combination(int n,int r) { int i, a, b; double c; if(r>n/2) r=n-r; a= n; b= 1; c= 1.0; for(i=0 ;i< r ;i++){ c= c/b*a; a--; b++; } return (long)c; } これはcをdoubleにして計算する分、丸めこみが生じ誤差がでる。 long combination(int n,int r) { long c; if(r> n-r) r=n-r; if(r==0) return 1; else if(r==1) return n; else{ c= combination(n-1,r-1)+ combination(n-1,r); return c; } } これは誤差は出ず正確であるがいかんせん遅い!

質問者が選んだベストアンサー

  • ベストアンサー
回答No.9

お詫びに: #include <cstdio> __int64 combination(int n, int r) { __int64* work = new __int64[n+1]; /* n は 64 まで */ for (int i = 0; i <= n; i++) { for (int j = i - 1; j > 0; j--) { work[j] += work[j - 1]; } work[i] = 1; } __int64 result = work[r]; delete[] work; return result; } namespace std {} using namespace std; int main() { for ( int i = 0; i < 33; ++i ) printf("64C%d = %I64d\n", i, combination(64,i)); return 0; } ----- 結果 ------- ..... 64C25 = 401038568751465792 64C26 = 601557853127198688 64C27 = 846636978475316672 64C28 = 1118770292985239888 64C29 = 1388818294740297792 64C30 = 1620288010530347424 64C31 = 1777090076065542336 64C32 = 1832624140942590534 だそうです。# VC6でコンパイル/実行

tera242
質問者

お礼

なんどもお返事ありがとうございます。大変感謝しております。64C32 = 1832624140942590534 と分かっただけでも大変参考になりました。これから自分がつくらないといけないものは1024C512なのでガンバってみます。これからも何かあればよろしくお願いします。

その他の回答 (8)

回答No.8

あああ、失礼しましたっ。 ダイナミックレンジ上げるためにunsigned long にしちまったのが敗因でした。 # __int64にしとけばよかった...

noname#30727
noname#30727
回答No.7

>しくしく、吹っ飛びます。 > >> for (i = 0; i <= n; i++) { >> for (j = i - 1; j > 0; j--) { > >ココで j が -1 になるので、 >work[-1] += work[-2]; // ぶっとびー for の条件式 j > 0 によって、負のインデックスでのアクセスはありえません。

回答No.6

> それを以下のようにコーディングします。 しくしく、吹っ飛びます。 > for (i = 0; i <= n; i++) { > for (j = i - 1; j > 0; j--) { ココで j が -1 になるので、 work[-1] += work[-2]; // ぶっとびー

noname#30727
noname#30727
回答No.5

パスカルの三角形の再帰を展開してみたらどうでしょうか? 配列内の値がどのように変化するかを 7Cr まで書きます。 0Cr: 1 1Cr: 1 1 2Cr: 1 2 1 3Cr: 1 3 3 1 4Cr: 1 4 6 4 1 5Cr: 1 5 10 10 5 1 6Cr: 1 6 15 20 15 6 1 7Cr: 1 7 21 35 35 21 7 1 それを以下のようにコーディングします。 ただし、64C32 だと 32bit の long ではオーバーフローするので、n の範囲を制限するか、より大きな型を使用してください。 long combination(int n, int r) { long work[65]; /* n は 64 まで */ int i, j; for (i = 0; i <= n; i++) { for (j = i - 1; j > 0; j--) { work[j] += work[j - 1]; } work[i] = 1; } return work[r]; }

tera242
質問者

お礼

大変ありがとうございます。やはり、誤差がでないのはパスカルの三角形ですね。ほかにいろいろかんがえてみます。大変参考になります。また、何かあればよろしくお願いします。

  • nitscape
  • ベストアンサー率30% (275/909)
回答No.4

約分だけでなく#3で言われているように素因数分解もつけてみました。しかしやはり桁あふれをおこします。 除算を複数回行うとどうしても誤差が生じてしまうので、誤差なしで求めるには無限桁計算のルーチンが必要になりますね。ということで乗算部分までは作ってみました。あとは無限桁の除算ルーチンを加えれば完成(?)です。 ただ無限桁の除算ルーチンは今まで作れたことがないので私にはできません。 void Soinsuu(int nData,CDWordArray& adwElement) { int i; int j; int n; bool b; if(nData == 1) return; b = false; n = int(sqrt(nData) + 1); for(i = 2; i <= n; i += 2) { if(i == 4) //2で1回、次からは2n+1で処理を i--; //行なうように4のときに1を減算 j = 0; while(1) { if(nData % i == 0) { j++; nData /= i; adwElement.Add(i); b = true; } else { break; } } } if(b == false) adwElement.Add(nData); return; } void exee(CDWordArray& adwData1,CDWordArray& adwData2) { int i; int j; for(i = 0; i < adwData1.GetSize(); i++) { for(j = 0; j < adwData2.GetSize(); j++) { if((adwData2[j] % adwData1[i]) == 0) { adwData2[j] /= adwData1[i]; adwData1[i] = 1; break; } } } } //adwData は0~9999まで! void Mul(CDWordArray& adwResult,CDWordArray& adwData) { int i; int j; DWORD dwUp; adwResult.RemoveAll(); adwResult.Add(1); dwUp = 0; for(i = 0; i < adwData.GetSize(); i++) { if(adwData[i] != 1) { for(j = 0; j < adwResult.GetSize(); j++) { adwResult[j] *= adwData[i]; adwResult[j] += dwUp; dwUp = 0; if(adwResult[j] >= 10000) { dwUp = adwResult[j] / 10000; adwResult[j] %= 10000; } } if(dwUp > 0) { adwResult.Add(dwUp); dwUp = 0; } } } for(i = adwResult.GetSize() - 1; i >= 0; i--) { TRACE("%d ",adwResult[i]); } TRACE("\n"); } void main() { int i; int n; int r; DWORD dwResult; DWORD dwBuff; CDWordArray adwUp; CDWordArray adwDown; CDWordArray adwUp2; CDWordArray adwDown2; n = 64; r = 32; for(i = n - r + 1; i <= n; i++) adwUp.Add(i); // n(n-1)(n-2)...(n-r+1) for(i = 1; i <= r; i++) adwDown.Add(i); // r! exee(adwUp,adwDown); //素因数分解する前に簡単な約分 for(i = 0; i < adwUp.GetSize(); i++) Soinsuu(adwUp[i],adwUp2); //分子の素因数分解 for(i = 0; i < adwDown.GetSize(); i++) Soinsuu(adwDown[i],adwDown2); //分母の素因数分解 exee(adwUp2,adwDown2); //素因数分解してから約分 dwResult = 1; for(i = 0; i < adwUp2.GetSize(); i++) { if(adwUp2[i] != 1) { dwResult *= adwUp2[i]; TRACE("%d * ",adwUp2[i]); } } TRACE("\n"); dwBuff = 1; for(i = 0; i < adwDown2.GetSize(); i++) { if(adwDown2[i] != 1) { dwBuff *= adwDown2[i]; TRACE("%d * ",adwDown2[i]); } } TRACE("\n"); dwResult /= dwBuff; //求め終わり...but、桁あふれあり TRACE("%dC%d = %d\n",n,r,dwResult); adwUp.RemoveAll(); Mul(adwUp,adwUp2); //分子の無限桁計算(桁あふれなし) adwDown.RemoveAll(); Mul(adwDown,adwDown2); //分母の無限桁計算(桁あふれなし) //多桁でadwUp/adwDownの計算ができれば誤差なしで求められる }

tera242
質問者

お礼

僕が最後に考えたのは多倍長演算です。最終的には実験に1024C512が要りそうなので、多倍長演算で計算を実行してみます。

回答No.3

nCr = n! / ((n-r)! * r!) だから、分子 n! と 分母 (n-r)! : r! それぞれを 素因数分解し、分子,分母から共通項を消去すれば、 かなりイケそうな気がします。 # 気がするだけ^^; すんません。

tera242
質問者

お礼

ちょっと、やってみます。ありがとうございます。

noname#11476
noname#11476
回答No.2

一つの方法は、tera242さんのdoubleを使うルーチンで、最後に丸め処理( c + 0.5 してから整数化する=四捨五入)方法です。 それだと丸め誤差は出てこないと予想されます。 ただ、計算の方法として、 a = n; b = r; c = 1.0; for(i=0 ;i< r ;i++){ c= c/b*a; a--; b--; } return (long)(c + 0.5); としたほうが賢明かもしれません。(小数部を有効に使うことにより、cの数が大きくなりすぎるのを防ぐ) (ただ返す数字はlongでdoubleを考えると多分こんなことはしなくても大丈夫と思いますが) 他の方法は、ln(n!) を計算する、factln(n) (精度double)を用意して、 c = factln(n) - factln(n-r) - factln(r) return (long)(c + 0.5); とするやり方も考えられます。 参考になりそうな数学的な話は、 http://mathworld.wolfram.com/BinomialCoefficient.html http://mathworld.wolfram.com/Factorial.html で、あとは上記の参考文献などを見ればもっと効率の良い方法はありそうです。 が、、、詳細は見たことが無いので分かりません_o_ どれも確かめたわけではありませんので、自信なしとしておきます。 ご参考になれば。

tera242
質問者

お礼

四捨五入は大変参考になりました。それを使うことによって他のプログラミングに生かせそうです。

  • nitscape
  • ベストアンサー率30% (275/909)
回答No.1

CDWordArrayはDWORD型の自由に配列サイズを変えられる配列です。そこにどんどん乗算すべき値を代入しています。dwUpは分子、dwDownは分母としています。数値が大きくなりすぎないように約分してから最後に一回だけ除算を行っています。あとTRACEはテスト用に入れたものでメッセージを表示するのに使いました。意味はありません。 適当に作ったので非常に効率は悪いですが、こういう人間的なアルゴリズムもあるという参考程度にどうでしょうか?&もしかしたらnCrの定義を間違えて計算しているかもしれません。 int i; int j; int n; int r; DWORD dwResult; DWORD dwBuff; CDWordArray dwUp; CDWordArray dwDown; n = 64; r = 32; for(i = n - r + 1; i <= n; i++) { dwUp.Add(i); // n(n-1)(n-2)...(n-r+1) } for(i = 1; i <= r; i++) { dwDown.Add(i); // r! } for(i = 0; i < dwDown.GetSize(); i++) { for(j = 0; j < dwUp.GetSize(); j++) { if((dwUp[j] % dwDown[i]) == 0) { dwUp[j] /= dwDown[i]; dwDown[i] = 1; break; } } } dwResult = 1; for(i = 0; i < dwUp.GetSize(); i++) { dwResult *= dwUp[i]; TRACE("%d * ",dwUp[i]); } TRACE("\n"); dwBuff = 1; for(i = 0; i < dwDown.GetSize(); i++) { dwBuff *= dwDown[i]; TRACE("%d * ",dwDown[i]); } TRACE("\n"); dwResult /= dwBuff; TRACE("%dC%d = %d\n",n,r,dwResult);

tera242
質問者

お礼

回答大変ありがとう。ございます。参考にさせていただきます。

関連するQ&A

  • 異なるn個の整数からr個の整数を取り出す組み合わせ

    異なるn個の整数からr個の整数を取り出す組み合わせの数 nCrを求める関数 int combination(int n, int r){ /* ・・・ */} を作成せよ。なおnCrは以下のように定義される。 nCr = n-1Cr-1 + n-1Cr (ただし nC0 = nCn =1、nC1 =n ) (新版 明解C言語 入門編(柴田望洋 著) P.197 演習8-6) というので答えが int combination(int n, int r) { if((n>r) && (r>0)){ return combination(n-1,r-1) + combination(n-1,r); } else if(n==r || r==0){ return 1; } } ・という風になると教えてもらったのですがなぜこうなるのかが分かりません。 ・else if(n==r || r==0){ というのは削っても正常に動きますが、必要な物なのでしょうか? ・またifを使うときは if→eise if →else の順に使って 2つの時は if→else と使っていたのですが 上のものはif→else ifと書いています。 加えてelse(n==r || r==0){ と書いたらコンパイルエラーになってしまいました なぜelse ifと書くのでしょうか? 以上3点について教えてください。よろしくお願いいたします。

  • 組み合わせ

    aに100、bに20や2を入力すると プログラムが停止します。 計算できるように御指摘お願いします。 以下のプログラムです。 #include<stdio.h> int factrical(int n){ if(n>0){ /*printf("%d\n",n);*/ return (n*factrical(n-1)); } else{ return(1);} } int combination(int n ,int r){ return(factrical(n)/(factrical(n-r)*factrical(r))); } int main (void){ int a,b,c; printf("二つの数を入力してください。\n"); do{ printf("大きい方の数を入力してください。\n"); scanf("%d",&a); scanf("%d",&b); }while(a<b); c=combination(a,b); printf("%d",c); return(0);}

  • 困ってます…nCrを求めるC言語プログラミング

    nCr、つまりn個のうちr個を取り出すときの場合の数を求めるプログラミングを作りたいのですが、どうもうまくいきません。 関数combinationを作って求めるという指定もあり、自分で出来るとこまで作ってみたのですが訳がわからなくなってしまい、かなり困っています…; コンパイルは出来るのですが実行してもセグメントエラーが出るばかりで… すみませんがご指摘していただけないでしょうか…? #include<stdio.h> //階乗を計算する関数 int fact(int num){ int i; if(num < 0){ return -1; } else if(num == 0){ return 1; } else if(num == 1){ return 1; } else { i = num * fact(num - 1); return i; } } //コンビネーションを計算 int combination(int n, int r) { int fact(int num); int i; i=fact(n)/fact(r)/fact(n-r); return combination(n-1, r-1)-combination(n,r-1); } int main(void) { int n, r; while ( printf("n r を入力して下さい。"), scanf("%d%d", &n, &r) == 2 ) { printf("nCr(%d,%d)=%d\n", n, r, combination(n, r)); } return 0; }

  • nCrはどうあらわせばい??

    キーボードから二つの正の整数n,r を入力し,組み合わせの数nCr を計算して画面表示するプログラムを作成せよ.ただし,組み合わせの数を計算する関数のプロトタイプをint combination( int, int )とし,my_scanf()をキーボードから二つの正の整数n,r を入力する関数,kaijo() を階乗を求めるプログラムとする. という問題なんですが #include <stdio.h> int my_scanf(void){ int n; do{ scanf("%d",&n); }while(n <= 0); return n; } int kaijo(int m){ int i,x = m; for(i=1;i<m;i++){ x *= i; } return x; } int combination( int m, int w){ int ncr; ncr = m/w; return ncr; } int main (void){ int pos,sum_n,sum_r,answer; printf("n = "); pos = my_scanf(); sum_n = kaijo(pos); printf("r = "); pos = my_scanf(); sum_r = kaijo(pos); answer = combination(sum_n,sum_r); printf("nCr = %d",answer); return(0); } 結果的にはn!/r!を求めるプログラムに・・・・。 combination関数内を書き直せばいいのでしょうか?

  • nCrの計算

    nCrの計算のプログラムを nCr=n!/(r!(n-r)!) を用いて再帰的関数を使って書いたのですが、もし nCr=n(n-1)(n-2)・・・(n-r+1)/r! であることを用いて、nからmまでの掛算を実現する2引数の関数を定義して、再帰的関数呼び出しを用いたnCrのプログラムを作成するとしたらどうなるでしょうか。 関数x!の定義は、関数の宣言をlong factorial(int x)として、 if (x==0) return(1); else return(x*factorial(x-1)); となることは分かるのですが、 2引数の関数m(m+1)・・・nはどう作れば良いのか全くわからないので、プログラムが書けない状態です。アドバイスお願いします。

  • 組み合わせ プログラミング

    c言語についてです os linux コンパイラはgccです long fact2(int n,int m)を作成してfact2(n,m)を使って組み合わせを計算するプログラムを作れという問題で下記のように作りましたが コンパイルできません. エラーメッセージは 2-1.c:14: error: 関数 `fact2' への引数が少なすぎます 2-1.c:14: error: 関数 `fact2' への引数が少なすぎます 2-1.c:14: error: 関数 `fact2' への引数が少なすぎます です 関数のとこが違うと思うですが どうしたらいいのかわかりません それともなにか他のとこが違うのでしょうか? #include <stdio.h> long fact2(int,int); main() { int n, m; long c; printf(" nCm (n>m) \n"); printf("input n m ="); scanf("%d %d",&n, &m); c =fact2(n) / (fact2(m) * fact2(n-m)); printf("%dC%d = %ld\n",n,m,c); } long fact2(int n,int m) { int i; long c=1; for(i=1; i<=n; i++) c*=i; return(c); }

  • nCrの値が表示されない

    C言語でnCrの値を求めるプログラムをnCr=n!/(r!*(n-r)!)を元に、再帰を用いて階乗を求める関数を用いて作成したのですが、nとrがn=40,r=11等の大きな値になるとnCrの値をunsigned long intで定義していても値が表示されませんでした。桁あふれではないんですが何故でしょうか?PCの構造にそこまで詳しくないので、詳しい理由を教えてください。

  • このプログラム見てください

    これで動いたと書いてあるのに動きません。 どこを直せば良いのか教えてください。 #include <stdio.h> int combination(int n,int r){ if ( r==0 ){ return 1; }else if( r==n ){ return 1; }else{ return (combination(n-1,r-1)+combination(n-1,r)); } } int main(){ int num_n=0; int num_r=0; int answer=0; printf("組み合わせの計算をします。数値を入力してください。N=?。\n"); printf("[n]:"); scanf("%d",&num_n); rewind(stdin); printf("[r]:"); scanf("%d",&num_r); rewind(stdin); answer=combination(num_n,num_r); printf("%dC%d=%d\n" , num_n, num_r, answer); return 0; }

  • 組み合わせ

    n個の集合からp個を取る組み合わせの総数を出力するプログラムなんですが nCp=n!/p!(n-p)!という式を使い #include<stdio.h> int kaijo(int m); int comb(int n,int p); int main(void) { int i,j; printf("n="); scanf("%d",&i); printf("p="); scanf("%d",&j); printf("comb(%d,%d)=%d\n",i,j,comb(i,j)); } int kaijo(int m) { if(m>0) return(m*kaijo(m-1)); else return 1; } int comb(int n,int p) { if(n>0) return((n*kaijo(n-1))/(p*kaijo(p-1)*(n-p)*kaijo(n-p-1))); else return 1; } と書いてみたのですがこれではnが大きいとC言語のint型で扱える最大値を超えてしまい正しい結果が出力されません。  そこでint型を使ったままでnやpが大きい場合でもある程度出力できるようにしたいのですがどう改良したらよいのでしょうか? おそらくnCp=n*(n-1)*・・・*(n-p+1)/p!という式を使うのですがよく分かりません。よろしくお願いします。

  • 因数分解プログラム(C言語)について(2)

    つづきです /*因数分解できるか判断する関数*/ int judge(int *a,int *b,int *c,float *D) { *D = (float)((*b) * (*b)) - (4 * (*a) * (*c)); if(D > 0 || D ==0){ printf("判別式は条件を満たしました。\n因数分解を行います。\n"); } else if(D < 0){ printf("判別式はD < 0であるため、因数分解できません。\n"); exit(1); } return 0; } /*因数分解をする関数*/ int bunkai1(int *a,int *b,int *q,int *n1,int *m1,float *D) { *q = sqrt(*D); *n1 = -(*b) + *q; *m1 = 2 * (*a); return 0; } int bunkai2(int *a,int *b,int *q,int *n2,int *m2,float *D) { *q = sqrt(*D); *n2 = -(*b) - *q; *m2 = 2 * (*a); return 0; } /*約分を行う関数*/ int yakubun1(int *m1,int *n1,int *min1,int *flag,int *i,int *d,int *e) { /*最大公約数を見つける*/ if(*m1 < *n1){ *min1 = *m1; } else{ *min1 = *n1; } *flag = 0; for(*i = min1; *i > 0; *i--){ if(*m1 % *i == 0){ if(*n1 % *i == 0){ *flag = 1; break; } } } つづく 関連URL:http://www.okweb.ne.jp/kotaeru.php3?q=474593

専門家に質問してみよう