この記事の詳しい内容には毎日新聞の「皿を命令通り片付ける」ページから有料記事に進むことで読めます。
概要
洗った皿を片づけてくれるロボットがいます。ロボットは次の動作を繰り返して実行します。繰り返す回数は人間が伝えます。
- 大皿を2枚持つ
- 小皿を3枚持つ
- 持った皿を棚に片づける
例:大皿が4枚、小皿が6枚のとき、皿を全部片づけるには、ロボットに何回と伝えればよいでしょうか?
答え:
大皿が4枚、小皿が6枚なら、上の動作1-3を2回繰り返すと、皿は全部片づけられるので、答えは2回。
問題:大皿が8枚、小皿が12枚のとき、皿を全部片づけるには、ロボットに何回と伝えればよいでしょうか?
論理を考える
この問題を読んで、えー面倒だなあ、と思いながらまず考え付くのは、引く方法ではないでしょうか。 つまり、残りの皿の枚数から、ロボットが1回の動作で片づけられる皿の枚数を引く方法です。これは、小学校低学年程度の方法でしょう。そして、先のことは考えないでともかく突き進む方法です。引いた回数を数えると4回だと分かります。
次に考え付きそうなのは、皿の全枚数を、ロボットが1回の動作で片づけられる皿の枚数で割る方法です。計算すると大皿の場合も小皿の場合も4になるので、ロボットに4回と伝えればよいことが分かります。これは、全体から先を見通す方法で、小学校高学年程度の算数でしょうか。
もっと数学のセンスに富んだ方法があるのかも知れませんが、一般的な大人が面倒くさいと思いながら考え付くのはこの、引く方法と割る方法でしょう。
引くことを繰り返す
先のことは考えずに引き算を繰り返すことは、JavaScriptのwhile文で行えます。
let largePlates = 8; // 片づける大皿の数
let smallPlates = 12; // 片づける小皿の数
const plateL = 2; // 大皿は1度に2枚片付ける
const plateS = 3; // 小皿は1度に3枚片付ける
let count = 0;
// 大皿と小皿の残り枚数がともに0より大きい間
while (largePlates > 0 && smallPlates > 0) {
// 皿の枚数から1回当たりロボットが持つ枚数を引く
largePlates -= plateL;
smallPlates -= plateS;
// 実行回数をカウントする
count++;
print(count);
}
while文では、()内の条件がtrueである間、{と}の間に記述したコードが繰り返し実行されます。大皿の残り枚数であるlargePlatesから、ロボットが1回の動作で片づけられる大皿の枚数plateLを引きつづけます。小皿も同様です。回数を変数countで数えると、[コンソール]に1,2,3,4が表示されます。
この方法は一瞬で終わりますが、一定時間をおきながら引く方法もあります。
const timerID = window.setInterval(() => {
// 大皿と小皿の残り枚数がともに0より大きいなら
if (largePlates > 0 && smallPlates > 0) {
// 皿の枚数から1回当たりロボットが持つ枚数を引く
largePlates -= plateL;
smallPlates -= plateS;
// 実行回数をカウントする
count++;
// 大皿と小皿の残り枚数のどちらかでも0未満なら終わり
}
else {
window.clearInterval(timerID);
print(count);
}
}, 1000);
5秒待つと、[コンソール]に4が表示されます。
あらかじめ割る
全体を見渡し先の見当をつけてから問題に当たるには、次のようなコードが考えられます。
// 余りを求め、余りが0なら割り切れたということなので、商を求める
const remainL = largePlates % plateL;
if (remainL === 0) {
const dividedL = largePlates / plateL;
print(dividedL);
// 商はfor文の繰り返し回数に使用できる
for (let i = 0; i < dividedL; i++) {
largePlates -= plateL;
}
print(largePlates); // 0
}
const remainS = smallPlates % plateS;
if (remainS === 0) {
const dividedS = smallPlates / plateS;
print(dividedS);
for (let i = 0; i < dividedS; i++) {
smallPlates -= plateS;
}
print(smallPlates);
}
%演算子で余りを求め、余りが0なら割り切れたということなので、商を計算します。これが答えになります。その下のfor文は試し算(ためしざん)です。商の回数分だけ引き算を繰り返して残り枚数が0になることを確認しています。
サンプルプログラム
指定された回数だけ引き算をつづけるプログラムとしては次のものが記述できます。
const largePlates = 8; // 片づける大皿の数
const smallPlates = 12; // 片づける小皿の数
// 1回で片づけられる枚数
const plateL = 2; // 大皿は1度に2枚片付ける
const plateS = 3; // 小皿は1度に3枚片付ける
let remainL = largePlates; // 大皿の残り枚数
let remainS = smallPlates; // 小皿の残り枚数
function setup() {
noCanvas();
// 最初の状況
print('大皿: ' + remainL);
print('小皿: ' + remainS);
print('-----------------');
// 回数を指定するテキストフィールド
const textField = createInput('2');
textField.size(50);
textField.position(10, 20);
textField.style('text-align', 'center');
textField.elt.focus();
// [片づける]ボタン
const actionButton = setButton('片づける', {
x: 10,
y: 70
});
// マウスプレスで命令を出す
actionButton.mousePressed(() => {
cleanUp(int(textField.value()));
});
}
// 皿を片づける
function cleanUp(num) {
// 回数を数えるカウンタ
let count = 0;
const ms = 1000;
// msミリ秒にi回、引数の関数を実行する
const timerID = window.setInterval(() => {
// num回より多くは実行しない
if (count < num) {
count++;
print(count + '回めの片付け');
// 残り枚数から1度に片づけられる枚数を引く
// => 棚に入れたことになる
remainL -= plateL;
remainS -= plateS;
print('大皿の残枚数: ' + remainL);
print('小皿の残枚数: ' + remainS);
}
else {
print('終了')
window.clearInterval(timerID);
}
}, ms);
}
function setButton(label, pos) {
const button = createButton(label);
button.size(120, 40);
button.position(pos.x, pos.y);
return button;
}
テキストフィールドに繰り返したい回数を入力し[片づける]ボタンをクリックすると、[コンソール]に経過が表示されます。largePlatesに8、smallPlatesに12を代入し、テキストフィールドに4を入力して実行すると、期待通りの結果が得られます。
ただしこのプログラムでは、人間が間違って少ない回数を入力すると片づけられなかった皿が残り、多い回数を入力すると、残枚数がマイナスになります。
半端があるとき
お題では、片づける皿の枚数が、ロボットが1回で持てる枚数の倍数だったので、確実に割り切れました。しかし中学程度の数学(?)が扱えるなら、皿の枚数に半端があった場合も処理できるようにしたいところです。
そこで以降では、どのような枚数であっても、自動的に全部片づけるプログラムを考えてみます。論理としては、先の見通しを立てず引きつづけ、半端が出たらそのとき処理する、というものよりも、あらかじめ全体を見通しておいてから始める方がスマートな気がします。