[Updated]Billboard problem Resolved – Facebook Hacker Cup 2012

[Update@2012-01-24] I made a little mistake in a if...else... statement.

Facebook started Facebook Hacker Cup 2012 from January 20 at 4:00pm PST, and until January 23 at 4:00pm PST, the event is closed to receive submission.

There are three questions, when I learned the event, the event is already started and left me only near 16 hours to take some challenge.

I picked “Billboard problem”.

The original question

Billboards

We are starting preparations for Hacker Cup 2013 really early. Our first step is to prepare billboards to advertise the contest. We have text for hundreds of billboards, but we need your help to design them.

The billboards are of different sizes, but are all rectangular. The billboard widths and heights are all integers. We will supply you with the size in inches and the text we want printed. We want you to tell us how large we can print the text, such that it fits on the billboard without splitting any words across lines. Since this is to attract hackers like yourself, we will use a monospace font, meaning that all characters are of the same width (e.g.. ‘l’ and ‘m’ take up the same horizontal space, as do space characters). The characters in our font are of equal width and height, and there will be no additional spacing between adjacent characters or adjacent rows. If you print a word on one line and print the next word on the next line, you do not need to print a space between them.

Let’s say we want to print the text “Facebook Hacker Cup 2013″ on a 350×100″ billboard. If we use a font size of 33″ per character, then we can print “Facebook” on the first line, “Hacker Cup” on the second and “2013″ on the third. The widest of the three lines is “Hacker Cup”, which is 330″ wide. There are three lines, so the total height is 99″. We cannot go any larger.

Input

The first line of the input file contains a single integer T: the number of test cases. T lines follow, each representing a single test case in the form “W H S”. W and H are the width and height in inches of the available space. S is the text to be written.

Output

Output T lines, one for each test case. For each case, output “Case #t: s”, where t is the test case number (starting from 1) and s is the maximum font size, in inches per character, we can use. The size must be an integral number of inches. If the text does not fit when printed at a size of 1″, then output 0.

Constraints

  • 1 ≤ T ≤ 20
  • 1 ≤ W, H ≤ 1000

The text will contain only lower-case letters a-z, upper-case letters A-Z, digits 0-9 and the space character. The text will not start or end with the space character, and will never contain two adjacent space characters. The text in each case contains at most 1000 characters

Example input

5
20 6 hacker cup
100 20 hacker cup 2013
10 20 MUST BE ABLE TO HACK
55 25 Can you hack
100 20 Hack your way to the cup

Example output

Case #1: 3
Case #2: 10
Case #3: 2
Case #4: 8
Case #5: 7

My Solution

I used Objective-C to resolve this problem. I created a Mac app project. Later I think it’s completely unnecessary. I will try to use maybe pure C or Cocoa Console later.

Here is the my solution on github

The key idea behind my solution is divide and conquer.

1. Find the possible max font size

  1. Assume the billboard only show one line or one column, the MIN(width, height) will give the first guess of the max font size.
  2. Assume all the characters without space will be layout on the billboard one by one, the biggest font to whole all characters will be our possible max font size. Because if we add any space or non-hyphenation constraints to layout, we must shrink the font to hold these extra constraints.

Here I used the D&C to find the possible max font size:

- (NSInteger)maxFontSizeForText:(NSString *)text 
                withTextInArray:(NSArray *)textInArray 
               constraintToArea:(NSInteger)rectArea 
                     withBounds:(NSDictionary *)bounds
{
  NSParameterAssert(text);
  NSParameterAssert(textInArray);
  NSParameterAssert(bounds);

  NSInteger upperSize = [[bounds objectForKey:UPPER_BOUND_KEY] integerValue];
  NSInteger lowerSize = [[bounds objectForKey:LOWER_BOUND_KEY] integerValue];
//  NSLog(@"*** finding max size between %4ld...%ld", lowerSize, upperSize);

  NSInteger upperArea = upperSize * upperSize * ([text length] - ([textInArray count] - 1));
  NSInteger lowerArea = lowerSize * ([text length] - ([textInArray count] - 1));

  // No need to go further, there will be one size suitable for rectArea
  if (1 >= (upperSize - lowerSize)) { // there could be a case of 0
    if (upperArea <= rectArea) {
      return upperSize;
    }else if (lowerArea <= rectArea){
      return lowerSize;
    }else {
      return 0;
    }
  }

  // Otherwise, we need to go recursive routine to find the max size in lower...upper
  NSInteger middleSize = lowerSize + ((upperSize - lowerSize) >> 1);
  NSInteger middleArea = middleSize * middleSize * ([text length] - ([textInArray count] - 1));
  if (middleArea > rectArea) {
    NSDictionary *boundsDict = [NSDictionary dictionaryWithObjectsAndKeys:
                                [bounds objectForKey:LOWER_BOUND_KEY], LOWER_BOUND_KEY,
                                [NSNumber numberWithInteger:middleSize], UPPER_BOUND_KEY,
                                nil];
    return [self maxFontSizeForText:text withTextInArray:textInArray constraintToArea:rectArea withBounds:boundsDict];
  }else {
    NSDictionary *boundsDict = [NSDictionary dictionaryWithObjectsAndKeys:
                                [NSNumber numberWithInteger:middleSize], LOWER_BOUND_KEY,
                                [bounds objectForKey:UPPER_BOUND_KEY], UPPER_BOUND_KEY,
                                nil];
    return [self maxFontSizeForText:text withTextInArray:textInArray constraintToArea:rectArea withBounds:boundsDict];
  }
}

2. Find the correct max font size

Try to fit the text start with possible max font, then shrink the font size to meet the constraints.

This is simple but have some complex if...else... statements.

[Update 2012-01-24] Thanks to Chong, the following algorithm failed to find the correct font size for Case #7, it should be 9 instead of 8, to fix this, I need to modify 2 places, change realFontSize = width/[aWord length]; to realFontSize--; and change realFontSize = height/(currentLineIndex + 1); to realFontSize--;

// Another core algorithm that will try and shrink the font size to fit the billboard
- (NSInteger)correctMaxFontSizeForNoHyphenationFor:(NSArray *)textInArray 
                                      constraintTo:(NSInteger)possibleMaxFontSize 
                                        boardWidth:(NSInteger)width 
                                       boardHeight:(NSInteger)height
{
  NSParameterAssert(textInArray);

  if (possibleMaxFontSize == 0) {
    return 0;
  }

  NSInteger realFontSize = possibleMaxFontSize;
  BOOL finished = NO;
  do {
    NSInteger currentLineWidth = 0;
    NSInteger currentLineIndex = 0;

    for (int idx= 0; idx < [textInArray count]; idx++) {
      NSString *aWord = [textInArray objectAtIndex:idx];
      NSInteger wordLengthAppliedFontSize = [aWord length]*realFontSize;
      // width is not enough for a single word
      if (wordLengthAppliedFontSize > width) {
        realFontSize = width/[aWord length];
        currentLineIndex = 0;
        currentLineWidth = 0;
        break;
      }

      // current word is safe for at least one line, see if it is safe to add to current line.
      if (currentLineWidth + wordLengthAppliedFontSize > width) {
        // need a new line
        currentLineIndex++;
        currentLineWidth = wordLengthAppliedFontSize;

      }else{
        currentLineWidth += wordLengthAppliedFontSize;
      }

      if (currentLineWidth + realFontSize < width) {
        // current line is safe for one more space.
        currentLineWidth += realFontSize;
      //}else{   //<- Here cause the case #1 failed
      }else if (idx != ([textInArray count] -1)){
        // need a new line
        currentLineIndex++;
        currentLineWidth = 0;
      }

      // height is not enough for the whole text
      if ((currentLineIndex + 1) * realFontSize > height){
        realFontSize = height/(currentLineIndex + 1);
        currentLineIndex = 0;
        currentLineWidth = 0;
        break;
      }
      // done.
      if (idx == [textInArray count] -1) {
        // last line
        finished = YES;
      }

    }

  } while (realFontSize > 0 && !finished);

  return realFontSize;
}

3. Verify it

I created some test cases to test on it. It includes some extra test cases that need to be omitted when read in the file.

Download my test cases

Meanwhile, Facebook has an input file, you can also download it.

Output

Here is my output, what’s yours? Whose one is correct?

Case #1: 30 //<- Wrong, should be 60
Case #2: 5
Case #3: 31
Case #4: 6
Case #5: 30
Case #6: 83
Case #7: 8      //<- Wrong, should be 9
Case #8: 80
Case #9: 21
Case #10: 4
Case #11: 31
Case #12: 21
Case #13: 30
Case #14: 21
Case #15: 30
Case #16: 21
Case #17: 4
Case #18: 21
Case #19: 4
Case #20: 4

After modified step2, I think the following result is correct:

Case #1: 60 
Case #2: 5 
Case #3: 31 
Case #4: 6 
Case #5: 30 
Case #6: 83 
Case #7: 9 
Case #8: 80 
Case #9: 21 
Case #10: 4 
Case #11: 31 
Case #12: 21 
Case #13: 30 
Case #14: 21 
Case #15: 30 
Case #16: 21 
Case #17: 4 
Case #18: 21 
Case #19: 4 
Case #20: 4 

It’s fun, I’m no longer qualified for next round now, good luck, my friends. (hope I can enter the qualification round. And so do you.)

Be Sociable, Share!
    • Abhishek

      I think your answer for 1st test case is wrong. I got 60 for that.

      Reason: You can put the entire text in one line.
      Given width=900, height=60
      No of characters: 15 (including spaces)
      Font size: 60
      Width: 15*60=900
      Height: 60

      Hence it’s the maximum possible. Please let me know if I’m wrong.

      • tonny.xu

        hi, Abhishek,

        Ohh. Holy shit… Yeah, I was wrong.

        I made a mistake on the shrinking font block. I thought I can get a ticket to the Qualification Round. Now it’s gone….

        Thank you Abhishek.

    • Chong

      For case #7, I got 9 instead of 8
      The cutting can be
      LGi CEV Ock
      IE JdfuDJ
      PNN CPu J
      jHiF j Bfq
      zqeK K
      Totally 5 lines and 11 in width. To fit in a billboard of 100 in width, and 50 in height. The maximum font size can be 9.

      • tonny.xu

        Hi, Chong,

        You are right, my algorithm will ignore 9 got 8 directly, so I think my algorithm for finding correct max font size is completely failed.

        Thank you.

    • Chong

      Don’t worry. There is Google Code Jam coming in May or something. You still have chance to show your skills. By the way, the Facebook one gave wrong impression that our submission was correct, but it actually meant the submission was completed. Otherwise, we could have tried problem 3 which is much easier.