views:

599

answers:

7

I have the following problem: I am given a tree with N apples, for each apple I am given it's weight and height. I can pick apples up to a given height H, each time I pick an apple the height of every apple is increased with U. I have to find out the maximum weight of apples I can pick.

1 ≤ N ≤ 100000
0 < {H, U, apples' weight and height, maximum weight} < 231

Example:

N=4  H=100  U=10  

height weight  
  82     30
  91     10
  93      5
  94     15

The answer is 45: first pick the apple with the weight of 15 then the one with the weight of 30.

Could someone help me approach this problem?

A: 

It seems to me that you don't need a dynamic programming solution for this, but just a greedy algorithm. Sorting the apples by height, pick the heaviest apple from the apples whose height is between 100 and 100 - U. Then remove all those apples up to 100 - U and repeat for 90 - U (or increase the weight of all apples by U).

Because you can always pick the apples further down, the idea is to pick the heaviest apples which would fall out of reach first.

This should work because the increase in height U is constant. Where it not constant, then you'd have to use a recursive formula in combination with memoization, which is dynamic programming.

Virtlink
-1, not optimal.
-1. Recursion + memorization is not dynamic programming. It is a crude, poorly performing approximation of dynamic programming. Furthermore, as algorithmist says, that is not an optimal solution. You pick the heaviest apple even if it's at the bottom of the tree instead of a moderately less heavy apple is just about to become out of reach.
Billy ONeal
Before algorithmist's comment, I realised that I forgot something. I changed my answer accordingly. Dynamic programming is using a recursive formula, rewriting it iteratively and use *memoization* (without the 'r') to prevent recalculating a lot of values. This ís dynamic programming, my answer is a greedy approach.
Virtlink
Memoization isn't technically dynamic programming, but the solutions end up being almost identical. With memoization, you start from the top, and with DP you start from the bottom. Other than that, the code is the same.
Peter Alexander
That's not correct according to my textbook. Memoization still requires you to calculate the *last* value ever required in your recursion *first*, so it is also bottom-up.
Virtlink
Memorization also incurs the performance penalty of maintaining the machine stack. And the performance penalty of performing the lookups. That's why memorization is inferior to dynamic programming and why dynamic programming is used more often.
Billy ONeal
Well, yes, the things are calculated in the same order, but you write your program as a top down recursion.
Peter Alexander
@Billy, true, but memoization is far more natural when you are actually coding things -- in my opinion at least. For example, a Fibonacci function written using memoization is more recognizable as Fibonacci than the DP solution.
Peter Alexander
@Billy ONeal: Erhm... recursion uses the machine stack. Memoization does not have anything to do with the machine stack, and recursion is avoided by rewriting it iteratively, storing (memoizing) the values from last to first. So, no machine stack penalty.I don't know what definition of dynamic programming you use (you might want to explain what makes dynamic programming so much better then what I define) but our definitions differ and mine is from my college textbook Algorithm Design (Kleinberg and Tardos, 2005) so it's probably correct. Or I misunderstand you somehow, also possible...
Virtlink
@Virtlink: Dynamic programming does not use recursion. It takes a problem initially formulated using recursion, but the final dynamic programming algorithm is implemented using loops and tables. A recursion is useful for writing an algorithm that uses dynamic programming, but an algorithm that is still recursive is not dynamic programming. "Memorization" leaves the recursion as-is, but adds a lookup table to the beginning of the function which checks if it has ever been called with those exact arguments and returns the table's result if so. Therefore you pay for the machine stack *and* table.
Billy ONeal
@Virtlink: Consider the difference between http://en.wikipedia.org/wiki/Memoization and http://en.wikipedia.org/wiki/Dynamic_programming
Billy ONeal
A: 

The first thing to realize with this problem is that you should always pick apples in order of decreasing height. If you are going to pick apples A and B, with A higher than B, then there is no point picking B first because it may push A too high; on the other hand, picking A first would increase B, but not to a height greater than A+U. The end result is the same in both cases, but picking B first may eliminate the chance of picking A.

The first thing to do is to sort the apples in decreasing order (i.e. from highest to lowest).

Next, devise a recursive solution to the problem (ignoring complexity). Looking at the first apple, you have to decide "would it be better to take it, or leave it?" So the solution is essentially:

max(take first apple, don't take first apple).

You can recurse this down for the second apple, third apple etc.

This should give you function with parameters number of seen apples and number of taken apples. This gives you a function input space size of O(N2). From there, just memoize your inputs and you have an algorithm that has time complexity of O(N2) as well.

Peter Alexander
Oh, the bound was added after I started writing. Hmm, will need to think a little more.
Peter Alexander
A: 

Ok, you know your maximum height, and you know how far everything will move up when you pick something off of the tree. Basically everything between the top and top - U will be unavailable after you pick one apple off of the tree, so maybe your first pick should be from that set?

edit: As you march down the tree, at each slot take the heaviest and then check the other candidates against the ones you've already selected; if any are heavier than one you've picked, swap in.

(Yes, this would be easier bottom-up, now that that idea has been put forth.)

dash-tom-bang
not necessarily,if I have something like: 1:90 5 2:81 10 3:82 10 ,with H=100 and U=10,If I pick the first and then one out of the remaining two(dosen't matter witch one,both are 10) you get a maximum of 15 ,where by loosing the first apple and picking the second and the third you get 20
John Retallack
Not necessarily. Consider the apples {(w=1,h=100), (w=10,h=85), (w=10,h=85)}. If you pick in the 90-100 region first then you won't be able to pick both the w=10 apples, which is the solution here. (Edit: wow, we gave pretty similar counter-examples!)
Peter Alexander
ah good point- so I don't give away the answer, is there anything you can do with the apples "in hand" versus the apples that are soon to be out of reach? (a rhetorical question!)
dash-tom-bang
+1  A: 

1) Determine for each apple, n, how long its life is--i.e. the number of picks, Pn, it will remain on the tree.

2) From the apples with the largest Pn, pick out the heaviest. Decrement Pn for each remaining apple in this group.

3) Repeat step 2 until All apples have Pn = 0.

dmb
I don't get it- if a heavy apple is at the bottom and a light one at the top, you'll only pick the one at the bottom and then let the top one become inaccessible?
dash-tom-bang
@dash: No; if it helps, this answer is equivalent to mine (we answered only 58s apart).
Roger Pate
@dash Basically, at any given moment, you want to pick the heaviest apple that *won't* be available later. In order to know which ones *will* be available later, you start by picking lowest first.
dmb
@dmb What do you mean by group,"Decrement Pn for each remaining apple in this group.",apples having the same Pn ?
John Retallack
@bwarner You won't pick the light apple,because the heavier apples will have a bigger Pn,so you will pick the heaviest out of the second or third apple ,e.q:1: 91 1 2:81 10 3:81 11,with H=100,U=10 ,so yo will first pick the third apple beacuse it has the highest Pn,then the remaing two will have the same Pn but you will pick the heaviest
John Retallack
Got it now, thanks!
bwarner
@John Decrement Pn for the apples with the *greatest* Pn. It's like saying, "these apples will be around in the last round, but I won't pick them then, so I might as well treat them as living one round less." Not sure how clear I'm being--does that make sense?
dmb
@dmb yes,I got it,thanks,Isn't the complexity of this algorithm still O(n^2) in worst case,cause if I have all the Pn equal,I have to itarate through all apples to find the heaviest.
John Retallack
@John: Sort them first to avoid that.
Roger Pate
hmmm, if all Pn are equal, you can just sort them by weight and take the heaviest Pn. Actually, you don't even need them sorted, just put them on a heap. I'm not that up on complexity theory, but I think that means O(nlogn) to build the heap, then popping off the top max(Pn) is O(logPn) or O(logn), whichever is less (either way, insignificant). This is how Roger Pate's code above works--since, in fact, I think that using a heap is probably the best implementation no matter how the Pn's are distributed.
dmb
Mm- my bad, I didn't get the point that only the longest-lived group's "time of life" was decremented. That operation seems superfluous to me anyway; you'll have a candidate list of apples you can choose from, and after picking one, you inject the next higher set into the candidate list. Keeping this list sorted by weight would lead to an O(n log(n)) solution.
dash-tom-bang
+2  A: 

Work backwards. Start by deciding the last apple you will pick, then the second to last, etc.

import heapq

def solve(apples, H, U):
  """Solve apple-picking problem.

  apples must be a sequence of (H, W) pairs.
  """
  if U == 0:
    return sum(x[1] for x in apples if x[0] <= H)

  apples = sorted(apples, reversed=True)
  # also creates a copy, to not modify caller's data

  picked_weight = 0
  available_weights = []  # stored negative for heapq

  offset = U - H % U
  if offset == U: offset = 0
  top = offset - U

  while (apples or available_weights) and top <= H:
    if available_weights:
      picked_weight += -heapq.heappop(available_weights)
      top += U
    else:
      top += U * max(1, (apples[-1][0] - top) // U)

    while apples and apples[0][0] <= top:
      heapq.heappush(available_weights, -apples.pop()[1])

  return picked_weight

Simple test:

def test(expected, apples, H, U):
  actual = solve(apples, H, U)
  if expected != actual:
    print "expected=%r actual=%r | H=%r U=%r apples=%r" % (
              expected,   actual,     H,   U,   apples)

test(45, [(82, 30), (91, 10), (93,  5), (94, 15)], 100, 10)
test(22, [( 1,  1), ( 2,  1), (81, 10), (82, 10)], 100, 10)
test(20, [( 1, 10), ( 2, 10), (11,  1)], 20, 10)
test(20, [(81, 10), (82, 10), (90,  5)], 100, 10)
test(1, [(2**31 - 1, 1)], 2**31 - 1, 1)

Someone requested C++, so here it is. It's nearly identical code and logic to the above Python, except for one change: C++ stdlib's heap functions work with the max value instead of the min, so no need for the negation. (I kept this self-contained, but utilities such as a heap adapter and container inserter will make the code easier to use.)

#include <cstdio>
#include <algorithm>
#include <iostream>
#include <numeric>
#include <vector>


struct Apple {
  int h, w;

  friend bool operator<(Apple const& a, Apple const& b) {
    return a.h < b.h or (a.h == b.h and a.w < b.w);
  }
  friend bool operator>(Apple const& a, Apple const& b) {
    return b < a;
  }
  friend std::ostream& operator<<(std::ostream& s, Apple const& v) {
    s << '(' << v.h << ", " << v.w << ')';
    return s;
  }
};

template<class T, class C, T C::*M>
struct sum {
  T operator()(T const& cur, C const& next) { return cur + next.*M; }
};

int solve(std::vector<Apple> apples, int H, int U) {
  if (U == 0) {
    return std::accumulate(apples.begin(), apples.end(), 0, sum<int, Apple, &Apple::w>());
  }

  std::sort(apples.begin(), apples.end(), std::greater<Apple>());

  int picked_weight = 0;
  std::vector<int> available_weights;  // NOT stored negative, unlike Python code

  int offset = U - H % U;
  if (offset == U) offset = 0;
  int top = offset - U;

  while ((apples.size() or available_weights.size()) and top <= H) {
    if (available_weights.size()) {
      picked_weight += available_weights.front();
      std::pop_heap(available_weights.begin(), available_weights.end());
      available_weights.pop_back();
      top += U;
    }
    else {
      top += U * std::max(1, (apples.back().h - top) / U);
    }

    while (apples.size() and apples.back().h <= top) {
      available_weights.push_back(apples.back().w);
      std::push_heap(available_weights.begin(), available_weights.end());
      apples.pop_back();
    }
  }

  return picked_weight;
}

C++ tests:

template<int N>
void test(int expected, Apple (&apples)[N], int H, int U) {
  std::vector<Apple> v (apples, apples + N);
  int actual = solve(v, H, U);
  if (expected != actual) {
    std::printf("expected=%d actual=%d | H=%d U=%d apples=[",
                    expected,   actual,     H,   U);
    std::vector<Apple>::const_iterator i = v.begin();
    if (i != v.end()) {
      std::cout << *i;
      for (++i; i != v.end(); ++i) {
        std::cout << ", " << *i;
      }
    }
    std::cout << "]" << std::endl;
  }
}

int main() {
  {
    Apple data[] = {{82, 30}, {91, 10}, {93,  5}, {94, 15}};
    test(45, data, 100, 10);
  }
  {
    Apple data[] = {{ 1,  1}, { 2,  1}, {81, 10}, {82, 10}};
    test(22, data, 100, 10);
  }
  {
    Apple data[] = {{ 1, 10}, { 2, 10}, {11,  1}};
    test(20, data, 20, 10);
  }
  {
    Apple data[] = {{81, 10}, {82, 10}, {90,  5}};
    test(20, data, 100, 10);
  }
  {
    int n = 2147483647; // 2**31 - 1
    Apple data[] = {{n, 1}};
    test(1, data, n, 1);
  }
}
Roger Pate
on witch criteria do I determine witch apple I want to pick last ?
John Retallack
What if H=2^31 and U=1?
Peter Alexander
Better yet, what if U=0?
Peter Alexander
if U=0, the problem is trivial--you pick all the apples.
dmb
@John: What is the criteria you're trying to maximize? That's what you use. Are you having problems figuring out which candidates will be available for the last pick?
Roger Pate
@Peter: Then it is still linear in O(H/U). You can't beat that for this problem, as far as I can see. dmb answered U=0.
Roger Pate
I'm sure there is an O(nlog(n)) solution to this problem as well
John Retallack
@John: What is your *n*? Note that linear is faster than O(n log n), and if N < H/U, you can break early in my algorithm.
Roger Pate
I know liniar is faster in theory but considering that N <10000 and H and U is < 2^31,I think that a nlogn algorithm will perform better.
John Retallack
In order to pick my last apple I just take the heaviest apple on the lowest level ?
John Retallack
@Roger, yes, it is linear, but won't run in any reasonable amount of time. Given the bounds, he's looking for each a O(N) or O(Nlg(N)) solution.
Peter Alexander
@John: Exactly, yes.
Roger Pate
You are right it's O(n),Congrats on the solution,wouldn't thought that there is such a simple and efficient solution
John Retallack
I still can't figure out how do I know when to stop.For example:1:90 5 2:81 10 3:81 10,where U=10 and H=100,how do I know I should stop after picking apple 2 and 3?
John Retallack
start at H = 0, and bump it up by U. Once H = Top, you're done. This isn't linear, is it? If all of your apples are the same weight and on the ground, you need to inspect every unselected one of them every choice, unless you sort them but then you still end up with O(n log(n)).
dash-tom-bang
What is early break?
Timmy
@John: Maybe I just don't know how to explain it, but I'll post the code.
Roger Pate
@Timmy: Early loop termination. In the above code, this is terminating the loop if both apples and available_weights are empty, plus advancing top if available_weights is empty.
Roger Pate
@Roger thanks for the code,I don't understand what's the role of 'offset'
John Retallack
@John: Consider H=100 U=9, H=20 U=8, or any other time H % U != 0.
Roger Pate
So much for not giving away the answer to homework, eh?
dash-tom-bang
`while (apples[0][0] < top): heapq.heappush(available_weights, -apples.pop(0)[1])` is not going to run in O(n), fwiw.
dash-tom-bang
@dash: I try to treat homework and non-homework questions identically. (I've posted on [meta](http://meta.stackoverflow.com/) about it before, though I can't find it now.) Either you can be helpful and explain what's going on so they can understand it, or you can't. Knowing it's homework in advance (or that he doesn't immediately want the answer to a riddle-like question) helps, but in the end doesn't change much.
Roger Pate
@dash: I knew something was off about that, thanks.
Roger Pate
A: 

Here is a big hint:

Order the apples by decreasing height.

Consider the case where all N apples are a different heights. This is trivial, then just pick the apples one by one in decreasing height yielding the optimal solution (ask yourself why.)

The only difficulty in this problem arises if two or more apples are at the same height. In fact, the only real difficulty arises when you have a gap between consecutive apple heights. For example, suppose you have t time steps before apples start going out of reach. That means that at this exact moment you can treat all apples within t steps of the highest group with equal priority. This is because you have t "freebie" picks before the tree starts fighting back.

Here's a picture

  ------------- H
  _
  |
  [t]
  |
  _
  A A A A ... A <-closest group to H
  ._
  .|
  .[t]      all apples in this range should be treated with same priority
  .|        
  ._     
ldog
downvote with no comment?
ldog
A: 

@Roger Pate, Could you please explain a little more the code you've written.I'm not at all familiar with Python and I'm finding it hard to understand.

Dman
Done. However, this isn't a real answer, so you should probably delete it. Once you get a little bit of reputation, you'll be able to leave comments.
Roger Pate