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


問題2.

 数式とは何かを、次のように定義する:

  1. 英字1文字は、それだけで数式である。これをオぺランドと呼ぶ。
  2. α および β がそれぞれ数式なら、(α+β) および (α*β) はそれぞれ数式であり、
    + と * を演算子と呼ぶ。
  3. 上記 1 および 2 で定義されるものだけを数式という。

例えば、(a*(b+(c+d))) は数式であり、オペランドとして a, b, c, d をそれぞれ1個ずつ、 演算子として + を2個と * を1個含んでいる。
一方、a+b*c や (a+b)*c は、( )がきちんと付いていないので数式ではない。

 このような数式の値を計算する際、+ の計算には p 単位時間、* の計算には q 単位時間それぞれかかり、部分数式 α, β および演算子 op に対して (α op β) の計算にかかる時間は

   opの計算時間 + max{αの計算にかかる時間, βの計算にかかる時間}
であるとする。ただし、オペランドの計算にかかる時間は0である。
例えば、p=1, q=3 のとき
  (((a+(b+(c+d)))*e)*f) の計算にかかる時間は 9
  (((a+b)+(c*d))+(e*f)) の計算にかかる時間は 5
である。

 さて、+, * に次のような交換律と結合律が成り立っているとする:

  交換律 (α+β) = (β+α), (α*β) = (β*α)
  結合律 (α+(β+γ)) = ((α+β)+γ), (α*(β*γ)) = ((α*β)*γ)
ここで、α, β, γ は任意の数式である。 このとき、与えられた数式と等しい数式のうち、計算時間が最小のものを求める プログラムを書きたい。例えば、
  (((a+(b+(c+d)))*e)*f), ((a+b)*(((c*d)+e)*f))
はそれぞれ
  (((a+b)+(c+d))*(e*f)), (((a+b)*f)*((c*d)+e))
と等しく、後者の計算にかかる時間はそれぞれ 6,7 で最小である。

 下記のプログラムでは、結合律だけを考慮して計算時間が最小の数式を一つだけ 求めている不完全なプログラムである。

 (1) 関数(手続き、プロシージャ) make_tree (Quick BASIC では makeTree) を完成させよ。 空欄は関数本体の一部分となっているが、必要なら、空欄になって いない部分も含めて関数全体を書き直してよい。
 (2) 関数(手続き、プロシージャ) compute_time (Quick BASIC では computeTime)   を修正して、交換律も考慮したものとせよ。 どこをどのように直したらよいかを、解答用紙に印刷されているプログラムに加筆・修正して示せ。


Cプログラム(2)

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

#define MaxStrLen 256                  /* 数式の最大長 */
#define max(x,y) ((x)>(y) ? (x) : (y))
#define min(x,y) ((x)>(y) ? (y) : (x))

typedef struct node {                  /* 2分木の節(ノード)を表す */
            char op;                   /* 演算子の種別 */
            struct node *left,*right;  /* 左の子、右の子へのポインタ */
        } CELL;
typedef struct node *TREE;
int p,q;

TREE make_tree(char *expr)
/* 文字列exprで与えられる数式を2分木に変換し、その根へのポインタを関数値とする */
{
    int i,length,paren_balance; TREE root;

    length=strlen(expr);

    if (length==1) {
        root = (TREE)malloc(sizeof(CELL));
        root->op = expr[0];
        root->left = root->right = NULL;
        return root;
    }

 -----------------------------------------------------------------
|                                                                 |
|                                                                 |
|                                                                 |
|                                                                 |
|              空欄(1)                        |
|                                                                 |
|                                                                 |
|                                                                 |
|                                                                 |
 -----------------------------------------------------------------

int time(TREE root)  /* rootを根とする2分木(=数式)の計算時間を求める */
{
    switch (root->op) {
      case '+': return max(time(root->left), time(root->right)) + p;
      case '*': return max(time(root->left), time(root->right)) + q;
      default:  return 0;
    }
}

void print_tree(TREE root)  /* rootを根とする2分木(=数式)をプリントする */
{
    if (root->left != NULL) {
        putchar('('); print_tree(root->left);
    }
    putchar(root->op);
    if (root->right != NULL) {
        print_tree(root->right); putchar(')');
    }
}

void assoc_left(TREE root)  /* rootにおいて左結合律を適用する */
{
    TREE rootleft;

    rootleft = root->left; root->left = (root->left)->left;
    rootleft->left = rootleft->right; rootleft->right = root->right;
    root->right = rootleft;
}

void assoc_right(TREE root)  /* rootにおいて右結合律を適用する */
{
    TREE rootright;

    rootright = root->right; root->right = (root->right)->right;
    rootright->right = rootright->left; rootright->left = root->left;
    root->left = rootright;
}

void restore_left(TREE root)  /* assoc_leftする前の状態へ戻す */
{
    assoc_right(root);
}

void restore_right(TREE root)  /* assoc_rightする前の状態へ戻す */
{
    assoc_left(root);
}

int compute_time(TREE root)
/* rootを根とする2分木によって表された数式の最小計算時間を求め関数値とし、
   同時に、その最小値を与える数式を表すように2分木を作り直す */
{
    int label;  /* 場合分け用ラベル */
    int optime,lefttime,righttime,oldtime,newtime,newtime_left,newtime_right;

    if (isalpha(root->op)) return 0;  /* オぺランドの場合 */

    optime = op_time(root->op);       /* 演算子の計算時間 */
    label = 2*(root->op == (root->left)->op  ? 1 : 0) +
              (root->op == (root->right)->op ? 1 : 0);
    switch (label) {
      case 0:
              return max(compute_time(root->left),
                         compute_time(root->right)) + optime;
      case 1:
              lefttime  = compute_time(root->left);
              righttime = compute_time(root->right);
              oldtime = max(lefttime,righttime)+optime;

              assoc_right(root);
              lefttime  = compute_time(root->left);
              righttime = compute_time(root->right);
              newtime = max(lefttime,righttime)+optime;
              if (oldtime>newtime) return newtime;
              else {
                  restore_right(root); return oldtime;
              }
      case 2: case 3:
              lefttime  = compute_time(root->left);
              righttime = compute_time(root->right);
              oldtime = max(lefttime,righttime)+optime;

              assoc_left(root);
              lefttime  = compute_time(root->left);
              righttime = compute_time(root->right);
              newtime = max(lefttime,righttime)+optime;
              if (oldtime>newtime) return newtime;
              else {
                  restore_left(root); return oldtime;
              }
    }
}

int op_time(char op)  /* 演算子の計算時間を求める */
{
    switch (op) {
      case '+': return p;
      case '*': return q;
    }
}

void main()
{
    char expr[MaxStrLen]; TREE root;

    printf("数式を入力して下さい。"); gets(expr);
    printf("p,qの値を入力して下さい。"); scanf("%d %d", &p,&q);

    root = make_tree(expr);
    printf("与えられた数式は\n    ");
    print_tree(root);
    printf("\nで、その計算時間は%dである。", time(root));
    printf("\n最小計算時間は%dで、", compute_time(root));
    printf("\nそれは次のように計算すればよい:\n    ");
    print_tree(root);
}


QuickBASICプログラム(2)

DEFINT A-Z
DECLARE FUNCTION makeTree (s$)
DECLARE SUB printTree (root)
DECLARE SUB assocLeft (root)
DECLARE SUB assocRight (root)
DECLARE SUB restoreLeft (root)
DECLARE SUB restoreRight (root)
DECLARE FUNCTION max% (x%, y%)
DECLARE FUNCTION min% (x%, y%)
DECLARE FUNCTION time (root)
DECLARE FUNCTION computeTime (root)
DECLARE FUNCTION operatorTime (op$)

CONST maxMemorySize = 1000  ' 空き領域の大きさの上限
CONST nil = -1              ' 空ポインタ
TYPE treeNode               ' 2分木の節(ノード)を表す
    op    AS STRING * 1     ' 演算子の種別
    left  AS INTEGER        ' 左の子へのポインタ
    right AS INTEGER        ' 右の子へのポインタ
END TYPE
DIM SHARED tree(maxMemorySize) AS treeNode
COMMON SHARED p, q
COMMON SHARED getMemory     ' 空き領域(配列tree)の先頭位置の添字

    PRINT "数式を入力して下さい。": INPUT expr$
    PRINT "p,qの値を入力して下さい。": INPUT p, q

    getMemory = 0
    root = makeTree(expr$)
    PRINT "与えられた数式は": PRINT "    ";
    CALL printTree(root): PRINT
    PRINT "で、その計算時間は"; time(root); "である。"
    PRINT "最小計算時間は"; computeTime(root); "で、"
    PRINT "それは次のように計算すればよい:": PRINT "    ";
    CALL printTree(root)
END

SUB assocLeft (root)  ' rootにおいて左結合律を適用する
    rootleft = tree(root).left
    tree(root).left = tree(tree(root).left).left
    tree(rootleft).left = tree(rootleft).right
    tree(rootleft).right = tree(root).right
    tree(root).right = rootleft
END SUB

SUB assocRight (root)  ' rootにおいて右結合律を適用する
    rootright = tree(root).right
    tree(root).right = tree(tree(root).right).right
    tree(rootright).right = tree(rootright).left
    tree(rootright).left = tree(root).left
    tree(root).left = rootright
END SUB

FUNCTION computeTime (root)
' rootを根とする2分木によって表された数式の最小計算時間を求め関数値とし、
' 同時に、その最小値を与える数式を表すように2分木を作り直す
    IF "a" <= tree(root).op AND tree(root).op <= "z" THEN
        computeTime = 0: EXIT FUNCTION
    END IF

    opTime = operatorTime(tree(root).op)
    label = 0
    IF tree(root).op = tree(tree(root).right).op THEN label = label + 1
    IF tree(root).op = tree(tree(root).left).op THEN label = label + 2
    SELECT CASE label
      CASE 0
          ltime = computeTime(tree(root).left)
          rtime = computeTime(tree(root).right)
          computeTime = max(ltime, rtime) + opTime
          EXIT FUNCTION
      CASE 1
          lefttime = computeTime(tree(root).left)
          righttime = computeTime(tree(root).right)
          oldtime = max(lefttime, righttime) + opTime
          CALL assocRight(root)
          lefttime = computeTime(tree(root).left)
          righttime = computeTime(tree(root).right)
          newtime = max(lefttime, righttime) + opTime
          IF oldtime > newtime THEN
              computeTime = newtime: EXIT FUNCTION
          ELSE
              CALL restoreRight(root)
              computeTime = oldtime: EXIT FUNCTION
          END IF
      CASE 2, 3
          lefttime = computeTime(tree(root).left)
          righttime = computeTime(tree(root).right)
          oldtime = max(lefttime, righttime) + opTime
          CALL assocLeft(root)
          lefttime = computeTime(tree(root).left)
          righttime = computeTime(tree(root).right)
          newtime = max(lefttime, righttime) + opTime
          IF oldtime > newtime THEN
              computeTime = newtime: EXIT FUNCTION
          ELSE
              CALL restoreLeft(root)
              computeTime = oldtime: EXIT FUNCTION
          END IF
    END SELECT
END FUNCTION

FUNCTION makeTree (expr$)
' 文字列exprで与えられる数式を2分木に変換し、その根へのポインタを関数値とする
    length = LEN(expr$)
    IF length = 1 THEN
        newNode = getMemory: getMemory = getMemory + 1
        tree(newNode).op = MID$(expr$, 1, 1)
        tree(newNode).left = nil: tree(newNode).right = nil
        makeTree = newNode: EXIT FUNCTION
    END IF

 -----------------------------------------------------------------
|                                                                 |
|                                                                 |
|                                                                 |
|                                                                 |
|              空欄(1)                        |
|                                                                 |
|                                                                 |
|                                                                 |
|                                                                 |
 -----------------------------------------------------------------
END FUNCTION

FUNCTION max (x, y)
    IF x > y THEN max = x ELSE max = y
END FUNCTION

FUNCTION min (x, y)
    IF x > y THEN min = y ELSE min = x
END FUNCTION

FUNCTION operatorTime (op$)  ' 演算子の計算時間を求める
    SELECT CASE op$
      CASE "+": operatorTime = p
      CASE "*": operatorTime = q
    END SELECT
END FUNCTION

SUB printTree (root)  ' rootを根とする2分木(=数式)をプリントする
    IF tree(root).left <> nil THEN
        PRINT "("; : CALL printTree(tree(root).left)
    END IF
    PRINT tree(root).op;
    IF tree(root).right <> nil THEN
        CALL printTree(tree(root).right): PRINT ")";
    END IF
END SUB

SUB restoreLeft (root)  ' assocLeftする前の状態へ戻す
    CALL assocRight(root)
END SUB

SUB restoreRight (root)   ' assocRightする前の状態へ戻す
    CALL assocLeft(root)
END SUB

FUNCTION time (root)  ' rootを根とする2分木(=数式)の計算時間を求める
    SELECT CASE tree(root).op
      CASE "+"
          time = max(time(tree(root).left), time(tree(root).right)) + p
      CASE "*"
          time = max(time(tree(root).left), time(tree(root).right)) + q
      CASE ELSE
          time = 0
    END SELECT
END FUNCTION


Pascalプログラム(2)

program problem2;
type TREE = ^node;
     node = record              {2分木の節(ノード)を表す}
              op:char;          {演算子の種別}
              left,right:TREE;  {左の子、右の子へのポインタ}
            end;
var p,q:integer;

    function max(x,y:integer):integer;
    begin
        if x>y then max:=x else max:=y
    end; {max}

    function min(x,y:integer):integer;
    begin
        if x>y then min:=y else min:=x
    end; {min}

    procedure make_tree(var root:TREE; expr:string);
    {文字列exprで与えられる数式を2分木に変換し、その根へのポインタを返す}
    label exit_proc,exit_for;
    var i,len,paren_balance:integer;
    begin
        len := length(expr);
        if len=1 then begin
            new(root); root^.op := expr[1];
            root^.left := nil; root^.right := nil;
            goto exit_proc
        end;

         -----------------------------------------------------------
        |                                                           |
        |                                                           |
        |                                                           |
        |                                                           |
        |              空欄(1)                      |
        |                                                           |
        |                                                           |
        |                                                           |
        |                                                           |
         -----------------------------------------------------------
    end; {make_tree}

    function time(root:TREE):integer; 
    {rootを根とする2分木(=数式)の計算時間を求める}
    begin
        case root^.op of
          '+': time := max(time(root^.left), time(root^.right)) + p;
          '*': time := max(time(root^.left), time(root^.right)) + q;
          else time := 0;
        end
    end; {time}

    procedure print_tree(root:TREE);
    {rootを根とする2分木(=数式)をプリントする}
    begin
        if root^.left<>nil then begin
            write('('); print_tree(root^.left)
        end;
        write(root^.op);
        if root^.right<>nil then begin
            print_tree(root^.right); write(')')
        end
    end; {print_tree}

    function compute_time(var root:TREE):integer;
    {rootを根とする2分木によって表された数式の最小計算時間を求め関数値とし、}
    {同時に、その最小値を与える数式を表すように2分木を作り直す}
    label exit_func;
    var caselabel,
        lefttime,righttime,optime,
        oldtime,newtime,newtime_left,newtime_right:integer;

        function op_time(op:char):integer; {演算子の計算時間を求める}
        begin
            case op of
                 '+': op_time:=p;
                 '*': op_time:=q;
            end
        end; {op_time}

        procedure assoc_left(var root:TREE); {rootにおいて左結合律を適用する}
        var rootleft:TREE;
        begin
            rootleft := root^.left; root^.left := (root^.left)^.left;
            rootleft^.left := rootleft^.right; rootleft^.right := root^.right;
            root^.right := rootleft
        end; {assoc_left}

        procedure assoc_right(var root:TREE); {rootにおいて右結合律を適用する}
        var rootright:TREE;
        begin
            rootright := root^.right; root^.right := (root^.right)^.right;
            rootright^.right := rootright^.left; rootright^.left := root^.left;
            root^.left := rootright
        end; {assoc_right}

        procedure restore_left(var root:TREE); {assoc_leftする前の状態へ戻す}
        begin assoc_right(root) end;

        procedure restore_right(var root:TREE); {assoc_rightする前の状態へ戻す}
        begin assoc_left(root) end;

    begin {compute_time}
        if  ('a' <= root^.op) and (root^.op <= 'z') then begin
            compute_time:=0; goto exit_func
        end;

        optime := op_time(root^.op);
        caselabel := 0;
        if root^.op=(root^.right)^.op then caselabel := caselabel+1;
        if root^.op=(root^.left)^.op  then caselabel := caselabel+2;
        case caselabel of
          0: begin
                 compute_time := max(compute_time(root^.left),
                                     compute_time(root^.right)) + optime;
                 goto exit_func;
             end;
          1: begin
                 lefttime  := compute_time(root^.left);
                 righttime := compute_time(root^.right);
                 oldtime := max(lefttime,righttime)+optime;

                 assoc_right(root);
                 lefttime  := compute_time(root^.left);
                 righttime := compute_time(root^.right);
                 newtime := max(lefttime,righttime)+optime;

                 if oldtime>newtime then begin
                     compute_time := newtime; goto exit_func
                 end
                 else begin
                     restore_right(root);
                     compute_time := oldtime; goto exit_func
                 end
             end;
        2,3: begin                 lefttime  := compute_time(root^.left);
                 righttime := compute_time(root^.right);
                 oldtime := max(lefttime,righttime)+optime;

                 assoc_left(root);
                 lefttime  := compute_time(root^.left);
                 righttime := compute_time(root^.right);
                 newtime := max(lefttime,righttime)+optime;

                 if oldtime>newtime then begin
                     compute_time := newtime; goto exit_func
                 end
                 else begin
                     restore_left(root);
                     compute_time := oldtime; goto exit_func
                 end
             end
        end;
    exit_func:
    end; {compute_time}

var expr:string; root:TREE;

begin {main}
    write('数式を入力して下さい。'); readln(expr);
    write('p,qの値を入力して下さい。'); readln(p,q);

    make_tree(root,expr);
    writeln('与えられた数式は'); write('    ');
    print_tree(root); writeln;
    writeln('で、その計算時間は', time(root), 'である。');
    writeln('最小計算時間は', compute_time(root), 'で、');
    writeln('それは次のように計算すればよい:'); write('    ');
    print_tree(root); writeln
end.


JOIホームページへ戻る

JOI'96へ戻る