目盛りのない水差しが二つある。 A の水差しには A リットル、B の水差しには B リットルまで入る。 最初は二つとも空で、一回に次の六つの操作のうち一つができる。 操作にはそれぞれ番号がある。 - 1:A を水道でいっぱいにする。 2:B をいっぱいにする。 - 3:A の水をすべて捨てる。 4:B の水をすべて捨てる。 - 5:A の水を B に注ぐ。 A が空になるか B がいっぱいになるまで注ぎ、水はあふれも消えもしない。 - 6:B の水を A に同じやり方で注ぐ。 二つのうちどちらか一方にぴったり t リットル入るようにする、いちばん短い操作の手順を求めよ。 いちばん短い手順がいくつもあれば、操作番号を前から比べて、初めて違う位置で番号が小さい手順を選ぶ。 答えは操作の回数 k を先に書き、続けて操作番号 k 個を順に書く。 最初から一方に t リットルあれば(t = 0)0 だけを出力する。 どうしてもできなければ -1 を出力する。
入力
最初の行に問いの数 q(1 ≤ q ≤ 20)。 続く q 行に A、B、t(1 ≤ A, B ≤ 90、0 ≤ t ≤ 90)
出力
問いごとに一行に "k 操作1 … 操作k"(空白一つで区切る)、または -1
例 1 · stdin
4
3 5 4
2 4 3
7 7 0
1 4 2
stdout
6 2 6 3 6 2 6
-1
0
4 1 5 1 5
main.py
エラー一覧00F8 ↓ · ⇧F8 ↑
入力(stdin)
実行すると、これらの行が順に input()・標準入力に渡されます(実行中に入力を求めません)。
$ 準備完了
コードを入力して解くにはパソコンで開いてください。モバイルでは問題と解答を見られます。
ほかの言語では?言語別の解答を見る · 8 言語の解答
二つの水差しの量 (a, b) を一つのマスとみなすと、操作一回はマスからマスへの一歩です。 いちばん短い手順は幅優先探索(BFS)で求めます。 キューの前から取り出して新しいマスを後ろに入れると、0 歩のマス、1 歩のマス、2 歩のマスの順に出てくるので、目標のマスを初めて取り出したときがいちばん短い手順です。 マスごとに「初めて入ってきた前のマス」と「そのときの操作」を書いておき、目標のマスから前のマスをたどって (0, 0) までさかのぼると手順が出ます。 操作を 1 番から順に試し、初めて着いたときだけ書けば、同じ歩数のマスが番号の小さい手順の順にキューに入るので、番号が小さい手順が自然に選ばれます。 マスは多くて 91 × 91 個なので、(a, b) を一つの数 a * (B + 1) + b に変えて一つのリストに書けば足ります。 キューは言語ごとに違います。 Python collections.deque の popleft()、Java ArrayDeque の add・poll、C# Queue<T> の Enqueue・Dequeue、C++ std::queue の push・front・pop です。 C にはキューがないので配列と先頭・末尾の番号で作り、JS・TS は shift() が前を抜くので遅くなりうるため、配列に先頭の番号を置いて動かします。 Craykon も 追加 で入れて先頭の番号を動かし、新リスト に前のマスと操作を書きます。 「見つからない」は Python None、JS・TS・Java・C# null、C++ std::optional、Craykon なし で返します。 落とし穴は六つ:① 後ろから取り出すと(Python pop())スタックになって深さ優先探索になり、最初に見つけた道がいちばん短い道ではありません。 Java ArrayDeque の push・pop はどちらも前側を使うので、同じくスタックになります。 ② 注ぐ量は min(a, B − b) です。 受ける側があふれるとき残りの水を捨てる(注ぐ側を 0 にする)と、作れる量が変わります。 ③ 目標を新しく入れるマスでだけ確かめると、最初のマス (0, 0) を見落として t = 0 に 0 ではなく操作一つを答えます。 ④ 目標は「どちらか一方」なので、B だけが t の場合も答えです。 ⑤ 前のマスをたどって集めた操作は、終わりから始めの向きに逆順で出てきます。 反転してから出力します(Python [::-1]、JS・TS reverse()、Java Collections.reverse、C# List.Reverse()、C++ std::reverse、Craykon 逆に())。 ⑥ 操作を番号順に試さないと(たとえば B 側を先に)、A 側と B 側に同じ長さの道があるとき(1 4 2 のように)違う手順を出します。
Pythonmain.py · 36 行
from collections import dequedef shortest_plan(A, B, t): """빈 두 물통에서 한쪽에 딱 t리터를 담는 가장 짧은 동작 번호 목록(가장 앞선 것). 안 되면 None.""" w = B + 1 # 상태 (a, b)를 수 하나 a * w + b로 적는다 prev = [-1] * ((A + 1) * w) # 그 상태로 처음 들어온 앞 상태(-1이면 아직 못 가 봄) move = [0] * ((A + 1) * w) # 그때 한 동작 번호 prev[0] = 0 queue = deque([0]) while queue: s = queue.popleft() a, b = divmod(s, w) if a == t or b == t: # 꺼낸 차례가 곧 가깝고 앞선 차례다 plan = [] while s != 0: # 앞 상태를 따라가면 끝에서 처음 쪽으로 거꾸로 나온다 plan.append(move[s]) s = prev[s] return plan[::-1] ab = min(a, B - b) # A에서 B로 부을 수 있는 양 ba = min(b, A - a) # B에서 A로 부을 수 있는 양 nexts = [(A, b), (a, B), (0, b), (a, 0), (a - ab, b + ab), (a + ba, b - ba)] for k, (na, nb) in enumerate(nexts, start=1): # 동작 번호가 작은 것부터 n = na * w + nb if prev[n] < 0: # 처음 닿을 때만 적는다(넣을 때 표시) prev[n] = s move[n] = k queue.append(n) return Noneq = int(input())for _ in range(q): A, B, t = map(int, input().split()) plan = shortest_plan(A, B, t) print(-1 if plan is None else " ".join(map(str, [len(plan)] + plan)))
JavaScriptmain.js · 41 行
const lines = require("fs").readFileSync(0, "utf8").trim().split("\n");// 빈 두 물통에서 한쪽에 딱 t리터를 담는 가장 짧은 동작 번호 목록(가장 앞선 것). 안 되면 nullfunction shortestPlan(A, B, t) { const w = B + 1; // 상태 (a, b)를 수 하나 a * w + b로 적는다 const prev = new Array((A + 1) * w).fill(-1); // 그 상태로 처음 들어온 앞 상태(-1이면 아직 못 가 봄) const move = new Array((A + 1) * w).fill(0); // 그때 한 동작 번호 prev[0] = 0; const queue = [0]; for (let head = 0; head < queue.length; head++) { // shift() 대신 머리 번호를 옮긴다 let s = queue[head]; const a = Math.floor(s / w); const b = s % w; if (a === t || b === t) { // 꺼낸 차례가 곧 가깝고 앞선 차례다 const plan = []; for (; s !== 0; s = prev[s]) plan.push(move[s]); // 끝에서 처음 쪽으로 거꾸로 나온다 return plan.reverse(); } const ab = Math.min(a, B - b); // A에서 B로 부을 수 있는 양 const ba = Math.min(b, A - a); // B에서 A로 부을 수 있는 양 const nexts = [[A, b], [a, B], [0, b], [a, 0], [a - ab, b + ab], [a + ba, b - ba]]; nexts.forEach(([na, nb], k) => { // 동작 번호(k + 1)가 작은 것부터 const n = na * w + nb; if (prev[n] < 0) { // 처음 닿을 때만 적는다(넣을 때 표시) prev[n] = s; move[n] = k + 1; queue.push(n); } }); } return null;}const q = Number(lines[0]);const out = [];for (let i = 1; i <= q; i++) { const [A, B, t] = lines[i].trim().split(" ").map(Number); const plan = shortestPlan(A, B, t); out.push(plan === null ? "-1" : [plan.length, ...plan].join(" "));}console.log(out.join("\n"));
TypeScriptmain.ts · 42 行
import { readFileSync } from "node:fs";const lines: string[] = readFileSync(0, "utf8").trim().split("\n");// 빈 두 물통에서 한쪽에 딱 t리터를 담는 가장 짧은 동작 번호 목록(가장 앞선 것). 안 되면 nullfunction shortestPlan(A: number, B: number, t: number): number[] | null { const w = B + 1; // 상태 (a, b)를 수 하나 a * w + b로 적는다 const prev: number[] = new Array((A + 1) * w).fill(-1); // 그 상태로 처음 들어온 앞 상태(-1이면 아직 못 가 봄) const move: number[] = new Array((A + 1) * w).fill(0); // 그때 한 동작 번호 prev[0] = 0; const queue: number[] = [0]; for (let head = 0; head < queue.length; head++) { // shift() 대신 머리 번호를 옮긴다 let s = queue[head]; const a = Math.floor(s / w); const b = s % w; if (a === t || b === t) { // 꺼낸 차례가 곧 가깝고 앞선 차례다 const plan: number[] = []; for (; s !== 0; s = prev[s]) plan.push(move[s]); // 끝에서 처음 쪽으로 거꾸로 나온다 return plan.reverse(); } const ab = Math.min(a, B - b); // A에서 B로 부을 수 있는 양 const ba = Math.min(b, A - a); // B에서 A로 부을 수 있는 양 const nexts: [number, number][] = [[A, b], [a, B], [0, b], [a, 0], [a - ab, b + ab], [a + ba, b - ba]]; nexts.forEach(([na, nb], k) => { // 동작 번호(k + 1)가 작은 것부터 const n = na * w + nb; if (prev[n] < 0) { // 처음 닿을 때만 적는다(넣을 때 표시) prev[n] = s; move[n] = k + 1; queue.push(n); } }); } return null;}const q = Number(lines[0]);const out: string[] = [];for (let i = 1; i <= q; i++) { const [A, B, t] = lines[i].trim().split(" ").map(Number); const plan = shortestPlan(A, B, t); out.push(plan === null ? "-1" : [plan.length, ...plan].join(" "));}console.log(out.join("\n"));
JavaMain.java · 66 行
import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.util.ArrayDeque;import java.util.ArrayList;import java.util.Arrays;import java.util.Collections;import java.util.List;import java.util.StringTokenizer;public class Main { // 빈 두 물통에서 한쪽에 딱 t리터를 담는 가장 짧은 동작 번호 목록(가장 앞선 것). 안 되면 null // 상태 (a, b)는 수 하나 a * (B + 1) + b로 적는다 static List<Integer> shortestPlan(int A, int B, int t) { int w = B + 1; int[] prev = new int[(A + 1) * w]; // 그 상태로 처음 들어온 앞 상태(-1이면 아직 못 가 봄) int[] move = new int[(A + 1) * w]; // 그때 한 동작 번호 Arrays.fill(prev, -1); prev[0] = 0; ArrayDeque<Integer> queue = new ArrayDeque<>(); queue.add(0); while (!queue.isEmpty()) { int s = queue.poll(); // 앞에서 꺼낸다(add는 뒤에 넣는다) int a = s / w, b = s % w; if (a == t || b == t) { // 꺼낸 차례가 곧 가깝고 앞선 차례다 List<Integer> plan = new ArrayList<>(); for (; s != 0; s = prev[s]) plan.add(move[s]); // 끝에서 처음 쪽으로 거꾸로 Collections.reverse(plan); return plan; } int ab = Math.min(a, B - b); // A에서 B로 부을 수 있는 양 int ba = Math.min(b, A - a); // B에서 A로 부을 수 있는 양 int[][] nexts = {{A, b}, {a, B}, {0, b}, {a, 0}, {a - ab, b + ab}, {a + ba, b - ba}}; for (int k = 0; k < 6; k++) { // 동작 번호(k + 1)가 작은 것부터 int n = nexts[k][0] * w + nexts[k][1]; if (prev[n] < 0) { // 처음 닿을 때만 적는다(넣을 때 표시) prev[n] = s; move[n] = k + 1; queue.add(n); } } } return null; } public static void main(String[] args) throws IOException { BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); int q = Integer.parseInt(in.readLine().trim()); StringBuilder out = new StringBuilder(); for (int i = 0; i < q; i++) { StringTokenizer st = new StringTokenizer(in.readLine()); int A = Integer.parseInt(st.nextToken()); int B = Integer.parseInt(st.nextToken()); int t = Integer.parseInt(st.nextToken()); List<Integer> plan = shortestPlan(A, B, t); if (plan == null) { out.append(-1).append('\n'); continue; } out.append(plan.size()); for (int k : plan) out.append(' ').append(k); out.append('\n'); } System.out.print(out); }}
C#Main.cs · 58 行
using System;using System.Collections.Generic;using System.Linq;using System.Text;class Program{ // 빈 두 물통에서 한쪽에 딱 t리터를 담는 가장 짧은 동작 번호 목록(가장 앞선 것). 안 되면 null // 상태 (a, b)는 수 하나 a * (B + 1) + b로 적는다 static List<int>? ShortestPlan(int A, int B, int t) { int w = B + 1; var prev = Enumerable.Repeat(-1, (A + 1) * w).ToArray(); // 그 상태로 처음 들어온 앞 상태(-1이면 아직 못 가 봄) var move = new int[(A + 1) * w]; // 그때 한 동작 번호 prev[0] = 0; var queue = new Queue<int>(); queue.Enqueue(0); while (queue.Count > 0) { int s = queue.Dequeue(); int a = s / w, b = s % w; if (a == t || b == t) // 꺼낸 차례가 곧 가깝고 앞선 차례다 { var plan = new List<int>(); for (; s != 0; s = prev[s]) plan.Add(move[s]); // 끝에서 처음 쪽으로 거꾸로 plan.Reverse(); return plan; } int ab = Math.Min(a, B - b); // A에서 B로 부을 수 있는 양 int ba = Math.Min(b, A - a); // B에서 A로 부을 수 있는 양 var nexts = new[] { (A, b), (a, B), (0, b), (a, 0), (a - ab, b + ab), (a + ba, b - ba) }; for (int k = 0; k < 6; k++) // 동작 번호(k + 1)가 작은 것부터 { int n = nexts[k].Item1 * w + nexts[k].Item2; if (prev[n] < 0) // 처음 닿을 때만 적는다(넣을 때 표시) { prev[n] = s; move[n] = k + 1; queue.Enqueue(n); } } } return null; } static void Main() { int q = int.Parse(Console.ReadLine()!.Trim()); var output = new StringBuilder(); for (int i = 0; i < q; i++) { int[] p = Console.ReadLine()!.Trim().Split(' ').Select(int.Parse).ToArray(); var plan = ShortestPlan(p[0], p[1], p[2]); output.Append(plan == null ? "-1" : string.Join(" ", plan.Prepend(plan.Count))).Append('\n'); } Console.Write(output); }}
C++main.cpp · 56 行
#include <algorithm>#include <iostream>#include <optional>#include <queue>#include <vector>// 빈 두 물통에서 한쪽에 딱 t리터를 담는 가장 짧은 동작 번호 목록(가장 앞선 것). 안 되면 빈 optional// 상태 (a, b)는 수 하나 a * (B + 1) + b로 적는다std::optional<std::vector<int>> shortestPlan(int A, int B, int t) { int w = B + 1; std::vector<int> prev((A + 1) * w, -1); // 그 상태로 처음 들어온 앞 상태(-1이면 아직 못 가 봄) std::vector<int> move((A + 1) * w, 0); // 그때 한 동작 번호 std::queue<int> queue; prev[0] = 0; queue.push(0); while (!queue.empty()) { int s = queue.front(); // front()로 읽고 pop()으로 뺀다 queue.pop(); int a = s / w, b = s % w; if (a == t || b == t) { // 꺼낸 차례가 곧 가깝고 앞선 차례다 std::vector<int> plan; for (; s != 0; s = prev[s]) plan.push_back(move[s]); // 끝에서 처음 쪽으로 거꾸로 std::reverse(plan.begin(), plan.end()); return plan; } int ab = std::min(a, B - b); // A에서 B로 부을 수 있는 양 int ba = std::min(b, A - a); // B에서 A로 부을 수 있는 양 int nexts[6][2] = {{A, b}, {a, B}, {0, b}, {a, 0}, {a - ab, b + ab}, {a + ba, b - ba}}; for (int k = 0; k < 6; k++) { // 동작 번호(k + 1)가 작은 것부터 int n = nexts[k][0] * w + nexts[k][1]; if (prev[n] < 0) { // 처음 닿을 때만 적는다(넣을 때 표시) prev[n] = s; move[n] = k + 1; queue.push(n); } } } return std::nullopt;}int main() { int q; std::cin >> q; while (q--) { int A, B, t; std::cin >> A >> B >> t; auto plan = shortestPlan(A, B, t); if (!plan) { std::cout << -1 << '\n'; continue; } std::cout << plan->size(); for (int k : *plan) std::cout << ' ' << k; std::cout << '\n'; }}
Cmain.c · 61 行
#include <stdio.h>#define MAXS (91 * 91)static int prev[MAXS]; /* 그 상태로 처음 들어온 앞 상태(-1이면 아직 못 가 봄) */static int move[MAXS]; /* 그때 한 동작 번호 */static int queue[MAXS]; /* 상태마다 한 번만 들어가니 이 크기면 된다 */static int plan[MAXS];static int min(int x, int y) { return x < y ? x : y; }/* 빈 두 물통에서 한쪽에 딱 t리터를 담는 가장 짧은 동작 번호(가장 앞선 것)를 plan에 채우고 개수를 돌려준다. 안 되면 -1 상태 (a, b)는 수 하나 a * (B + 1) + b로 적는다 */static int shortest_plan(int A, int B, int t) { int w = B + 1; for (int s = 0; s < (A + 1) * w; s++) prev[s] = -1; int head = 0, tail = 0; prev[0] = 0; queue[tail++] = 0; while (head < tail) { int s = queue[head++]; int a = s / w, b = s % w; if (a == t || b == t) { /* 꺼낸 차례가 곧 가깝고 앞선 차례다 */ int len = 0; for (; s != 0; s = prev[s]) plan[len++] = move[s]; /* 끝에서 처음 쪽으로 거꾸로 */ for (int i = 0, j = len - 1; i < j; i++, j--) { int tmp = plan[i]; plan[i] = plan[j]; plan[j] = tmp; } return len; } int ab = min(a, B - b); /* A에서 B로 부을 수 있는 양 */ int ba = min(b, A - a); /* B에서 A로 부을 수 있는 양 */ int na[6] = {A, a, 0, a, a - ab, a + ba}; int nb[6] = {b, B, b, 0, b + ab, b - ba}; for (int k = 0; k < 6; k++) { /* 동작 번호(k + 1)가 작은 것부터 */ int n = na[k] * w + nb[k]; if (prev[n] < 0) { /* 처음 닿을 때만 적는다(넣을 때 표시) */ prev[n] = s; move[n] = k + 1; queue[tail++] = n; } } } return -1;}int main(void) { int q; scanf("%d", &q); while (q--) { int A, B, t; scanf("%d %d %d", &A, &B, &t); int len = shortest_plan(A, B, t); printf("%d", len); for (int i = 0; i < len; i++) printf(" %d", plan[i]); printf("\n"); } return 0;}
Craykonmain.cray · 59 行
// 빈 두 물통에서 한쪽에 딱 t리터를 담는 가장 짧은 동작 번호 목록(가장 앞선 것). 안 되면 빈값// 상태 (a, b)는 수 하나 a * (B + 1) + b로 적는다가장짧은계획(A, B, t){ w = B + 1; prev = 新リスト((A + 1) * w, -1); // 그 상태로 처음 들어온 앞 상태(-1이면 아직 못 가 봄) move = 新リスト((A + 1) * w, 0); // 그때 한 동작 번호 prev[0] = 0; queue = [0]; head = 0; // 목록 앞에서 빼지 않고 머리 번호를 옮긴다 間 (head < queue.個数) { s = queue[head]; head += 1; a = 数学.商(s, w); b = s % w; もし (a == t || b == t) { plan = []; 間 (s != 0) { plan.追加(move[s]); s = prev[s]; } plan.逆に(); 返す plan; } ab = 数学.小さい方(a, B - b); // A에서 B로 부을 수 있는 양 ba = 数学.小さい方(b, A - a); // B에서 A로 부을 수 있는 양 nexts = [A * w + b, a * w + B, b, a * w, (a - ab) * w + b + ab, (a + ba) * w + b - ba]; 繰り返す (k = 0; k < 6; k++) { n = nexts[k]; もし (prev[n] < 0) { prev[n] = s; move[n] = k + 1; queue.追加(n); } } } 返す なし;}q = コンソール.行読み().空白除去().数に();繰り返す (i = 0; i < q; i++){ p = コンソール.数読み(); plan = 가장짧은계획(p[0], p[1], p[2]); もし (plan == なし) { コンソール.書く(-1); } でなければ { plan.挿入(0, plan.個数); コンソール.書く(plan.つなぐ(" ")); }}