11

I have a few large 2D arrays like:

   1   2  3  4  5
   --------------
1 | 0  1  1  1  0
2 | 0  1  1  1  0
3 | 0  1  0  1  1
4 | 0  1  0  1  1

So, the largest rectangular block (by area) satisfying ==1 starts at (1,2) and its dimensions are (2,3).

How to find it with Mathematica without iterating explicitly?


NB:

Just to ease your testing here is one of my samples:

matrix = ImageData@Binarize@Import@"https://i.stack.imgur.com/ux7tA.png"
Dr. belisarius
  • 60,527
  • 15
  • 115
  • 190

5 Answers5

9

This is my attempt using BitAnd

maxBlock[mat_] := Block[{table, maxSeq, pos},

  maxSeq[list_] := 
   Max[Length[#] & /@ Append[Cases[Split[list], {1 ..}], {}]];

  table = 
   Flatten[Table[
     MapIndexed[{#2[[1]], maxSeq[#1]} &, 
      FoldList[BitAnd[#1, #2] &, mat[[k]], Drop[mat, k]]], {k, 1, 
      Length[mat]}], 1];

  pos = Ordering[(Times @@@ table), -1][[1]];

  {Times[##], {##}} & @@ table[[pos]]]

Result for belisarius' picture:

Timing[maxBlock[Unitize[matrix, 1.]]]

(* {1.13253, {23433, {219, 107}}} *)

On the plus side this code seems faster than David's and Sjoerd's code, but for some reason it returns a rectangle which is one smaller in both dimensions than their result. Since the difference is exactly one I suspect a counting error somewhere but I can't find it at the moment.

Heike
  • 24,102
  • 2
  • 31
  • 45
  • +1, very nice. in any case the tiny bug is easy to squash: add 1 to the results :) – acl Aug 23 '11 at 22:07
  • WoW ... That is fast! Very nice! – Dr. belisarius Aug 23 '11 at 22:29
  • strange, I get 220, 108 when I try your code, which seems to be the correct answer. – Sjoerd C. de Vries Aug 24 '11 at 06:31
  • I see where I went wrong. I tried my code on `matrix = ImageData@Import@"http://i.stack.imgur.com/ux7tA.png"` (without `Binarize`, which is why I used `Unitize`). With `Binarize` I get 220, 108 as well. – Heike Aug 24 '11 at 07:53
  • 1
    BTW very nice code. It took a bit of mind jogging before I could grasp it and I was satisfied it would indeed find the largest rectangle in special cases with several holes. I had been thinking on this 'sweeping action' you do (using Times in my case) but couldn't find a nice functional way to do it. The `MapIndexed` / `FoldList` combo is a nice one. I still wonder whether it can't be improved, because a lot of BitAnding is done again and again. – Sjoerd C. de Vries Aug 24 '11 at 13:31
5

Well, just to prove it's possible using functional programming here's my terribly, terribly inefficient brute force approach:

First, I generate a list of all possible squares, sorted in order of descending area:

rectangles = Flatten[
               Table[{i j, i, j}, 
                     {i, Length[matrix]}, 
                     {j, Length[matrix[[1]]]}
               ],1 
             ] // Sort // Reverse;

For a given rectangle I do a ListCorrelate. If a free rectangle of this size can be found in the matrix there should be at least one number in the result that corresponds to the area of that rectangle (assuming the matrix contains only 1's and 0's). We check that using Max. As long as we don't find a match we look for smaller rectangles (LengthWhile takes care of that). We end up with the largest rectangle number that fits in the matrix:

LengthWhile[
   rectangles, 
   Max[ListCorrelate[ConstantArray[1, {#[[2]], #[[3]]}], matrix]] != #[[1]] &
]

On my laptop, using belisarius' image, it took 156 seconds to find that the 11774+1th rectangle (+1 because the LengthWhile returns the number of the last rectangle that doesn't fit) is the largest one that will fit

In[70]:= rectangles[[11774 + 1]]

Out[70]= {23760, 220, 108}
Sjoerd C. de Vries
  • 16,122
  • 3
  • 42
  • 94
  • that does work. it would be interesting to know if there is any simple algorithm with much better complexity than this. – acl Aug 23 '11 at 15:58
  • @Sjoerd +1 Well done! I will accept if nobody comes up with something more efficient. – Dr. belisarius Aug 23 '11 at 16:21
  • @acl The wikipedia lemma cited above mentions some algorithms, but I bet they use iteration... I actually first thought of using one of V8's new image processing functions, but couldn't find a useful one (in this context) on short notice so I defaulted on this one. – Sjoerd C. de Vries Aug 23 '11 at 16:43
  • You can set an upper bound on the area of the rectangles to try as: `uBound = Total@Flatten@matrix;rect = DeleteCases[rectangles, x_ /; x[[1]] > uBound];` – abcd Aug 24 '11 at 02:16
  • @yoda Yep, makes it go down to 59 s. I suppose there are more quick wins to be found this way, for instance, looking for the longest vertical and horizontal line. Area can't be higher than the product of their lengths. – Sjoerd C. de Vries Aug 24 '11 at 08:02
4

A viable option is to ignore the dictum to avoid iteration.

First a routine to find the largest length given a fixed width. Use it on the transposed matrix to reverse those dimensions. It works by divide and conquer, so is reasonably fast.

maxLength[mat_, width_, min_, max_] := Module[
  {len = Floor[(min + max)/2], top = max, bottom = min, conv},
  While[bottom <= len <= top,
   conv = ListConvolve[ConstantArray[1, {len, width}], mat];
   If[Length[Position[conv, len*width]] >= 1,
    bottom = len;
    len = Ceiling[(len + top)/2],
    top = len;
    len = Floor[(len + bottom)/2]];
   If[len == bottom || len == top, Return[bottom]]
   ];
  bottom
  ]

Here is the slower sweep code. We find the maximal dimensions and for one of them we sweep downward, maximizing the other dimension, until we know we cannot improve on the maximal area. The only efficiency I came up with was to increase the lower bounds based on prior lower bounds, so as to make the maxLength calls slightly faster.

maxRectangle[mat_] := Module[
  {min, dims = Dimensions[mat], tmat = Transpose[mat], maxl, maxw, 
   len, wid, best},
  maxl = Max[Map[Length, Cases[Map[Split, mat], {1 ..}, 2]]];
  maxw = Max[Map[Length, Cases[Map[Split, tmat], {1 ..}, 2]]];
  len = maxLength[tmat, maxw, 1, maxl];
  best = {len, maxw};
  min = maxw*len;
  wid = maxw - 1;
  While[wid*maxl >= min,
   len = maxLength[tmat, wid, len, maxl];
   If[len*wid > min, best = {len, wid}; min = len*wid];
   wid--;
   ];
  {min, best}
  ]

This is better than Sjoerd's by an order of magnitude, being only terrible and not terrible^2.

In[364]:= Timing[maxRectangle[matrix]]

Out[364]= {11.8, {23760, {108, 220}}}

Daniel Lichtblau

Daniel Lichtblau
  • 6,854
  • 1
  • 23
  • 30
1

I cannot compete with Heike's logic, but I can refactor her code a little.

maxBlock[mat_] := Module[{table, maxSeq, pos, i},
  maxSeq = Max[0, Length /@ Split@# ~Cases~ {1 ..}] &;
  table = Join @@
    Table[
       {i++, maxSeq@j},
       {k, Length@mat},
       {j, i = 1; FoldList[BitAnd, mat[[k]], mat~Drop~k]}
    ];
  pos = Ordering[Times @@@ table, -1][[1]];
  {# #2, {##}} & @@ table[[pos]]
]

I believe this is cleaner, and it runs about 20% faster.

Mr.Wizard
  • 24,179
  • 5
  • 44
  • 125
  • @belisarius thanks. I took a break from StackOverflow for a couple of months while I did other things. – Mr.Wizard Aug 24 '11 at 01:54
  • Not sure whether you should refer to Heike as 'her'. It is a male name in some parts of my country, see http://www.nobelprize.org/nobel_prizes/physics/laureates/1913/onnes-bio.html – Sjoerd C. de Vries Aug 24 '11 at 06:26
  • 1
    @Sjoerd Try as I may, try as I might... http://stackoverflow.com/questions/7127729/color-plot-by-order-of-points-in-list-mathematica/7128218#7128218 (comments) ;-) – Mr.Wizard Aug 24 '11 at 06:31
  • @Mr.Wizard I have missed something. Why do you consider the usage of `Block` in this case as an abuse of `Block`? – Alexey Popkov Sep 02 '13 at 01:26
  • @Alexey I make the edit to follow up on [this comment](http://mathematica.stackexchange.com/questions/31449/map-function-on-the-specified-location/31453#comment98165_31453), which has an example. In this particular case it shouldn't matter if `mat` is numeric but still it is not "good practice" IMHO. – Mr.Wizard Sep 02 '13 at 03:37
-2

Do you consider convolution as iterating explicitly? If not then it can be used to do what you want. With a simple kernel, say 3x3 1s, you can quickly zero out those non-contiguous 1s.

Edit:

Mathematica has built-in convolution function, you can use that, or brew your own:

Here's the pseudo code (untested of course :)

kernel = [ [1,1,1], [1,1,1], [1,1,1] ]

for row = 1, row <= image_height - 1, row++
  for col = 1, col <= image_width - 1, col++
    compare kernel with the 3x3 matrix at image(row, col):
      if there is 0 on left AND right of the center column, OR
      if there is 0 on top AND bottom of center row, THEN
         zero out whole area from image(row-1, col-1) to image(row+1, col+1)
         # The above may need refinement
  end
end

After that what's left are the contiguous square area of 1s. You can do connected area analysis and determine the biggest area from there.

holygeek
  • 15,653
  • 1
  • 40
  • 50
  • 2
    Thanks for the update, but it doesn't really meet the OP's requirement "How to find it with Mathematica without iterating explicitly?" – Sjoerd C. de Vries Aug 23 '11 at 14:39
  • My limited experience tells me that that's not possible. Sounds like doing search without searching. – holygeek Aug 23 '11 at 14:52
  • In the functional programming style you can hide a lot of implicit iterations – Sjoerd C. de Vries Aug 23 '11 at 14:56
  • 2
    @holygeek: I think that belisarius wants to use Mathematica's pattern matching (or some functional or recursive approach) to do the search without being explicit about the underlying steps/loops. – Simon Aug 23 '11 at 14:57
  • @holygeek I guess what belisarius wants is something similar in spirit to this: To obtain all sequences in a list that contain more than 1 consecutive elements, one can do `Select[Split[lst], Length@# > 1 &]`. Clearly something somewhere is iterating, but you don't explicitly specify it in your code. – acl Aug 23 '11 at 15:52