22
33public class PiecingItTogether {
44 private static final double DOUBLE_EQUALITY_TOLERANCE = 1e-9 ;
5+ private static final int MAX_DIMENSION = 1000 ;
56
67 public static JigsawInfo getCompleteInformation (JigsawInfo input ) {
7- Integer rows = input .getRows ().isPresent () ? input .getRows ().getAsInt () : null ;
8- Integer cols = input .getColumns ().isPresent () ? input .getColumns ().getAsInt () : null ;
9- Integer pieces = input .getPieces ().isPresent () ? input .getPieces ().getAsInt () : null ;
10- Integer border = input .getBorder ().isPresent () ? input .getBorder ().getAsInt () : null ;
11- Integer inside = input .getInside ().isPresent () ? input .getInside ().getAsInt () : null ;
12- Double aspect = input .getAspectRatio ().isPresent () ? input .getAspectRatio ().getAsDouble () : null ;
13- String format = input .getFormat ().orElse (null );
14-
15- // Final information to be compared with the original input to detect inconsistencies
16- JigsawInfo result = null ;
17-
18- // Check format and aspect ratio don't contradict each other or set aspect if format is square
19- if (format != null && aspect != null ) {
20- if (!format .equals (getFormatFromAspect (aspect ))) {
21- throw new IllegalArgumentException ("Contradictory data" );
22- }
23- } else if ("square" .equalsIgnoreCase (format )) {
24- aspect = 1.0 ;
25- }
26-
27- // Derive any missing value among pieces, border, and inside
28- if (pieces != null && border != null && inside != null ) {
29- if (pieces != border + inside ) {
30- throw new IllegalArgumentException ("Contradictory data" );
31- }
32- } else if (pieces != null && border != null ) {
33- inside = pieces - border ;
34- } else if (border != null && inside != null ) {
35- pieces = border + inside ;
36- } else if (inside != null && pieces != null ) {
37- border = pieces - inside ;
38- }
39-
40- // Try to compute missing information using the numbers of rows and cols
41- if (rows != null && cols != null ) {
42- result = fromRowsAndCols (rows , cols );
43- } else if (rows != null ) {
44- Optional <Integer > possibleCols = calculateOtherSide (rows , true , pieces , border , inside , aspect );
45-
46- if (possibleCols .isPresent ()) {
47- cols = possibleCols .get ();
48- result = fromRowsAndCols (rows , cols );
49- }
50- } else if (cols != null ) {
51- Optional <Integer > possibleRows = calculateOtherSide (cols , false , pieces , border , inside , aspect );
52-
53- if (possibleRows .isPresent ()) {
54- rows = possibleRows .get ();
55- result = fromRowsAndCols (rows , cols );
56- }
57- }
58-
59- if (result != null ) {
60- checkConsistencyOrThrow (result , input );
61- return result ;
62- }
63-
64- // Try to compute missing information using the value of aspect ratio
65- if (aspect != null ) {
66- if (pieces != null ) {
67- double actualRows = Math .sqrt (pieces / aspect );
68- rows = roundIfClose (actualRows );
69-
70- double actualCols = aspect * rows ;
71- cols = roundIfClose (actualCols );
72-
73- result = fromRowsAndCols (rows , cols );
74- checkConsistencyOrThrow (result , input );
75- return result ;
76- } else if (inside != null && inside == 0 ) {
77- List <JigsawInfo > validGuesses = new ArrayList <>();
78-
79- for (int fixed : List .of (1 , 2 )) {
80- tryGuessWithFixedSide (fixed , true , aspect , input ).ifPresent (validGuesses ::add ); // rows = fixed
81- tryGuessWithFixedSide (fixed , false , aspect , input ).ifPresent (validGuesses ::add ); // cols = fixed
82- }
83-
84- if (validGuesses .size () == 1 ) {
85- return validGuesses .getFirst ();
86- } else if (validGuesses .size () > 1 ) {
87- throw new IllegalArgumentException ("Insufficient data" );
88- } else {
89- throw new IllegalArgumentException ("Contradictory data" );
90- }
91- }
92- }
93-
94- if (pieces == null && border == null && (inside == null || inside == 0 )) {
95- throw new IllegalArgumentException ("Insufficient data" );
96- }
97-
98- // Brute force as a last resort
998 List <JigsawInfo > validGuesses = new ArrayList <>();
1009
101- // Brute-force using pieces
102- if (pieces != null ) {
103- for (int r = 1 ; r <= pieces ; r ++) {
104- if (pieces % r != 0 ) {
105- continue ; // cols must be integer
10+ if (input .getPieces ().isPresent ()) {
11+ // If pieces is known, we only test divisors of pieces
12+ int pieces = input .getPieces ().getAsInt ();
13+ for (int rows = 1 ; rows <= pieces ; rows ++) {
14+ if (pieces % rows != 0 ) {
15+ continue ;
10616 }
107-
108- int c = pieces / r ;
109- tryGuess (r , c , input ).ifPresent (validGuesses ::add );
17+ int columns = pieces / rows ;
18+ createValidJigsaw (rows , columns , input ).ifPresent (validGuesses ::add );
11019 }
111- } else if (inside != null ) {
112- for (int r = 3 ; r <= inside + 2 ; r ++) {
113- int innerRows = r - 2 ;
20+ } else if (input .getInside ().isPresent () && input .getInside ().getAsInt () > 0 ) {
21+ // If inside pieces is non-zero, we only test divisors of inside
22+ int inside = input .getInside ().getAsInt ();
23+ for (int innerRows = 1 ; innerRows <= inside ; innerRows ++) {
11424 if (inside % innerRows != 0 ) {
11525 continue ;
11626 }
117-
118- int innerCols = inside / innerRows ;
119- int c = innerCols + 2 ;
120-
121- tryGuess (r , c , input ).ifPresent (validGuesses ::add );
27+ int innerColumns = inside / innerRows ;
28+ createValidJigsaw (innerRows + 2 , innerColumns + 2 , input ).ifPresent (validGuesses ::add );
12229 }
12330 } else {
124- int max = Math .min (border , 1000 );
125-
126- for (int r = 1 ; r <= max ; r ++) {
127- for (int c = 1 ; c <= max ; c ++) {
128- tryGuess (r , c , input ).ifPresent (validGuesses ::add );
31+ // Brute force using border constraint if available
32+ int maxDimension = input .getBorder ().isPresent ()
33+ ? Math .min (input .getBorder ().getAsInt (), MAX_DIMENSION )
34+ : MAX_DIMENSION ;
35+
36+ for (int rows = 1 ; rows <= maxDimension ; rows ++) {
37+ for (int columns = 1 ; columns <= maxDimension ; columns ++) {
38+ createValidJigsaw (rows , columns , input ).ifPresent (validGuesses ::add );
12939 }
13040 }
13141 }
@@ -140,170 +50,65 @@ public static JigsawInfo getCompleteInformation(JigsawInfo input) {
14050 }
14151
14252 private static String getFormatFromAspect (double aspectRatio ) {
143- String format ;
144-
14553 if (Math .abs (aspectRatio - 1.0 ) < DOUBLE_EQUALITY_TOLERANCE ) {
146- format = "square" ;
54+ return "square" ;
14755 } else if (aspectRatio < 1.0 ) {
148- format = "portrait" ;
56+ return "portrait" ;
14957 } else {
150- format = "landscape" ;
151- }
152-
153- return format ;
154- }
155-
156- private static Integer roundIfClose (double value ) {
157- double rounded = Math .round (value );
158-
159- if (Math .abs (value - rounded ) < DOUBLE_EQUALITY_TOLERANCE ) {
160- return (int ) rounded ;
58+ return "landscape" ;
16159 }
162-
163- throw new IllegalArgumentException ("Contradictory data" );
16460 }
16561
166- private static int calculateOtherSideFromPieces (int knownSide , int pieces ) {
167- if (pieces <= 0 || pieces % knownSide != 0 ) {
168- throw new IllegalArgumentException ("Contradictory data" );
169- }
170-
171- return pieces / knownSide ;
172- }
173-
174- private static int calculateOtherSideFromBorder (int knownSide , int border ) {
175- if (knownSide == 1 ) {
176- return border ;
177- }
178-
179- if (knownSide == border ) {
180- return 1 ;
181- }
182-
183- if ((border + 4 <= 2 * knownSide ) || border % 2 != 0 ) {
184- throw new IllegalArgumentException ("Contradictory data" );
185- }
186-
187- return ((border + 4 ) - 2 * knownSide ) / 2 ;
188- }
189-
190- private static int calculateOtherSideFromInside (int knownSide , int inside ) {
191- if (knownSide <= 2 || inside % (knownSide - 2 ) != 0 ) {
192- throw new IllegalArgumentException ("Contradictory data" );
193- }
194-
195- return (inside / (knownSide - 2 )) + 2 ;
196- }
197-
198- private static Optional <Integer > calculateOtherSide (int knownSide , boolean isRowKnown , Integer pieces , Integer border , Integer inside , Double aspect ) {
199- if (pieces != null ) {
200- return Optional .of (calculateOtherSideFromPieces (knownSide , pieces ));
201- } else if (border != null ) {
202- return Optional .of (calculateOtherSideFromBorder (knownSide , border ));
203- } else if (aspect != null ) {
204- double raw = isRowKnown ? knownSide * aspect : knownSide / aspect ;
205- return Optional .of (roundIfClose (raw ));
206- } else if (inside != null ) {
207- if (inside == 0 ) {
208- // When inside is zero and all other values (otherSide, pieces, border, aspect) are unknown,
209- // it only tells us that either rows or cols is 1 or 2, but provides no way to infer the rest.
210- // Since we can't make any further assumptions, we stop here.
211- throw new IllegalArgumentException ("Insufficient data" );
212- }
213-
214- return Optional .of (calculateOtherSideFromInside (knownSide , inside ));
215- }
216-
217- return Optional .empty ();
218- }
219-
220- private static JigsawInfo fromRowsAndCols (int rows , int cols ) {
221- int pieces = rows * cols ;
222- int border = (rows == 1 || cols == 1 ) ? pieces : 2 * (rows + cols - 2 );
62+ private static JigsawInfo fromRowsAndCols (int rows , int columns ) {
63+ int pieces = rows * columns ;
64+ int border = (rows == 1 || columns == 1 ) ? pieces : 2 * (rows + columns - 2 );
22365 int inside = pieces - border ;
224- double aspect = (double ) cols / rows ;
225- String format = getFormatFromAspect (aspect );
66+ double aspectRatio = (double ) columns / rows ;
67+ String format = getFormatFromAspect (aspectRatio );
22668
22769 return new JigsawInfo .Builder ()
22870 .pieces (pieces )
22971 .border (border )
23072 .inside (inside )
23173 .rows (rows )
232- .columns (cols )
233- .aspectRatio (aspect )
74+ .columns (columns )
75+ .aspectRatio (aspectRatio )
23476 .format (format )
23577 .build ();
23678 }
23779
23880 /**
23981 * Verifies that all known values in the input match those in the computed result.
240- * If any known value in the input contradicts the corresponding computed value,
241- * an IllegalArgumentException is thrown.
82+ * Returns false if any known value conflicts, true if all values are consistent.
24283 *
24384 * @param computed the fully inferred jigsaw information
244- * @param input the original partial input with possibly known values
245- * @throws IllegalArgumentException if any known value in the input conflicts with the computed result
85+ * @param input the original partial input with possibly empty values
86+ * @return true if all values are consistent, false if any conflict exists
24687 */
247- private static void checkConsistencyOrThrow (JigsawInfo computed , JigsawInfo input ) {
248- if (!valuesMatch (computed .getPieces (), input .getPieces ()) || !valuesMatch (computed .getBorder (), input .getBorder ()) || !valuesMatch (computed .getInside (), input .getInside ()) || !valuesMatch (computed .getRows (), input .getRows ()) || !valuesMatch (computed .getColumns (), input .getColumns ()) || !valuesMatch (computed .getAspectRatio (), input .getAspectRatio ()) || !valuesMatch (computed .getFormat (), input .getFormat ())) {
249- throw new IllegalArgumentException ("Contradictory data" );
250- }
88+ private static boolean isConsistent (JigsawInfo computed , JigsawInfo input ) {
89+ return valuesMatch (computed .getPieces (), input .getPieces ()) &&
90+ valuesMatch (computed .getBorder (), input .getBorder ()) &&
91+ valuesMatch (computed .getInside (), input .getInside ()) &&
92+ valuesMatch (computed .getRows (), input .getRows ()) &&
93+ valuesMatch (computed .getColumns (), input .getColumns ()) &&
94+ valuesMatch (computed .getAspectRatio (), input .getAspectRatio ()) &&
95+ valuesMatch (computed .getFormat (), input .getFormat ());
25196 }
25297
25398 /**
254- * Attempts to compute a valid jigsaw configuration by fixing one dimension
255- * (either rows or columns) and inferring the other using the aspect ratio.
256- *
257- * @param fixed the known size of one side (either rows or columns)
258- * @param isRowFixed true if the fixed value represents rows, false if columns
259- * @param aspect the desired aspect ratio (cols / rows)
260- * @param input the original input to check for consistency
261- * @return an Optional containing a valid inferred configuration, or empty if the guess is invalid
262- */
263- private static Optional <JigsawInfo > tryGuessWithFixedSide (int fixed , boolean isRowFixed , double aspect , JigsawInfo input ) {
264- try {
265- int other = isRowFixed ? roundIfClose (fixed * aspect ) : roundIfClose (fixed / aspect );
266-
267- int rows = isRowFixed ? fixed : other ;
268- int cols = isRowFixed ? other : fixed ;
269-
270- JigsawInfo guess = fromRowsAndCols (rows , cols );
271- checkConsistencyOrThrow (guess , input );
272- return Optional .of (guess );
273- } catch (IllegalArgumentException ignored ) {
274- return Optional .empty ();
275- }
276- }
277-
278- /**
279- * Attempts to construct a jigsaw configuration using the specified number of rows and columns.
99+ * Attempts to construct a valid jigsaw configuration using the specified number of rows and columns.
280100 * Returns a valid result only if the configuration is consistent with the input.
281101 *
282- * @param rows number of rows to try
283- * @param cols number of columns to try
284- * @param input the original input to check for consistency
102+ * @param rows number of rows to try
103+ * @param columns number of columns to try
104+ * @param input the original input to check for consistency
285105 * @return an Optional containing a valid configuration, or empty if inconsistent
286106 */
287- private static Optional <JigsawInfo > tryGuess (int rows , int cols , JigsawInfo input ) {
288- try {
289- JigsawInfo guess = fromRowsAndCols (rows , cols );
290- checkConsistencyOrThrow (guess , input );
291- return Optional .of (guess );
292- } catch (IllegalArgumentException ignored ) {
293- return Optional .empty ();
294- }
107+ private static Optional <JigsawInfo > createValidJigsaw (int rows , int columns , JigsawInfo input ) {
108+ JigsawInfo candidate = fromRowsAndCols (rows , columns );
109+ return isConsistent (candidate , input ) ? Optional .of (candidate ) : Optional .empty ();
295110 }
296111
297- /**
298- * Compares two optional values for equality.
299- * Returns true if either value is absent, or if both are present and equal.
300- * Used to check consistency between inferred and input data.
301- *
302- * @param a the first optional value
303- * @param b the second optional value
304- * @param <T> the type of the contained values
305- * @return true if the values are consistent (both missing or equal if present), false otherwise
306- */
307112 private static <T > boolean valuesMatch (Optional <T > a , Optional <T > b ) {
308113 if (a .isPresent () && b .isPresent ()) {
309114 return Objects .equals (a .get (), b .get ());
0 commit comments