(C) Copyright by IOI日本委員会 1994 All rights reserved.


問題3 解答・解説

 以下に示すプログラムは解答の一例である。アルゴリズムは簡単である。 k 個の箱に、左から順に碁石を配ってみるだけである。 箱 1 から箱 i-1 にはすでに碁石が何個かずつ(左の箱ほど個数が多いように) 配られているとする。まだ碁石が m 個残っていて(すなわち、m > 0)、 しかも、まだ碁石が配られていない箱がある場合(すなわち、i≦k のとき)、 箱 i に m 個、m-1 個、...、1 個と多い方から順にそれぞれ配ってみる。 箱 i に j 個(1≦j≦m)の碁石を配った場合、残った m-j 個の碁石は 残りの箱 i+1 〜 箱 k に同じやり方で配る。 すべての碁石をすべての箱に 配り終わったとき(m=0 かつ i < k のとき)1つの解が得られたことになる。 以上のことを再帰的に行う関数(手続き、プロシージャ)partition(m,i,k) を 考えればよい。partition は、この例のように再帰的ではなく、 反復的に書いてもよい。
 別解として、 はじめに k 個の箱に1個の碁石を配ってから残りの n-k 個を配る方法、 最初の箱に n-k+1 個を残りの k-1 個の箱に1個の碁石を配ってから 碁石を1個ずつ移動する方法、 すべての組合わせを生成して合計が n 個になるものを出力する方法 (これはあまりよくない方法であるが)、 など他にも方法はいろいろある。

 C, Pascal では、解を記録するための配列 solution の代わりに 線形リスト等を用い、メモリを動的に割り当てる方がより一般性があるが、 問題文の中で解の個数の上限を指定しているので、例解ではそのような 方法は用いていない。また、 問題文の指定とは異なるが、配り方を先に出力してしまってから解の個数を 出力するのであれば、このような解の記録をとっておく必要はない。


Turbo Cプログラム

#include  < stdio.h > 

#define MAXk   100   /* kの上限 */
#define MAXSOL 300   /* 解(配り方)の個数の上限 */

int box[MAXk+1],     /* box[i]=箱iに配られた碁石の個数 (i=1,2,...,k) */
                     /* ただし、box[0]=n */
    count,           /* 求める配り方の数 */
    solution[MAXSOL][MAXk];  /* 解の記録 */

void partition(int, int, int);
void print_answer(int);
\end{verbatim}
\newpage

\begin{verbatim}
void main()
{
   int n,k;

   printf("nの値を入力して下さい。n="); scanf("%d", &n);
   printf("kの値を入力して下さい。k="); scanf("%d", &k);
   printf("\nn=%d, k=%d\n", n,k);
   if (n < =0 || k < =0 || k > =MAXk) { printf("データエラーです。\n"); exit(1); }
   if (n < k) { printf("可能な配り方はありません。\n"); exit(0); }

   count=0; box[0]=n; partition(n,1,k);
   print_answer(k);
   exit(0);
}


void partition(int m, int i, int k)
/* 既にbox[1]〜box[i-1]には碁石が配られている。このとき、残りのm個の碁石を
   box[i]〜box[k]に配る配り方をすべて求める (box[i-1]≧m≧0) */
{
   int j;

   if (m==0 && i > k) { /* 得られた1つの配り方を配列solutionに記録する */
      for (j=0; j < k; j++) solution[count][j]=box[j+1];
      if (count++ > =MAXSOL) { printf("解の個数が多すぎます。"); exit(1); }
   }
   if (m > 0 && i < =k) {
      for (j=box[i-1]; j > =1; j--) {
          box[i]=j; partition(m-j,i+1,k);
      }
   }
}


void print_answer(int k)  /* 答を出力する */
{
   int i,j;

   printf("可能な配り方は次の%d通りあります。\n", count);
   for (i=0; i < count; i++) {
      printf("%d", solution[i][0]); 
      for (j=1; j < k; j++) printf(" %d", solution[i][j]);
      if (i < count-1) printf(",   "); else printf("\n");
   }
}


Quick BASICプログラム

DEFINT A-z
DECLARE SUB partition(n,i,k)
DECLARE SUB printAnswer(k)
CONST maxk=100             ' kの上限
CONST maxsol=300           ' 解(配り方)の個数の上限
DIM SHARED box(0 TO maxk)  ' box[i]=箱iに配られた碁石の個数 (i=1,2,...,k)
                           ' ただし、box[0]=n
DIM SHARED solution(maxsol, maxk)  ' 解の記録
COMMON SHARED count        ' 求める配り方の数

   PRINT "nの値を入力して下さい。n=";: INPUT n: PRINT
   PRINT "kの値を入力して下さい。k=";: INPUT k: PRINT
   PRINT "n="; n; " k="; k
   IF n < =0 OR k < =0 OR k > =maxk THEN PRINT "データエラーです。": STOP
   IF n < k THEN PRINT "可能な配り方はありません。":  STOP

   count=0: box(0)=n: CALL partition(n,1,k)
   CALL printAnswer(k)
END


SUB partition(m,i,k)
'既にbox[1]〜box[i-1]には碁石が配られている。このとき、残りのm個の碁石を
'box[i]〜box[k]に配る配り方をすべて求める (box[i-1]≧m≧0) */
   IF m=0 AND i > k THEN  ' 得られた1つの配り方を配列solutionに記録する
         count=count+1
         FOR j=1 TO k: solution(count,j)=box(j): NEXT j
   ELSEIF m > 0 AND i < =k THEN
      FOR j=box[i-1] TO 1 STEP -1
         box[i]=j: CALL partition(m-j,i+1,k)
      NEXT j
   END IF
END SUB


SUB printAnswer(k)  '答を出力する
   PRINT "可能な配り方は次の"; count; "通りあります。"
   FOR i=1 TO count 
      PRINT solution(i,1);: FOR j=2 to k: PRINT " "; solution(i,j);: NEXT j
      IF i < count THEN PRINT ",   "; ELSE PRINT
   NEXT i
END SUB


Turbo Pacalプログラム

program distribution;
   const maxk=100;       { kの上限 }
         maxsol=300;     { 解(配り方)の個数の上限 }
   var box : array[0..maxk] of integer;  
           { box[i]=箱iに配られた碁石の個数 (i=1,2,...,k)、ただし、box[0]=n }
       n,k,count : integer;  { countは求める配り方の数 }
       solution : array[1..maxsol, 1..maxk] of integer;  { 解の記録 }

   procedure partition(m,i,k : integer);
   { 既にbox[1]〜box[i-1]には碁石が配られている。このとき、残りのm個の碁石を }
   { box[i]〜box[k]に配る配り方をすべて求める (box[i-1]≧m≧0) }
      var j : integer;
   begin
      if (m=0) and (i > k) then begin 
         { 得られた1つの配り方を配列solutionに記録する }
           count:=count+1;
           for j:=1 to k do solution[count,j]:=box[j];
      end;
      if (m > 0) and (i < =k) then
         for j:=box[i-1] downto 1 do begin
            box[i]:=j; partition(m-j,i+1,k);
         end;
   end; { of partition }

   procedure print_answer(k : integer);  { 答を出力する }
      var i,j : integer;
   begin
      writeln('可能な配り方は次の', count, '通りあります。');
      for i:=1 to count do begin
         write(solution[i,1]); for j:=2 to k do write(' ', solution[i,j]);
         if i < count then write(',   ') else writeln;
      end;
   end; { of print_answer }

begin
   write('nの値を入力して下さい。n='); readln(n); writeln;
   write('kの値を入力して下さい。k='); readln(k); writeln;
   writeln('n=', n, ', k=', k);
   if (n < =0) or (k < =0) or (k > =maxk) then writeln('データエラーです。') else
   if n < k then writeln('可能な配り方はありません。')
   else begin
      count:=0; box[0]:=n; partition(n,1,k);
      print_answer(k);
   end;
end.