この記事の詳しい内容には毎日新聞の「皿を命令通り片付ける 」ページから有料記事に進むことで読めます。
概要
洗った皿を片づけてくれるロボットがいます。ロボットは次の動作を繰り返して実行します。繰り返す回数は人間が伝えます。
大皿を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回で持てる枚数の倍数だったので、確実に割り切れました。しかし中学程度の数学(?)が扱えるなら、皿の枚数に半端があった場合も処理できるようにしたいところです。
そこで以降では、どのような枚数であっても、自動的に全部片づけるプログラムを考えてみます。論理としては、先の見通しを立てず引きつづけ、半端が出たらそのとき処理する、というものよりも、あらかじめ全体を見通しておいてから始める方がスマートな気がします。
* 余談ですが、たとえばプリンターを操作するソフトウェアでは、先の見通しは立てていないように思われます。印刷枚数をプリンター内の残枚数よりも多く指定した場合、プリンターは印刷を開始し、用紙がなくなったら用紙切れの警告を発します。これに対し、先の見通しを立てる最たるものが最近の車に搭載されている安全技術です。
スバルのアイサイト は最も優れた安全技術の1つだと言われています。
全体を見通す
今の場合全体を見通すとは、先に皿の枚数をロボットが1回で持てる枚数で割って、余りが出るか割り切れるか調べておくことです。
割り切れた場合には前のサンプルと同じ処理で済みます。余りがあったときには後に回して処理します。次のコードでは、後に回すということを、タイマーの入れ子で行っています。余りはロボットが1回で持てる枚数より小さいので、1回で処理できます。
// 皿の枚数に半端があっても、大皿と小皿別に回数を求める
// 余りと商を利用する
const largePlates = 5; // 片づける大皿の数
const smallPlates = 11; // 片づける小皿の数
let count = 0; // 片付けの回数
function setup() {
noCanvas();
// 最初の状況
print('大皿: ' + largePlates);
print('小皿: ' + smallPlates);
print('-----------------');
// [片づける]ボタン
const actionButton = setButton('片づける', {
x: 10,
y: 20
});
// マウスプレスで命令を出す
actionButton.mousePressed(() => {
cleanUp();
});
}
function cleanUp() {
const plateL = 2; // 大皿は2枚
const plateS = 3; // 大皿は2枚
cleanUpPlate('大皿', largePlates, plateL);
cleanUpPlate('小皿', smallPlates, plateS);
}
function cleanUpPlate(type, plateNum, plate) {
// 片付け作業に入る前に、計算しておく
// 余り = 皿の枚数 % ロボットが1回に持てる枚数
// 余り => 半端な皿の枚数
const remain = plateNum % plate;
// 商 => 半端の1回をのぞいた予定作業回数
const divied = int(plateNum / plate);
// 片付けの回数を数える
let count = 0;
const ms = 1000;
// msミリ秒おきに片付ける
const timerID1 = window.setInterval(() => {
count++;
plateNum -= plate;
print(type + 'の片付け: ' + count + '回め 残数: ' + plateNum);
// 半端があるときは
if (count >= divied) {
if (remain !== 0) {
// 1回限りのタイマーで片づける
const timerID2 = window.setTimeout(() => {
count++;
print(type + 'の片付け: ' + count + '回め');
print(type + 'の半端の' + remain + '枚を片づけた');
window.clearTimeout(timerID2);
print(type + '終わり');
}, ms);
// 予定回数に達している
}
else {
print(type + '終わり');
}
window.clearInterval(timerID1);
}
}, ms);
}
function setButton(label, pos) {
const button = createButton(label);
button.size(120, 40);
button.position(pos.x, pos.y);
return button;
}
下図は実行結果です。
皿オブジェクトの移動
皿の残数から、ロボットが1回で片づけられる皿の数を引いていくと、最後はゼロになります。これは、ロボットが、皿の置いてあったテーブルから皿を全部棚に移したということです。最後にこれを表してみましょう。
皿はJavaScriptのObjectオブジェクトで表し、テーブルと食器棚は配列で表し、皿オブジェクトを入れます。ロボットは皿オブジェクトを、テーブルの配列から食器棚の配列に移します。図にすると下図のようなイメージです。
移動する論理
皿オブジェクトの配列から配列への移動と言っても、元の配列から要素を削除し、その削除した要素を覚えておいて、別の配列に追加するだけです。
テーブルにある皿を含む、移動前の配列をbeforeArray、食器棚に移した皿を入れる、移動後の配列をafterArrayとします。またロボットが1回で移すことのできる枚数(2か3)をplateNumOnceとします。
要素の削除にはArray.splice()メソッドが使用できます。splice()の引数に0とplateNumOnceを指定すると、beforeArrayの先頭からplateNumOnce分の要素が削除でき、削除した要素を持つ新しい配列が得られます。これをforループでafterArrayに追加します。
// 片づける前の先頭からplateNumOnce分、要素を削除 => plateNumOnce分だけ片づけたので、Plateオブジェクトが減る
let deletedArray = beforeArray.splice(0, plateNumOnce);
// 削除した要素を片づけた後の配列に移す
for (let i = 0; i < deletedArray.length; i++) {
afterArray.push(deletedArray[i]);
}
皿のオブジェクトは簡単で、大皿か小皿かを表すだけです。
// Plateクラス
class Plate {
constructor(type) {
this.type = type; // 'large'か'small'
}
}
下記は、移動の論理を含んだ全コードです。大皿と小皿の枚数として4と6をそれぞれ指定しています。コードを実行し、[コンソール]を見ると、皿オブジェクトが配列から配列に移動しているのが分かります。
const largePlateNum = 4;
const smallPlateNum = 6;
let largeOnTable = []; // 片づける前の大皿を入れる配列
let smallOnTable = []; // 片づける前の小皿を入れる配列
let largeOnBoard = []; // 片づけた大皿を入れる配列
let smallOnBoard = []; // 片づけた小皿を入れる配列
function setup() {
noCanvas();
// 大皿の数だけ大皿タイプのPlateオブジェクトを大皿の配列に入れる
for (let i = 0; i < largePlateNum; i++) {
largeOnTable.push(new Plate('large'));
}
// 小皿の数だけ小皿タイプのPlateオブジェクトを小皿の配列に入れる
for (let i = 0; i < smallPlateNum; i++) {
smallOnTable.push(new Plate('small'));
}
// [片づける]ボタン
const actionButton = setButton('片づける', {
x: 10,
y: 20
});
// マウスプレスで命令を出す
actionButton.mousePressed(() => {
// 片づける前の皿を入れた配列と、片づけた皿を入れる配列を渡す
cleanUpPlate(largeOnTable, largeOnBoard);
cleanUpPlate(smallOnTable, smallOnBoard);
});
print(largeOnTable);
print(smallOnTable);
}
// 片づける前の皿を入れた配列と、片づけた皿を入れる配列を受け取る
function cleanUpPlate(beforeArray, afterArray) {
// 受け取った片づける前のPlateオブジェクトのタイプを調べる
// 'large'か'small'
const type = beforeArray[0].type;
// 皿の数
const platesNum = beforeArray.length;
// 1回で持てる皿の数
let plateNumOnce;
if (type === 'large') {
plateNumOnce = 2; // 大皿は2枚持てる
}
else if (type === 'small') {
plateNumOnce = 3; // 小皿は3枚持てる
}
const remain = platesNum % plateNumOnce; // 余り
const divied = int(platesNum / plateNumOnce); // 商
let count = 0; // 回数を数えるカウンタ
const ms = 1000; // msミリ秒
const timerID1 = window.setInterval(() => {
count++;
print(type + 'の片付け: ' + count + '回め');
// 片づける前の先頭からplateNumOnce分、要素を削除 => plateNumOnce分だけ片づけたので、Plateオブジェクトが減る
let deletedArray = beforeArray.splice(0, plateNumOnce);
// 削除した要素を片づけた後の配列に移す
for (let i = 0; i < deletedArray.length; i++) {
afterArray.push(deletedArray[i]);
}
print(beforeArray);
print(afterArray);
if (count >= divied) {
if (remain !== 0) {
const timerID2 = window.setTimeout(() => {
// 片づける前の先頭からremain分、要素を削除 => remain分だけ片づけたので、片づける前の配列は空になる
let deletedArray = beforeArray.splice(0, remain);
// 削除した要素を片づけた後の配列に移す
for (let i = 0; i < deletedArray.length; i++) {
afterArray.push(deletedArray[i]);
}
print(type + 'の半端の' + remain + '枚を片づけた');
print(beforeArray)
print(afterArray);
window.clearTimeout(timerID2);
print(type + '終わり');
}, ms);
// 予定回数に達している
}
else {
print(type + '終わり');
}
window.clearInterval(timerID1);
}
}, ms);
}
function setButton(label, pos) {
const button = createButton(label);
button.size(120, 40);
button.position(pos.x, pos.y);
return button;
}
// Plateクラス
class Plate {
constructor(type) {
this.type = type; // 'large'か'small'
}
}
下図は実行結果です。(1)は配列の移動前の状態を示しています。(2)は大皿の1回めの移動で、テーブルに2枚あり、食器棚に2枚移ったことを表しています。
可視化
皿やロボットなどの画像を読み込んで、テーブルから食器棚への大皿と小皿の移動を可視化した例です。
const largePlateNum = 19; // 大皿の数
const smallPlateNum = 23; // 小皿の数
let largeOnTable = []; // 片づける前の大皿を入れる配列
let smallOnTable = []; // 片づける前の小皿を入れる配列
let largeOnBoard = []; // 片づけた大皿を入れる配列
let smallOnBoard = []; // 片づけた小皿を入れる配列
// 大皿と小皿のイメージを入れる配列
let largImages, smallImages;
let robotImage;
function preload() {
const large = loadImage('images/large.png');
const largeUpright = loadImage('images/large_upright.png');
const small = loadImage('images/small.png');
const smallUpright = loadImage('images/small_upright.png');
// 大皿のイメージを大皿専用の配列に入れる
largImages = [large, largeUpright];
// 小皿のイメージを小皿専用の配列に入れる
smallImages = [small, smallUpright];
robotImage = loadImage('images/cleanRobot.png');
}
function setup() {
createCanvas(600, 300);
noStroke();
// 大皿の数だけ繰り返す
for (let i = 0; i < largePlateNum; i++) {
// 大皿タイプのPlateオブジェクトを作成して大皿用の配列に入れる
largeOnTable.push(new Plate('large'));
}
// // 小皿の数だけ繰り返す
for (let i = 0; i < smallPlateNum; i++) {
// 小皿タイプのPlateオブジェクトを作成して小皿用の配列に入れる
smallOnTable.push(new Plate('small'));
}
const actionButton = setButton('片づける', {
x: 450,
y: 260
});
actionButton.mousePressed(() => {
// 片づける前の皿を入れた配列と、片づけた皿を入れる配列を渡す
cleanUpPlate(largeOnTable, largeOnBoard);
cleanUpPlate(smallOnTable, smallOnBoard);
});
}
function draw() {
background(220);
// 皿を置く台
fill(128, 64, 0);
rect(10, 200, 230, 50);
// 皿を片づける、奥の棚
fill(94, 31, 0);
rect(260, 10, 300, 150);
// 棚の仕切り
fill(160, 80, 4);
rect(260, 60, 300, 20);
// 片づける前の大皿を描画(縦に並べる)
for (let i = 0; i < largeOnTable.length; i++) {
image(largeOnTable[i].images[0], 20, 200 - i * 10);
}
// 片づけた後の大皿を描画(横に並べる)
for (let i = 0; i < largeOnBoard.length; i++) {
image(largeOnBoard[i].images[1], 270 + i * 10, 100, 25, 52);
}
// 片づける前の小皿を描画(縦に並べる)
for (let i = 0; i < smallOnTable.length; i++) {
image(smallOnTable[i].images[0], 150, 220 - i * 10);
}
// 片づけた後の小皿を描画(横に並べる)
for (let i = 0; i < smallOnBoard.length; i++) {
image(smallOnBoard[i].images[1], 280 + i * 6, 40, 12, 30);
}
// 飾りのロボット
image(robotImage, 320, 140);
}
// 片づける前の皿を入れた配列と、片づけた皿を入れる配列を受け取る
function cleanUpPlate(beforeArray, afterArray) {
// 受け取った片づける前のPlateオブジェクトのタイプを調べる
// 'large'か'small'
const type = beforeArray[0].type;
// 皿の数
const platesNum = beforeArray.length;
// 1回で持てる皿の数
let plateNumOnce;
if (type === 'large') {
plateNumOnce = 2; // 大皿は2枚持てる
}
else if (type === 'small') {
plateNumOnce = 3; // 小皿は3枚持てる
}
const remain = platesNum % plateNumOnce; // 余り
const divied = int(platesNum / plateNumOnce); // 商
let count = 0; // 回数を数えるカウンタ
const ms = 1000; // msミリ秒
const timerID1 = window.setInterval(() => {
count++;
print(type + 'の片付け: ' + count + '回め');
// 片づける前の先頭からplateNumOnce分、要素を削除 => plateNumOnce分だけ片づけたので、Plateオブジェクトが減る
let deletedArray = beforeArray.splice(0, plateNumOnce);
// 削除した要素を片づけた後の配列に移す
for (let i = 0; i < deletedArray.length; i++) {
afterArray.push(deletedArray[i]);
}
print(beforeArray);
print(afterArray);
if (count >= divied) {
if (remain !== 0) {
const timerID2 = window.setTimeout(() => {
// 片づける前の先頭からremain分、要素を削除 => remain分だけ片づけたので、片づける前の配列は空になる
let deletedArray = beforeArray.splice(0, remain);
// // 削除した要素を片づけた後の配列に移す
for (let i = 0; i < deletedArray.length; i++) {
afterArray.push(deletedArray[i]);
}
print(type + 'の半端の' + remain + '枚を片づけた');
print(beforeArray)
print(afterArray);
window.clearTimeout(timerID2);
print(type + '終わり');
}, ms);
// 予定回数に達している
}
else {
print(type + '終わり');
}
window.clearInterval(timerID1);
}
}, ms);
}
function setButton(label, pos) {
const button = createButton(label);
button.size(120, 40);
button.position(pos.x, pos.y);
return button;
}
// Plateクラス
class Plate {
constructor(type) {
this.type = type; // 'large'か'small'
if (this.type === 'large') {
this.images = largImages;
}
else if (this.type === 'small') {
this.images = smallImages;
}
}
}
下図をクリックすると、プログラムが開きます。