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


問題4の解答と解説

 数式 ...α... において、括弧で囲まれた 部分数式 α の前後の括弧を削除してもよいかどうかは、α が どのような形をしているかと、左括弧の直前の演算子(left_op と呼ぼう; 演算子でなく左括弧の場合もある)と右括弧の直後の演算子(right_op と呼ぼう; 右括弧の場合もある)が何であるかとによって定まる。 すなわち、α 内の不要な括弧をすべて削除したとき α が 次のいずれの形をしているか(変数 temp_op_type で表わす)を考える:

 PLUS型: α は + または - を含んでいる。
 OTHERS型: α は * だけを含んでいるかまたは演算子を まったく含んでいない。

 OTHERS型の場合には、α の前後の括弧を無条件に削除できる。 PLUS型の場合には、left_op が - の場合(問題文の例 h)か、 * の場合(問題文の例 k)か、right_op が * である場合(問題文の例 d)だけが括弧を削除できない。

 以上の考えに基づいて不要な括弧対のすべてを空白で置き換えること を再帰的に行なうのが関数remove_parentheses である。この関数は、 文字位置 str から始まる部分数式(上記の α に相当するもの。 前後に括弧あり)を 対象にし、この部分数式の末尾の右括弧 ')' を指すポインタを関数値として 返す。また,この部分数式の型を第3引数 op_type を介して返すとともに、 この部分数式を囲む括弧対が空白に置き換えられたか否かを 第4引数 removed を介して返す。第2引数 left_op は、対象としている 部分数式を囲んでいる左括弧の直前の演算子(左括弧の場合もある)の 種類を表わす。
 入力した文字列は、前後に括弧を付けて関数remove_parentheses に渡される。 remove_parenthesesによって不要な括弧を空白に置き換えた後、 関数 trim によって空白を消去して出力する。

 周知のように、スタックを使うとどんな再帰的プログラムも再帰のない 繰り返し型のプログラムに直すことができるが、この問題の場合、次の 定理を使うと数式内の対応する括弧対をすべて求めることができるので、 スタックを使わずに本質的に再帰のないプログラムを書くこともできる。

  定理  左括弧 ( と 右括弧 ) だけから成る 文字列が整合しているとは、左右の括弧のすべてがきちんと対応して いることをいう。 文字列のプレフィックスとは、その文字列の先頭部分の 文字列のことである(空文字列の場合も、文字列全体の場合も含む)。 文字列 γ に対して

   f(γ) = (γ内の左括弧の個数) - (γ内の右括弧の個数)

と定義する。このとき、左括弧と右括弧だけからなる文字列 α が整合している必要十分条件は、f(α)=0 かつ α の 任意のプレフィックス β に対して f(β)≧0 となることである。
したがって、数式 α(β において β の直前の 左括弧に対応する右括弧を求めるには、β を左から順に1文字ずつ スキャンしていき、初めて f((β')=0 となる β' を求めればよい。 β' の末尾の1文字が対応する右括弧である。


Cプログラム(1)

#include <stdio.h>
#include <string.h>

typedef enum { false, true } boolean;
typedef enum { PLUS='+', MINUS='-', ASTERISK='*', OTHERS=NULL } OP;

OP chr2op(char ch)  /* 文字(演算子)の種類を分類する */
{
   switch (ch) {
      case PLUS: case MINUS: case ASTERISK: break;
      default: ch = OTHERS;  
   }
   return ch;
}

char *remove_parentheses(char *str, OP left_op,
                                    OP *op_type, boolean *removed)
{
   OP temp_op_type,latest_op,right_op,inner_op; char *pt; boolean rmvd;

   pt = str; temp_op_type = latest_op = OTHERS;
   while (*(++pt) != ')') {
      switch (*pt) {
          case PLUS: case MINUS: 
               temp_op_type = PLUS; 
               break;
          case '(':
               pt = remove_parentheses(pt,latest_op,&inner_op,&rmvd);
               if (rmvd==true && temp_op_type==OTHERS) temp_op_type = inner_op;
               break;
      }
      latest_op = chr2op(*pt); 
   }
   right_op = chr2op(*(pt+1)); *removed = false;
   if (temp_op_type==OTHERS ||
       left_op!=MINUS && left_op!=ASTERISK && right_op!=ASTERISK) {
      *str = *pt = ' '; *removed = true;
   }
   *op_type = temp_op_type; return pt;
}

void trim(char *str)  /* 文字列strの中の空白文字を削除する */
{
   char *pt;

   pt = str;
   do {
      if (*pt!=' ') { *str = *pt; str++; }
   } while (*(++pt) != NULL);
   *str = '\0';
}

void main()
{
   char str[103];

   str[0] = '(';
   printf("数式を入力してください。\n"); scanf("%s",str+1);
   strcat(str,") ");
   remove_parentheses(str,OTHERS,NULL,NULL);
   trim(str); printf("%s\n",str);
}


BASICプログラム(1)

DEFINT A-Z

CONST plus = 1, minus = 2, asterisk = 3, others = 0
CONST true = -1, false = 0

DECLARE FUNCTION chr2OP (ch AS STRING)
DECLARE FUNCTION removeParentheses (str, leftOP, OPtype, removed)
DECLARE SUB trim ()

COMMON SHARED strn AS STRING

   PRINT "数式を入力して下さい。": INPUT strn$
   strn$ = "(" + strn$ + ") "
   dummy1 = removeParentheses(1, others, dummy2, dummy3)
   CALL trim: PRINT strn$
END

FUNCTION chr2OP (ch AS STRING)  ' 文字(演算子)の種類を分類する
   SELECT CASE ch
      CASE "+":  chr2OP = plus
      CASE "-":  chr2OP = minus
      CASE "*":  chr2OP = asterisk
      CASE ELSE: chr2OP = others
   END SELECT
END FUNCTION

FUNCTION removeParentheses (str, leftOP, OPtype, removed)
   tempOPtype = others: latestOP = others
   pt = str + 1: ch$ = MID$(strn$, pt, 1)
   WHILE (ch$ <> ")")
      SELECT CASE ch$
         CASE "+", "-"
              tempOPtype = plus
         CASE "("
              pt = removeParentheses((pt), (latestOP), innerOP, rmvd)
              IF (rmvd = true) AND (tempOPtype = others) THEN
                 tempOPtype = innerOP
              END IF
      END SELECT
      latestOP = chr2OP(ch$): pt = pt + 1: ch$ = MID$(strn$, pt, 1)
   WEND
   removed = false: ch$ = MID$(strn$, pt + 1, 1): rightOP = chr2OP(ch$)
   left = (leftOP <> minus) AND (leftOP <> asterisk)
   IF (left AND (rightOP <> asterisk)) OR (tempOPtype = others) THEN
      MID$(strn$, str, 1) = " ": MID$(strn$, pt, 1) = " ": removed = true
   END IF
   OPtype = tempOPtype: removeParentheses = pt
END FUNCTION

SUB trim  ' 文字列strn$の中の空白文字を削除する
   dummy$ = ""
   FOR i = 1 TO LEN(strn$)
       IF MID$(strn, i, 1) <> " " THEN
          dummy$ = dummy$ + MID$(strn$, i, 1)
       END IF
   NEXT i
   strn$ = dummy$
END SUB


Pascalプログラム(1)

program problem4;
type OP = (PLUS,MINUS,ASTERISK,OTHERS);
var   strn: string[103];

function chr2op(ch: char): OP;  { 文字(演算子)の種類を分類する }
begin
   case ch of
      '+': chr2op := PLUS;
      '-': chr2op := MINUS;
      '*': chr2op := ASTERISK;
      else chr2op := OTHERS;
   end;
end; { chr2op }

function remove_parentheses(str:integer; left_op:OP; var op_type:OP;
                            var removed:boolean):integer;
var temp_op_type,latest_op,right_op,inner_op:OP;
    pt: integer; rmvd: boolean;
begin
   pt := str; temp_op_type := OTHERS; latest_op := OTHERS; inc(pt);
   while strn[pt]<>')' do begin
      case strn[pt] of
         '+','-' : temp_op_type := PLUS;
             '(' : begin 
                      pt := remove_parentheses(pt,latest_op,inner_op,rmvd);
                      if (rmvd=true) and (temp_op_type=OTHERS)
                      then temp_op_type := inner_op;
                   end;
      end;
      latest_op := chr2op(strn[pt]); inc(pt);
   end;
   right_op := chr2op(strn[pt+1]); removed := false;
   if (temp_op_type=OTHERS) or
      (left_op<>MINUS) and (left_op<>ASTERISK) and (right_op<>ASTERISK)
   then begin
      strn[str] := ' '; strn[pt] := ' '; removed := true;
   end;
   op_type := temp_op_type; remove_parentheses := pt;
end; { remove_parentheses }


procedure trim;  { 文字列strnの中の空白文字を削除する }
begin while pos(' ',strn)>0 do delete(strn,pos(' ',strn),1) end;

var i,dummy1: integer; dummy2: OP; dummy3: boolean;

begin { main }
   writeln('数式を入力して下さい。'); readln(strn);
   strn := '('+strn+') ';
   dummy1 := remove_parentheses(1,OTHERS,dummy2,dummy3);
   trim; writeln(strn);
end.


JOIホームページへ戻る

JOI'95へ戻る