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


問題4 解答・解説

 問題のポイントは3つある : 1)ファイル入出力、2)グループとその メンバー数を求める方法、3)指定された順序で出力するためにソートを 行なうこと。
 1)については、空白行でデータを区切ることをせず、データが何行に わたるかがデータの冒頭に書かれているので、データを文字列として 読み込んでから整数に変換するという必要がなく直接整数として 読み込めるので、なんら問題はない。
 2)は解答例のように再帰を使うと簡単である。
 3)がこの問題の最大のポイントで、解答例に示したように、 グループとそのメンバー数の対をすべて求めてから以下に述べるような radix sort を行なうのが最も上策であろう。エレガントではないが、 求めたすべての対をメンバー数に従って昇順にソートしておいてから、 指数を0から255まで変えてその指数のグループがあるかどうかをメンバー数の 小さい方から順に調べていくという方法とか、本質的に同じことであり やはりエレガントというにはほど遠い方法であるが、0から255まで小さい方から 順にその指数を持つグループをすべて求めていき、同じ指数のグループを 求めている最中に新たに得られたグループはその指数を持つグループの中で メンバー数の順でソートされた位置に入れて記録していく(従って、 すべてのグループを見つけ終わった時点で出力したい順に並んでいる)方法 とかが考えられる。これらの方法はエレガントではないものの、 問題4は問題3とは違いどんなにバカな方法を用いても計算量はごく 小さくて済むので、そのことを認識した上であえてそのような方法を取るのは 悪いことではない。

 以下に示すプログラムは解答の一例である。 解答例で、手続き(関数) sort() は、2桁の数を radix sort するために 使われている。radix sort (基数ソート法)とは、$n$桁の対象 (この例では n=2 で各桁とも数であるが、一般には数でなくても 大小関係のあるデータなら何でも構わない)をソートするのに、 下の桁から順に各桁毎にソートしていく方法である。その際、大小を比較する 対象になるのは注目している1つの桁だけであるが、2つのデータの入れ替えを する際はその1桁だけの入れ替えではなく、全桁を同時に(すなわち、 2つのデータをそっくり)入れ換える。例えば、

      あ b 7,   さ c 2,   あ a 5,   さ a 2,   あ a 3
という5つの3桁のデータを、上の桁をあいうえお順に、 中の桁をアルファベット順に、下の桁を昇順にソートしたいとする。すなわち、
      あ a 3,   あ a 5,   あ b 7,   さ a 2,   さ c 2
のように並べ変えたいとする。はじめに、下の桁だけに注目 してソートを行ない、
      さ c 2,   さ a 2,   あ a 3,   あ a 5,   あ b 7
を得る。次いで、中の桁だけに注目してソートを行なうと
      さ a 2,   あ a 3,   あ a 5,    あ b 7,   さ c 2
となる。最後に、上の桁だけに注目してソートを行なうと 求める結果が得られる。
 この方法でソートがうまくいくためには、各桁毎のソートにあたって 用いるソート法が、「ソートの対象になっている桁の値が等しい2つのデータは、 ソート前における位置の前後関係がソートによって変わらない」という性質 (このような性質を持っているソート法は安定なソート法(stable sort)と 呼ばれる)を持っている必要がある。 解答例に示したものは insertion sort と呼ばれるソート法に基づいており (bubble sortでもよい)、この性質を持っている。
 なお、例えばC(他の言語でも同様)の関数(手続き) find_groups()やfindGroups() において
   if (x+1 < n && y+1 < n && 0 <= x-1 && 0 <= y-1) {
      find_groups(x+1,y,i,count); find_groups(x,y+1,i,count);
      find_groups(x-1,y,i,count); find_groups(x,y-1,i,count);
   }
とするのは誤りである。また、格子点の範囲のチェックのためには、 前もって範囲外に番兵を置いておく方法もある(問題2の解答参照)。


Turbo Cプログラム

#include  < stdio.h > 

#define MAXSIZE  100          /* 格子サイズの上限 */
#define COUNTED   -1          /* カウント済みであることを表す */
#define MAXGROUP 100          /* 出来るグループ数の上限 */

int kousi[MAXSIZE][MAXSIZE];  /* kousi[x][y]は格子点(x,y)の指数 */
struct pair {
  int index,member;           /* group[i].index はi番目のグループの指数 */
} group[MAXGROUP];            /* group[i].memberはそのメンバー数 */
int n,                        /* 格子サイズ */
    groupNo;                  /* 出来たグループの総数を表す */

void find_solution(void);
void find_groups(int, int, int, int *);
void sort(int);

void main()
{
   FILE *fp; int x,y,i;

   /* データを入力する */
   fp=fopen("INPUT.DAT","r");
   fscanf(fp, "%d",&n);
   for (x=0; x < n; x++) 
      for (y=0; y < n; y++) fscanf(fp, "%d",&kousi[x][y]);
   fclose(fp);

   find_solution();  /* グループとそのメンバー数を求める */

   /* 答の出力 */
   fp=fopen("OUTPUT.SOL","w");
   for (i=0; i < groupNo; i++)
      fprintf(fp, "%d %d\n",group[i].index,group[i].member);

   fclose(fp);
   exit(0);
}


void find_solution()  /* グループとそのメンバー数を求める */
{
   int x,y,count;     /* countは1つのグループ内の要素数のカウント用 */

   groupNo=0;
   for (x=0; x < n; x++) for (y=0; y < n; y++) 
      if (kousi[x][y]!=COUNTED) { /* まだカウントが済んでいない点である */
         group[groupNo].index=kousi[x][y];
         count=0; find_groups(x,y,kousi[x][y],&count);
         group[groupNo++].member=count;
      }

   /* memberを下の桁、indexを上の桁として、groupをradix sortする */
   sort(0);  /* 下の桁 */
   sort(1);  /* 上の桁 */
}


void find_groups(int x, int y, int i, int *count)
/* 指数がiである点(x,y)と同じグループに属する点をすべて探してカウントし、
   カウントが済んだ点には印COUNTEDを付ける */
{
   if (kousi[x][y]==i) {
      (*count)++;  kousi[x][y]=COUNTED;
      if (x+1 < n)  find_groups(x+1,y,i,count);
      if (y+1 < n)  find_groups(x,y+1,i,count);
      if (0 <= x-1) find_groups(x-1,y,i,count);
      if (0 <= y-1) find_groups(x,y-1,i,count);
   }
}


void sort(int k)
/* k=0のときmemberに関して、k=1のときindexに関して、groupを昇順にソートする */
{
   struct pair key; int i,j; 

   for (i=1; i < groupNo; i++) {
      key=group[i];
      for (j=i-1; 0 <= j; j--) {
         if (k==0 ? key.member >= group[j].member : key.index >= group[j].index)
            break;
         group[j+1]=group[j]; 
      }
      group[j+1]=key;
   }
}


Quick BASICプログラム

DECLARE SUB findSolution()
DECLARE SUB findGroups(x,y,i,count)
DECLARE SUB sort(k)
CONST maxsize=100                   '格子サイズの上限
CONST counted=-1                    'カウント済みであることを表す
CONST maxgroup=100                  '出来るグループ数の上限
DIM SHARED kousi(maxsize,maxsize)   '格子点(x,y)の指数を表す
TYPE pair
   index  AS INTEGER
   member AS INTEGER
END TYPE
DIM SHARED group(maxgroup) AS pair  'group(i).index はi番目のグループの指数
                                    'group(i).memberはそのメンバー数
COMMON SHARED n                     '実際の格子サイズ
COMMON SHARED groupNo               '出来たグループの総数を表わす

   'データを入力する
   OPEN "INPUT.DAT" FOR INPUT AS 1
   INPUT #1,n
   FOR x=1 TO n: FOR y=1 TO n
      INPUT #1,kousi(x,y)
   NEXT y: NEXT x
   CLOSE 1

   CALL findSolution  'グループとそのメンバー数を求める

   '答の出力
   OPEN "OUTPUT.SOL" FOR OUTPUT AS 2
   FOR i=1 TO groupNo
      PRINT #2,group(i).index; " "; group(i).member
   NEXT i
   CLOSE 2
END


SUB findSolution()  'グループとそのメンバー数を求める
   groupNo=0
   FOR x=1 TO n: FOR y=1 TO n
      IF kousi(x,y) <  > counted THEN
         'まだカウントが済んでいない点である
         groupNo=groupNo+1: group(groupNo).index=kousi(x,y)
         count=0: CALL findGroups(x,y,kousi(x,y),count)
         group(groupNo).member=count
      END IF
   NEXT y: NEXT x

   'memberを下の桁、indexを上の桁として、groupをradix sortする
   CALL sort(0)  ' 下の桁
   CALL sort(1)  ' 上の桁
END SUB


SUB findGroups(x,y,i,count)
'指数がiである点(x,y)と同じグループに属する点をすべて探してカウントし、
'カウントが済んだ点には印countedを付ける
'countはグループ内の要素数のカウント用変数
   IF kousi(x,y)=i THEN
      count=count+1: kousi(x,y)=counted
      IF x+1 < =n THEN CALL findGroups(x+1,y,i,count)
      IF y+1 < =n THEN CALL findGroups(x,y+1,i,count)
      IF 1 < =x-1 THEN CALL findGroups(x-1,y,i,count)
      IF 1 < =y-1 THEN CALL findGroups(x,y-1,i,count)
   END IF
END SUB


SUB sort(k)
'k=0のときmemberに関して、k=1のときindexに関して、groupを昇順にソートする
   DIM keydata AS pair
   FOR i=2 TO groupNo
      keydata=group(i)
      FOR j=i-1 TO 1 STEP -1
         IF k=0 THEN
            found=keydata.member > =group(j).member
         ELSE 
            found=keydata.index > =group(j).index
         END IF
         IF found THEN EXIT FOR
         group(j+1)=group(j)
      NEXT j
      group(j+1)=keydata
   NEXT i
END SUB


Turbo Pascalプログラム

program kousiten;
   const maxSize=100;   { 格子サイズの上限 }
         counted=-1;    { カウント済みであることを表す }
         maxGroup=100;  { 出来るグループ数の上限 }
   type pair=record index,member : integer end;
   var input,output : text;
       kousi : array[1..maxSize, 1..maxSize] of integer;
                        { kousi[x,y]は格子点(x,y)の指数 }
       group : array[1..MAXGROUP] of pair;
                        { group[i].index はi番目のグループの指数 }
                        { group[i].memberはそのメンバー数 }
       n,               { 格子サイズ }
       groupNo,         { 出来たグループの総数を表す }
       count,           { 1つのグループ内の要素数のカウント用 }
       x,y,i : integer;


   procedure find_solution;  { 答を求める }
      var x,y,i,
          count : integer;  { 1つのグループ内の要素数のカウント用 }

      procedure find_groups(x,y,i : integer);
      { 指数がiである点(x,y)と同じグループに属する点をすべて探してカウントし、
        カウントが済んだ点には印countedを付ける }
      begin
         if kousi[x,y]=i then begin
            inc(count);  kousi[x,y]:=counted;
            if x+1 < =n then find_groups(x+1,y,i);
            if y+1 < =n then find_groups(x,y+1,i);
            if 1 < =x-1 then find_groups(x-1,y,i);
            if 1 < =y-1 then find_groups(x,y-1,i);
         end;
      end; { of find_groups }

      procedure sort(k : integer);
      { k=0ならmemberに関して、k=1ならindexに関して、groupを昇順にソートする }
         label INSERTKEY;
         var key : pair;  i,j : integer;  found : boolean;
      begin
         for i:=2 to groupNo do begin
            key:=group[i];
            for j:=i-1 downto 0 do begin
               if k=0 then found:=key.member > =group[j].member
                      else found:=key.index > =group[j].index;
               if found then goto INSERTKEY;
               group[j+1]:=group[j];
            end;
            INSERTKEY: group[j+1]:=key;
         end;
      end; { of sort }

   begin { of find_solution }
      groupNo:=0;
      for x:=1 to n do
         for y:=1 to n do
            if kousi[x,y] <  > counted then begin
               { まだカウントが済んでいない点である }
               inc(groupNo);  group[groupNo].index:=kousi[x,y];
               count:=0; find_groups(x,y,kousi[x,y]);
               group[groupNo].member:=count;
            end;

      { memberを下の桁、indexを上の桁として、groupをradix sortする }
      sort(0); { 下の桁 }
      sort(1); { 上の桁 }
   end; { of find_solution }

begin
   { データを入力する }
   assign(input,'INPUT.DAT'); reset(input);
   read(input, n);
   for x:=1 to n do
      for y:=1 to n do read(input, kousi[x,y]);
   close(input);

   find_solution;  { グループとそのメンバー数を求める }

   { 答の出力 }
   assign(output, 'OUTPUT.SOL'); rewrite(output);
   for i:=1 to groupNo do writeln(output, group[i].index,' ',group[i].member);
   close(output);
end.