This is the sixtieth third part of the ILP series. For your convenience you can find other parts in the table of contents in Part 1 – Boolean algebra
Today we are going to solve Surasshupakku riddle. We have a board with letters on it and we need to cut the board in a way that each part has each letter exactly once. To make the solution unique we can cut using diagonals only, no horizontal or vertical lines allowed.
This is going to be a big one but let’s start with the code as usual:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 |
var solver = new CplexMilpSolver(4); var board = @"!A!!CA !!!B!B !B!C!! !!!!!A !!B!A! C!C!!!".Split(' '); var width = board[0].Length; var height = board.Length; var distinctLetters = String.Join("", board).Where(l => l != '!').Distinct().ToArray(); var partsCount = String.Join("", board).Where(l => l == distinctLetters[0]).Count(); Console.WriteLine("Create variables"); var fields = Enumerable.Range(0, height).Select(r => Enumerable.Range(0, width).Select(c => new System.Dynamic.ExpandoObject()).ToArray()).ToArray(); for(int row=0;row<height;++row){ for(int column=0;column<width;++column){ dynamic self = fields[row][column]; self.Top = solver.CreateAnonymous(Domain.PositiveOrZeroInteger); self.Bottom = solver.CreateAnonymous(Domain.PositiveOrZeroInteger); self.Left = solver.CreateAnonymous(Domain.PositiveOrZeroInteger); self.Right = solver.CreateAnonymous(Domain.PositiveOrZeroInteger); self.Slash = solver.CreateAnonymous(Domain.BinaryInteger); self.BackSlash = solver.CreateAnonymous(Domain.BinaryInteger); } } Console.WriteLine("Make neighbours match"); for(int row=0;row<height;++row){ for(int column=0;column<width;++column){ dynamic self = fields[row][column]; if(row > 0){ dynamic up = fields[row-1][column]; self.Top = up.Bottom; } if(row < height - 1){ dynamic down = fields[row+1][column]; self.Bottom = down.Top; } if(column > 0){ dynamic left = fields[row][column - 1]; self.Left = left.Right; } if(column < width - 1){ dynamic right = fields[row][column+1]; self.Right = right.Left; } } } Console.WriteLine("Make at most one diagonal"); for(int row=0;row<height;++row){ for(int column=0;column<width;++column){ dynamic self = fields[row][column]; solver.Set(ConstraintType.LessOrEqual, solver.Operation(OperationType.Addition, self.Slash, self.BackSlash), solver.FromConstant(1)); if(board[row][column] != '!'){ solver.Set(ConstraintType.Equal, solver.FromConstant(0), self.Slash); solver.Set(ConstraintType.Equal, solver.FromConstant(0), self.BackSlash); } } } Console.WriteLine("Make edges for parts"); for(int row=0;row<height;++row){ for(int column=0;column<width;++column){ dynamic self = fields[row][column]; solver.Set(ConstraintType.LessOrEqual, self.Top, solver.FromConstant(partsCount)); solver.Set(ConstraintType.LessOrEqual, self.Bottom, solver.FromConstant(partsCount)); solver.Set(ConstraintType.LessOrEqual, self.Left, solver.FromConstant(partsCount)); solver.Set(ConstraintType.LessOrEqual, self.Right, solver.FromConstant(partsCount)); solver.Set(ConstraintType.GreaterOrEqual, self.Top, solver.FromConstant(1)); solver.Set(ConstraintType.GreaterOrEqual, self.Bottom, solver.FromConstant(1)); solver.Set(ConstraintType.GreaterOrEqual, self.Left, solver.FromConstant(1)); solver.Set(ConstraintType.GreaterOrEqual, self.Right, solver.FromConstant(1)); } } Console.WriteLine("Propagate parts"); for(int row=0;row<height;++row){ for(int column=0;column<width;++column){ dynamic self = fields[row][column]; solver.Set(ConstraintType.Equal, solver.FromConstant(1), solver.Operation(OperationType.MaterialImplication, self.Slash, solver.Operation(OperationType.IsEqual, self.Right, self.Bottom))); solver.Set(ConstraintType.Equal, solver.FromConstant(1), solver.Operation(OperationType.MaterialImplication, self.Slash, solver.Operation(OperationType.IsEqual, self.Left, self.Top))); solver.Set(ConstraintType.Equal, solver.FromConstant(1), solver.Operation(OperationType.MaterialImplication, self.BackSlash, solver.Operation(OperationType.IsEqual, self.Right, self.Top))); solver.Set(ConstraintType.Equal, solver.FromConstant(1), solver.Operation(OperationType.MaterialImplication, self.BackSlash, solver.Operation(OperationType.IsEqual, self.Left, self.Bottom))); solver.Set(ConstraintType.Equal, solver.FromConstant(1), solver.Operation(OperationType.MaterialImplication, self.Slash, solver.Operation(OperationType.IsNotEqual, self.Top, self.Bottom))); solver.Set(ConstraintType.Equal, solver.FromConstant(1), solver.Operation(OperationType.MaterialImplication, self.BackSlash, solver.Operation(OperationType.IsNotEqual, self.Top, self.Bottom))); var isEmpty = solver.Operation(OperationType.BinaryNegation, solver.Operation(OperationType.Disjunction, self.Slash, self.BackSlash)); solver.Set(ConstraintType.Equal, solver.FromConstant(1), solver.Operation(OperationType.MaterialImplication, isEmpty, solver.Operation(OperationType.IsEqual, self.Right, self.Bottom))); solver.Set(ConstraintType.Equal, solver.FromConstant(1), solver.Operation(OperationType.MaterialImplication, isEmpty, solver.Operation(OperationType.IsEqual, self.Left, self.Bottom))); solver.Set(ConstraintType.Equal, solver.FromConstant(1), solver.Operation(OperationType.MaterialImplication, isEmpty, solver.Operation(OperationType.IsEqual, self.Top, self.Bottom))); } } Console.WriteLine(@"Block the /\ propagation"); for(int row=1;row<height;++row){ for(int column=0;column<width-1;++column){ dynamic self = fields[row][column]; dynamic right = fields[row][column+1]; dynamic up = fields[row-1][column]; solver.Set(ConstraintType.Equal, solver.FromConstant(1), solver.Operation(OperationType.MaterialImplication, solver.Operation(OperationType.Conjunction, self.Slash, right.BackSlash), solver.Operation(OperationType.IsNotEqual, up.Right, self.Right))); } } Console.WriteLine(@"Block the \/ propagation"); for(int row=0;row<height-1;++row){ for(int column=0;column<width-1;++column){ dynamic self = fields[row][column]; dynamic right = fields[row][column+1]; dynamic down = fields[row+1][column]; solver.Set(ConstraintType.Equal, solver.FromConstant(1), solver.Operation(OperationType.MaterialImplication, solver.Operation(OperationType.Conjunction, self.BackSlash, right.Slash), solver.Operation(OperationType.IsNotEqual, down.Right, self.Right))); } } Console.WriteLine(@"Block the > propagation"); for(int row=0;row<height-1;++row){ for(int column=0;column<width-1;++column){ dynamic self = fields[row][column]; dynamic down = fields[row+1][column]; dynamic right = fields[row][column+1]; solver.Set(ConstraintType.Equal, solver.FromConstant(1), solver.Operation(OperationType.MaterialImplication, solver.Operation(OperationType.Conjunction, self.BackSlash, down.Slash), solver.Operation(OperationType.IsNotEqual, right.Bottom, self.Bottom))); } } Console.WriteLine(@"Block the < propagation"); for(int row=0;row<height-1;++row){ for(int column=1;column<width;++column){ dynamic self = fields[row][column]; dynamic down = fields[row+1][column]; dynamic left = fields[row][column-1]; solver.Set(ConstraintType.Equal, solver.FromConstant(1), solver.Operation(OperationType.MaterialImplication, solver.Operation(OperationType.Conjunction, self.Slash, down.BackSlash), solver.Operation(OperationType.IsNotEqual, left.Bottom, self.Bottom))); } } Console.WriteLine(@"Block on edge propagation"); for(int row=0;row<height;++row){ for(int column=0;column<width;++column){ if(row == 0 && column < width - 1){ dynamic self = fields[row][column]; dynamic right = fields[row][column+1]; solver.Set(ConstraintType.Equal, solver.FromConstant(1), solver.Operation(OperationType.MaterialImplication, self.Slash, solver.Operation(OperationType.IsNotEqual, self.Top, right.Top))); } if(row == 0 && column > 0){ dynamic self = fields[row][column]; dynamic left = fields[row][column-1]; solver.Set(ConstraintType.Equal, solver.FromConstant(1), solver.Operation(OperationType.MaterialImplication, self.BackSlash, solver.Operation(OperationType.IsNotEqual, self.Top, left.Top))); } if(row == height-1 && column < width - 1){ dynamic self = fields[row][column]; dynamic right = fields[row][column+1]; solver.Set(ConstraintType.Equal, solver.FromConstant(1), solver.Operation(OperationType.MaterialImplication, self.BackSlash, solver.Operation(OperationType.IsNotEqual, self.Bottom, right.Bottom))); } if(row == height-1 && column > 0){ dynamic self = fields[row][column]; dynamic left = fields[row][column-1]; solver.Set(ConstraintType.Equal, solver.FromConstant(1), solver.Operation(OperationType.MaterialImplication, self.Slash, solver.Operation(OperationType.IsNotEqual, self.Bottom, left.Bottom))); } if(column == 0 && row < height - 1){ dynamic self = fields[row][column]; dynamic down = fields[row+1][column]; solver.Set(ConstraintType.Equal, solver.FromConstant(1), solver.Operation(OperationType.MaterialImplication, self.Slash, solver.Operation(OperationType.IsNotEqual, self.Left, down.Left))); } if(column == 0 && row > 0){ dynamic self = fields[row][column]; dynamic up = fields[row-1][column]; solver.Set(ConstraintType.Equal, solver.FromConstant(1), solver.Operation(OperationType.MaterialImplication, self.BackSlash, solver.Operation(OperationType.IsNotEqual, self.Left, up.Left))); } if(column == width - 1 && row < height - 1){ dynamic self = fields[row][column]; dynamic down = fields[row+1][column]; solver.Set(ConstraintType.Equal, solver.FromConstant(1), solver.Operation(OperationType.MaterialImplication, self.BackSlash, solver.Operation(OperationType.IsNotEqual, self.Right, down.Right))); } if(column == width - 1 && row > 0){ dynamic self = fields[row][column]; dynamic up = fields[row-1][column]; solver.Set(ConstraintType.Equal, solver.FromConstant(1), solver.Operation(OperationType.MaterialImplication, self.Slash, solver.Operation(OperationType.IsNotEqual, self.Right, up.Right))); } } } Console.WriteLine("Propagate from starting points"); for(int part = 1; part <= partsCount; ++ part){ var isInPart = solver.FromConstant(0); for(int row=0;row<height;++row){ for(int column=0;column<width;++column){ dynamic self = fields[row][column]; self.IsInPart = solver.CreateAnonymous(Domain.BinaryInteger); isInPart = solver.Operation(OperationType.Addition, isInPart, self.IsInPart); solver.Set(ConstraintType.Equal, solver.FromConstant(1), solver.Operation(OperationType.MaterialImplication, self.IsInPart, solver.Operation(OperationType.IsEqual, self.Top, solver.FromConstant(part)))); } } solver.Set(ConstraintType.Equal, solver.FromConstant(1), isInPart); } Console.WriteLine("Make parts having correct letters"); foreach(var letter in distinctLetters){ var variables = new List<IVariable>(); for(int row=0;row<height;++row){ for(int column=0;column<width;++column){ if(board[row][column] == letter){ dynamic self = fields[row][column]; variables.Add(self.Top); } } } solver.Set(CompositeConstraintType.AllDifferent, variables[0], variables.Skip(1).ToArray()); } Console.WriteLine("Create paths"); var crossingPoints = new Dictionary<int, List<List<System.Dynamic.ExpandoObject>>>(); for(var part = 1; part <= partsCount; ++part){ Console.WriteLine("Create crossing points"); crossingPoints[part] = Enumerable.Range(0, height + 1).Select(row => Enumerable.Range(0, width + 1).Select(f => new System.Dynamic.ExpandoObject()).ToList()).ToList(); var partId = solver.FromConstant(part); for(int row=0;row<height;++row){ for(int column=0;column<width;++column){ dynamic self = fields[row][column]; self.TopLeft = crossingPoints[part][row][column]; self.TopRight = crossingPoints[part][row][column+1]; self.BottomLeft = crossingPoints[part][row+1][column]; self.BottomRight = crossingPoints[part][row+1][column+1]; } } Console.WriteLine("Create variables"); for(int row=0;row<height;++row){ for(int column=0;column<width;++column){ dynamic self = fields[row][column]; self.TopLeft.InPath = solver.CreateAnonymous(Domain.BinaryInteger); self.TopRight.InPath = solver.CreateAnonymous(Domain.BinaryInteger); self.BottomLeft.InPath = solver.CreateAnonymous(Domain.BinaryInteger); self.BottomRight.InPath = solver.CreateAnonymous(Domain.BinaryInteger); self.TopLeft.Identifier = solver.CreateAnonymous(Domain.PositiveOrZeroInteger); self.TopRight.Identifier = solver.CreateAnonymous(Domain.PositiveOrZeroInteger); self.BottomLeft.Identifier = solver.CreateAnonymous(Domain.PositiveOrZeroInteger); self.BottomRight.Identifier = solver.CreateAnonymous(Domain.PositiveOrZeroInteger); self.TopLeft.Degree = solver.CreateAnonymous(Domain.PositiveOrZeroInteger); self.TopRight.Degree = solver.CreateAnonymous(Domain.PositiveOrZeroInteger); self.BottomLeft.Degree = solver.CreateAnonymous(Domain.PositiveOrZeroInteger); self.BottomRight.Degree = solver.CreateAnonymous(Domain.PositiveOrZeroInteger); self.TopLeft.IsStart = solver.CreateAnonymous(Domain.BinaryInteger); self.TopRight.IsStart = solver.CreateAnonymous(Domain.BinaryInteger); self.BottomLeft.IsStart = solver.CreateAnonymous(Domain.BinaryInteger); self.BottomRight.IsStart = solver.CreateAnonymous(Domain.BinaryInteger); self.TopLeft.LowerNeighboursCount = solver.FromConstant(0); self.TopRight.LowerNeighboursCount = solver.FromConstant(0); self.BottomLeft.LowerNeighboursCount = solver.FromConstant(0); self.BottomRight.LowerNeighboursCount = solver.FromConstant(0); } } Console.WriteLine("Make sure points on the same edge belong to the part"); for(int row=0;row<height;++row){ for(int column=0;column<width;++column){ dynamic self = fields[row][column]; solver.Set(ConstraintType.Equal, solver.FromConstant(1), solver.Operation(OperationType.MaterialImplication, solver.Operation(OperationType.Conjunction, self.TopLeft.InPath, self.BottomLeft.InPath), solver.Operation(OperationType.IsEqual, partId, self.Left))); solver.Set(ConstraintType.Equal, solver.FromConstant(1), solver.Operation(OperationType.MaterialImplication, solver.Operation(OperationType.Conjunction, self.BottomRight.InPath, self.BottomLeft.InPath), solver.Operation(OperationType.IsEqual, partId, self.Bottom))); solver.Set(ConstraintType.Equal, solver.FromConstant(1), solver.Operation(OperationType.MaterialImplication, solver.Operation(OperationType.Conjunction, self.TopLeft.InPath, self.TopRight.InPath), solver.Operation(OperationType.IsEqual, partId, self.Top))); solver.Set(ConstraintType.Equal, solver.FromConstant(1), solver.Operation(OperationType.MaterialImplication, solver.Operation(OperationType.Conjunction, self.TopRight.InPath, self.BottomRight.InPath), solver.Operation(OperationType.IsEqual, partId, self.Right))); } } Console.WriteLine("Calculate degree"); for(int row=0;row<=height;++row){ for(int column=0;column<=width;++column){ dynamic self = crossingPoints[part][row][column]; HashSet<IVariable> neighbours = new HashSet<IVariable>(); if(row > 0){ dynamic up = crossingPoints[part][row-1][column]; neighbours.Add(up.InPath); } if(column > 0){ dynamic left = crossingPoints[part][row][column-1]; neighbours.Add(left.InPath); } if(column < width){ dynamic right = crossingPoints[part][row][column+1]; neighbours.Add(right.InPath); } if(row < height){ dynamic down = crossingPoints[part][row+1][column]; neighbours.Add(down.InPath); } IVariable isInPath = (IVariable)self.InPath; solver.Set(ConstraintType.Equal, self.Degree, solver.Operation(OperationType.Addition, neighbours.Select(n => solver.Operation(OperationType.Conjunction, isInPath, n)).ToArray())); } } Console.WriteLine("Make exactly one starting point"); HashSet<IVariable> points = new HashSet<IVariable>(); for(int row=0;row<=height;++row){ for(int column=0;column<=width;++column){ dynamic self = crossingPoints[part][row][column]; points.Add(self.IsStart); } } solver.Set(ConstraintType.Equal, solver.FromConstant(1), solver.Operation(OperationType.Addition, points.ToArray())); Console.WriteLine("Make letters have at least one point on path"); for(int row=0;row<height;++row){ for(int column=0;column<width;++column){ if(board[row][column] != '!'){ dynamic self = fields[row][column]; var belongsToPart = solver.Operation(OperationType.IsEqual, self.Top, partId); var isAtLeastOneOnPath = solver.Operation(OperationType.Disjunction, self.TopLeft.InPath, self.TopRight.InPath, self.BottomLeft.InPath, self.BottomRight.InPath); solver.Set(ConstraintType.Equal, solver.FromConstant(1), solver.Operation(OperationType.MaterialImplication, belongsToPart, isAtLeastOneOnPath)); } } } Console.WriteLine("Make sure starting point is on path"); for(int row=0;row<=height;++row){ for(int column=0;column<=width;++column){ dynamic self = crossingPoints[part][row][column]; solver.Set(ConstraintType.Equal, solver.FromConstant(1), solver.Operation(OperationType.MaterialImplication, self.IsStart, self.InPath)); } } Console.WriteLine("Make sure points on path have positive degree"); for(int row=0;row<=height;++row){ for(int column=0;column<=width;++column){ dynamic self = crossingPoints[part][row][column]; solver.Set(ConstraintType.Equal, solver.FromConstant(1), solver.Operation(OperationType.MaterialImplication, self.InPath, solver.Operation(OperationType.IsGreaterOrEqual, self.Degree, solver.FromConstant(1)))); } } Console.WriteLine("Make sure neighbours differ by one"); for(int row=0;row<height;++row){ for(int column=0;column<width;++column){ dynamic self = fields[row][column]; solver.Set(ConstraintType.Equal, solver.FromConstant(1), solver.Operation(OperationType.MaterialImplication, solver.Operation(OperationType.Conjunction, self.TopLeft.InPath, self.TopRight.InPath), solver.Operation(OperationType.IsEqual, solver.FromConstant(1), solver.Operation(OperationType.AbsoluteValue, solver.Operation(OperationType.Subtraction, self.TopLeft.Identifier, self.TopRight.Identifier))))); solver.Set(ConstraintType.Equal, solver.FromConstant(1), solver.Operation(OperationType.MaterialImplication, solver.Operation(OperationType.Conjunction, self.TopLeft.InPath, self.BottomLeft.InPath), solver.Operation(OperationType.IsEqual, solver.FromConstant(1), solver.Operation(OperationType.AbsoluteValue, solver.Operation(OperationType.Subtraction, self.TopLeft.Identifier, self.BottomLeft.Identifier))))); solver.Set(ConstraintType.Equal, solver.FromConstant(1), solver.Operation(OperationType.MaterialImplication, solver.Operation(OperationType.Conjunction, self.BottomRight.InPath, self.TopRight.InPath), solver.Operation(OperationType.IsEqual, solver.FromConstant(1), solver.Operation(OperationType.AbsoluteValue, solver.Operation(OperationType.Subtraction, self.BottomRight.Identifier, self.TopRight.Identifier))))); solver.Set(ConstraintType.Equal, solver.FromConstant(1), solver.Operation(OperationType.MaterialImplication, solver.Operation(OperationType.Conjunction, self.BottomRight.InPath, self.BottomLeft.InPath), solver.Operation(OperationType.IsEqual, solver.FromConstant(1), solver.Operation(OperationType.AbsoluteValue, solver.Operation(OperationType.Subtraction, self.BottomRight.Identifier, self.BottomLeft.Identifier))))); } } Console.WriteLine("Points on path have non-empty identifier"); for(int row=0;row<=height;++row){ for(int column=0;column<=width;++column){ dynamic self = crossingPoints[part][row][column]; solver.Set(ConstraintType.Equal, solver.FromConstant(1), solver.Operation(OperationType.Equivalency, self.InPath, solver.Operation(OperationType.IsNotEqual, solver.FromConstant(0), self.Identifier))); } } Console.WriteLine("Calculate lower neighbours"); for(int row=0;row<=height;++row){ for(int column=0;column<=width;++column){ dynamic self = crossingPoints[part][row][column]; if(row > 0){ dynamic up = crossingPoints[part][row-1][column]; var connected = solver.Operation(OperationType.Conjunction, self.InPath, up.InPath); self.LowerNeighboursCount = solver.Operation(OperationType.Addition, self.LowerNeighboursCount, solver.Operation(OperationType.Conjunction, connected, solver.Operation(OperationType.IsGreaterThan, self.Identifier, up.Identifier))); } if(column > 0){ dynamic left = crossingPoints[part][row][column-1]; var connected = solver.Operation(OperationType.Conjunction, self.InPath, left.InPath); self.LowerNeighboursCount = solver.Operation(OperationType.Addition, self.LowerNeighboursCount, solver.Operation(OperationType.Conjunction, connected, solver.Operation(OperationType.IsGreaterThan, self.Identifier, left.Identifier))); } if(row < height){ dynamic down = crossingPoints[part][row+1][column]; var connected = solver.Operation(OperationType.Conjunction, self.InPath, down.InPath); self.LowerNeighboursCount = solver.Operation(OperationType.Addition, self.LowerNeighboursCount, solver.Operation(OperationType.Conjunction, connected, solver.Operation(OperationType.IsGreaterThan, self.Identifier, down.Identifier))); } if(column < width){ dynamic right = crossingPoints[part][row][column+1]; var connected = solver.Operation(OperationType.Conjunction, self.InPath, right.InPath); self.LowerNeighboursCount = solver.Operation(OperationType.Addition, self.LowerNeighboursCount, solver.Operation(OperationType.Conjunction, connected, solver.Operation(OperationType.IsGreaterThan, self.Identifier, right.Identifier))); } } } Console.WriteLine("Starting point has no lower neighours"); for(int row=0;row<=height;++row){ for(int column=0;column<=width;++column){ dynamic self = crossingPoints[part][row][column]; solver.Set(ConstraintType.Equal, solver.FromConstant(1), solver.Operation(OperationType.MaterialImplication, self.IsStart, solver.Operation(OperationType.IsEqual, solver.FromConstant(0), self.LowerNeighboursCount))); } } Console.WriteLine("Inner point has one lower neighbour"); for(int row=0;row<=height;++row){ for(int column=0;column<=width;++column){ dynamic self = crossingPoints[part][row][column]; solver.Set(ConstraintType.Equal, solver.FromConstant(1), solver.Operation(OperationType.MaterialImplication, solver.Operation(OperationType.Conjunction, self.InPath, solver.Operation(OperationType.BinaryNegation, self.IsStart)), solver.Operation(OperationType.IsEqual, solver.FromConstant(1), self.LowerNeighboursCount))); } } } solver.AddGoal("Goal", solver.FromConstant(0)); solver.Solve(); for(var row = 0; row < height;++row){ for(var column = 0; column < width; ++ column){ dynamic self = fields[row][column]; if(board[row][column] != '!'){ Console.Write(board[row][column]); }else{ if(solver.GetValue(self.Slash) > 0.5)Console.Write("/"); else if(solver.GetValue(self.BackSlash) > 0.5)Console.Write("\\"); else Console.Write(" "); } } Console.WriteLine(); } |
Okay, let’s go one by one.
We start with defining variables for each field. Idea is we have 4 edges with numbers where a number identifies to which part the field belongs. We also have two binary flags indicating whether the field has diagonal crossed. This is in lines 16-28.
Next, we want to make sure edges are shared between fields. This is in lines 30-48. We just rewrite references, we could use equality on an ILP level.
Next, we make sure that a field is either empty or has one diagonal. Lines 53-64.
Next, we make sure edges have a positive number for parts. Lines 66-87.
Finally, we start the edge propagation. Idea is: if a field has diagonal crossed in it then this diagonal makes edge numbers different. This guarantees us that the cut is “not in the middle” but is from the very edge of the board. Why? Imagine that a cut ends somewhere in the middle and we require that edges on one side of the field have different value than edges on the other side of the field (because field is cut in half). But because that doesn’t cut whole piece, values will be propagated “around the corner” of the diagonal and we’ll get the contradiction so the only way to satisfy the requirements is to cut till the end of the board.
So in lines 89-114 we make sure that values are propagated correctly. For instance, if we have a field with / diagonal then bottom and right edges must have the same number, same for top and left, bot top and bottom must be different.
Next, in liens 116-162 we block some propagation “in the corner”. Similarly, in lines 164-223 we do not allow the propagation on the edges.
Now we know all edges will have some reasonable numbers. We can form the parts (pieces of the board).
So for each part we dedicate one special field which will be a sort of “starting point”. This is in lines 226-242. Next, in lines 244-257 we make sure that letters belong to parts correctly (so one letter in each part etc).
And this is almost all but it doesn’t guarantee that parts are compact. To guarantee that we’ll build a path in each part connecting all the nodes.
Idea is: we’ll build a path from some starting point in a part, going through all letters in that part. We start with some variables for crossing points in lines 265-275.
Next, we define variables for each crossing point in lines 277-307. InPath indicates whether a given crossing point is used in given path. Identifier is used to calculate the distance from starting point (indicated by IsStart). Degree is a helper field representing node degree being a number of edges going out of that node. Finally, LowerNeighboursCount shows how many nodes with lower identifier we are connected to.
In lines 309-322 we make sure that if two neighbouring crossing points are connected (so there is an edge in the path) then the edge must belong to the part as well. This means that we cannot connect two points from different parts.
Next, we calculate the node degrees in lines 324-349.
In lines 351-359 we make sure there is one starting point for each path. Staring point will be used as a source of the BFS algorithm.
Next, we make sure that letters of given part are included in the path (because our path is supposed to connect all letters). This is in lines 361-372.
In 374-380 we make sure that the starting point is on the path as well. Next, in lines 382-389 we make sure that nodes on the path have positive degrees (so there are no isolated points).
Finally, we start calculating the BFS distance. In lines 391-404 we make sure that neighbouring points differ in distance by one. Lines 406-413 make sure that all points on the path have some identifier (BFS distance). Finally, for each point we calculate how many neighbours have lower identifier in lines 415-444.
We use this count of lower neighbours to construct well-formed path. Starting point must have the lowest identifier on the path so in lines 446-453 we make sure that it has no lower nieghbours. On the other hand, each inner point must have exactly one lower neighbour, as set in lines 465-473.
That’s it. We then add a dummy goal and solve the problem. Output:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 |
Tried aggregator 2 times. MIP Presolve eliminated 12912 rows and 6301 columns. MIP Presolve modified 27458 coefficients. Aggregator did 13656 substitutions. Reduced MIP has 28623 rows, 14895 columns, and 84068 nonzeros. Reduced MIP has 14075 binaries, 820 generals, 0 SOSs, and 0 indicators. Presolve time = 0.22 sec. (143.74 ticks) Probing fixed 1348 vars, tightened 576 bounds. Probing changed sense of 605 constraints. Probing time = 0.51 sec. (144.89 ticks) Tried aggregator 2 times. MIP Presolve eliminated 6341 rows and 3940 columns. MIP Presolve modified 3661 coefficients. Aggregator did 1152 substitutions. Reduced MIP has 21130 rows, 9803 columns, and 62094 nonzeros. Reduced MIP has 8983 binaries, 820 generals, 0 SOSs, and 0 indicators. Presolve time = 0.11 sec. (83.35 ticks) Probing time = 0.05 sec. (6.22 ticks) Tried aggregator 1 time. MIP Presolve eliminated 0 rows and 22 columns. Reduced MIP has 21130 rows, 9781 columns, and 62050 nonzeros. Reduced MIP has 8947 binaries, 834 generals, 0 SOSs, and 0 indicators. Presolve time = 0.05 sec. (29.15 ticks) Probing fixed 59 vars, tightened 0 bounds. Probing changed sense of 12 constraints. Probing time = 0.22 sec. (57.09 ticks) Cover probing fixed 0 vars, tightened 48 bounds. Clique table members: 40802. MIP emphasis: balance optimality and feasibility. MIP search method: dynamic search. Parallel mode: deterministic, using up to 4 threads. Root relaxation solution time = 0.33 sec. (301.89 ticks) Nodes Cuts/ Node Left Objective IInf Best Integer Best Bound ItCnt Gap 0 0 0.0000 1400 0.0000 2734 0 0 0.0000 1329 Cuts: 751 3734 0 0 0.0000 1884 Cuts: 1194 5687 0 0 0.0000 2651 Cuts: 1550 7538 Repeating presolve. Tried aggregator 2 times. MIP Presolve eliminated 6215 rows and 2412 columns. MIP Presolve modified 685 coefficients. Aggregator did 18 substitutions. Reduced MIP has 14897 rows, 7351 columns, and 44153 nonzeros. Reduced MIP has 6765 binaries, 586 generals, 0 SOSs, and 0 indicators. Presolve time = 0.19 sec. (97.31 ticks) Probing time = 0.11 sec. (21.18 ticks) Tried aggregator 1 time. MIP Presolve eliminated 44 rows and 22 columns. Reduced MIP has 14853 rows, 7329 columns, and 44043 nonzeros. Reduced MIP has 6743 binaries, 586 generals, 0 SOSs, and 0 indicators. Presolve time = 0.06 sec. (34.19 ticks) Represolve time = 0.39 sec. (184.86 ticks) Probing fixed 8 vars, tightened 0 bounds. Probing time = 0.11 sec. (22.94 ticks) Cover probing fixed 0 vars, tightened 4 bounds. Clique table members: 29384. MIP emphasis: balance optimality and feasibility. MIP search method: dynamic search. Parallel mode: deterministic, using up to 4 threads. Root relaxation solution time = 0.39 sec. (193.42 ticks) Nodes Cuts/ Node Left Objective IInf Best Integer Best Bound ItCnt Gap 0 0 0.0000 1367 0.0000 10035 0 0 0.0000 2029 Cuts: 1794 11894 0 0 0.0000 1380 Cuts: 305 12794 0 0 0.0000 1906 Cuts: 947 14258 0 2 0.0000 1449 0.0000 14261 Elapsed time = 20.89 sec. (7845.10 ticks, tree = 0.01 MB, solutions = 0) 5 7 0.0000 1471 0.0000 16183 8 10 0.0000 1445 0.0000 19555 15 17 0.0000 1287 0.0000 24466 65 63 0.0000 1224 0.0000 30811 136 117 0.0000 1207 0.0000 36312 233 165 0.0000 1083 0.0000 40771 355 223 0.0000 1069 0.0000 44875 488 270 0.0000 952 0.0000 48556 603 315 0.0000 888 0.0000 53151 944 374 0.0000 944 0.0000 70778 Elapsed time = 28.98 sec. (11222.57 ticks, tree = 3.65 MB, solutions = 0) 1299 362 0.0000 845 0.0000 88984 1393 374 0.0000 773 0.0000 104039 1516 355 0.0000 623 0.0000 118633 1916 375 infeasible 0.0000 133536 2329 361 0.0000 689 0.0000 151154 2716 360 0.0000 811 0.0000 168102 2951 379 0.0000 1115 0.0000 188822 3216 453 0.0000 1346 0.0000 211396 3430 455 0.0000 761 0.0000 234654 4000 524 infeasible 0.0000 247219 Elapsed time = 53.22 sec. (21068.49 ticks, tree = 3.28 MB, solutions = 0) 4443 524 infeasible 0.0000 261648 4703 469 0.0000 857 0.0000 287118 5123 459 0.0000 895 0.0000 306110 5379 476 0.0000 853 0.0000 328370 5749 476 0.0000 741 0.0000 343838 6180 465 infeasible 0.0000 364627 6352 491 0.0000 866 0.0000 378672 6889 475 0.0000 799 0.0000 391983 7376 452 infeasible 0.0000 412075 7502 494 0.0000 1153 0.0000 431563 Elapsed time = 78.33 sec. (30826.18 ticks, tree = 1.75 MB, solutions = 0) 8156 536 0.0000 697 0.0000 443386 8751 566 infeasible 0.0000 461144 8999 546 0.0000 830 0.0000 469998 9695 608 0.0000 397 0.0000 478486 10476 598 0.0000 508 0.0000 486498 11030 537 infeasible 0.0000 498109 11235 594 0.0000 316 0.0000 508384 11905 597 infeasible 0.0000 521023 12618 577 0.0000 613 0.0000 530976 13196 546 0.0000 626 0.0000 539864 Elapsed time = 101.13 sec. (40601.02 ticks, tree = 1.13 MB, solutions = 0) 13631 587 0.0000 596 0.0000 551798 14369 592 0.0000 549 0.0000 562735 14858 585 0.0000 596 0.0000 572026 15327 583 0.0000 574 0.0000 583994 15802 583 0.0000 684 0.0000 594983 16241 550 infeasible 0.0000 611642 16656 593 0.0000 741 0.0000 627591 17275 585 infeasible 0.0000 639497 17383 555 0.0000 807 0.0000 652196 17942 577 0.0000 761 0.0000 665879 Elapsed time = 125.86 sec. (50201.40 ticks, tree = 1.88 MB, solutions = 0) 18513 583 0.0000 652 0.0000 679499 18995 600 0.0000 618 0.0000 692736 19424 652 infeasible 0.0000 704578 19729 633 0.0000 772 0.0000 716971 20071 656 0.0000 792 0.0000 730943 20479 742 0.0000 714 0.0000 747066 21097 747 infeasible 0.0000 760966 21203 713 0.0000 823 0.0000 772470 21578 749 0.0000 775 0.0000 786911 22085 788 0.0000 772 0.0000 800352 Elapsed time = 148.23 sec. (59891.44 ticks, tree = 2.31 MB, solutions = 0) 22553 782 infeasible 0.0000 814515 22862 779 0.0000 727 0.0000 829534 23300 793 0.0000 724 0.0000 842134 23703 788 0.0000 949 0.0000 858281 24088 799 0.0000 957 0.0000 875302 24367 813 0.0000 824 0.0000 889497 24760 839 infeasible 0.0000 906723 25126 818 0.0000 683 0.0000 919951 25267 815 0.0000 1126 0.0000 933780 25697 843 0.0000 578 0.0000 950043 Elapsed time = 171.72 sec. (69562.34 ticks, tree = 2.64 MB, solutions = 0) 26119 882 0.0000 938 0.0000 965560 26536 915 0.0000 627 0.0000 977390 27014 904 infeasible 0.0000 992348 27109 865 infeasible 0.0000 1005449 27401 913 0.0000 996 0.0000 1020692 27932 924 0.0000 1013 0.0000 1037806 28269 892 0.0000 706 0.0000 1051739 28621 939 0.0000 752 0.0000 1068363 29030 944 infeasible 0.0000 1084849 29276 953 0.0000 1054 0.0000 1101867 Elapsed time = 195.63 sec. (79229.26 ticks, tree = 4.17 MB, solutions = 0) 29687 967 0.0000 841 0.0000 1116646 29993 909 0.0000 827 0.0000 1130922 30394 976 0.0000 583 0.0000 1146379 31040 962 infeasible 0.0000 1157142 31223 955 0.0000 744 0.0000 1166044 31724 976 0.0000 603 0.0000 1177967 32305 972 0.0000 916 0.0000 1192680 32754 980 infeasible 0.0000 1206746 33223 951 0.0000 900 0.0000 1220570 33608 952 0.0000 730 0.0000 1233469 Elapsed time = 219.00 sec. (89007.21 ticks, tree = 4.58 MB, solutions = 0) 34062 955 0.0000 599 0.0000 1245691 34376 946 0.0000 960 0.0000 1261678 34725 979 0.0000 777 0.0000 1278134 34988 993 0.0000 816 0.0000 1293522 35358 1010 0.0000 667 0.0000 1302671 35834 986 0.0000 785 0.0000 1314709 36153 951 infeasible 0.0000 1324529 36514 966 0.0000 642 0.0000 1338254 36944 966 0.0000 852 0.0000 1354076 38304 934 0.0000 861 0.0000 1414190 Elapsed time = 250.09 sec. (101570.57 ticks, tree = 4.66 MB, solutions = 0) 39562 963 infeasible 0.0000 1471635 40021 1002 0.0000 2203 -0.0000 1515709 40030 1005 0.0000 2094 -0.0000 1522639 40480 389 infeasible -0.0000 1550223 40739 200 infeasible -0.0000 1559728 40993 126 infeasible -0.0000 1593299 41105 140 0.0000 1795 -0.0000 1619217 41346 195 0.0000 757 -0.0000 1641174 41664 195 infeasible -0.0000 1667486 41976 173 0.0000 890 -0.0000 1690788 Elapsed time = 410.53 sec. (158672.67 ticks, tree = 4.19 MB, solutions = 0) 42347 180 infeasible -0.0000 1712096 42383 184 0.0000 966 0.0000 1716356 42523 234 0.0000 1452 0.0000 1778062 42618 261 0.0000 1263 0.0000 1838032 42787 318 0.0000 1103 0.0000 1911520 * 42934 375 integral 0 0.0000 0.0000 1925167 0.00% Root node processing (before b&c): Real time = 20.70 sec. (7777.57 ticks) Parallel b&c, 4 threads: Real time = 461.17 sec. (193450.52 ticks) Sync time (average) = 32.72 sec. Wait time (average) = 0.15 sec. ------------ Total (root+branch&cut) = 481.88 sec. (201228.09 ticks) \A/\CA / \B\B B/C \ /\/\A \B A\ C/C /\ |
You can see it took almost 10 minutes to find the solution. You can see that the solution has two “dummy” cuts in top left corner which do not change the solution (although, we might want to get rid of them).
That was pretty intense but shows that you can construct pretty sophisticated things by combining various constraints.