"""
Several algorithms approaches to solving the rod cutting problem with Dynamic Programming.

See CLRS 15.1 for discussion.

Author: Michael Goldwasser

Use -h flag for documentation on usage.
"""

#-----------------------------------------------------------------------
def cut_rod(p, n):
    """Recursive top-down implementation.

    p is assumed to be an array of size n+1 such that p[k] defines price for length k
    (with p[0] = 0).
    """
    if n == 0:
        return 0
    else:
        q = float('-inf')
        # Consider a first cut of length i, for i from 1 to n inclusive
        for i in range(1, n+1):
            q = max(q, p[i] + cut_rod(p, n-i))
        return q

#-----------------------------------------------------------------------
def memoized_cut_rod(p, n, table=None):

    # create empty table, if none given
    if table is None:
        table = [None] * (n+1)

    # use known answer, if one exists
    if table[n] is not None:
        return table[n]

    # otherwise use recursive formulation
    if n == 0:
        q = 0
    else:
        q = float('-inf')
        # Consider a first cut of length i, for i from 1 to n inclusive
        for i in range(1, n+1):
            q = max(q, p[i] + memoized_cut_rod(p, n-i, table))

    # memoize answer before returning
    table[n] = q
    return q

#-----------------------------------------------------------------------
def bottom_up_cut_rod(p, n):
    r = [0] * (n+1)                # will be list of maximum revenues. r[0] = 0
    for j in range(1, n+1):
        q = float('-inf')
        for i in range(1, j+1):
            q = max(q, p[i] + r[j-i])
        r[j] = q
    return r[n]

#-----------------------------------------------------------------------
def extended_bottom_up_cut_rod(p, n):
    """Returns entire table of revenues and additional table of solutions."""
    r = [0] * (n+1)                # will be list of maximum revenues. r[0] = 0
    s = [0] * (n+1)                # s[j] is the size of the first piece when starting with j
    for j in range(1, n+1):
        q = float('-inf')
        for i in range(1, j+1):
            if p[i] + r[j-i] > q:     # found a better solution
                q = p[i] + r[j-i]
                s[j] = i
        r[j] = q
    return r, s

#-----------------------------------------------------------------------
def build_solution(n, trace):
    """Build list of rod sizes for optimal solution (given trace from extended algorithm)."""
    pieces = []
    while n > 0:
        cut = trace[n]
        pieces.append(cut)
        n = n - cut
    return pieces

#-----------------------------------------------------------------------
if __name__ == '__main__':
    from optparse import OptionParser, OptionGroup
    import sys
    import time
    import random

    distributions = {
        'gauss' : (lambda:random.gauss(options.density, 0.5)),
        'pareto' : (lambda:options.density*random.paretovariate(10)),
        }
        
    algorithms = {
        'recursion' : (cut_rod, None),
        'memoize' :   (memoized_cut_rod, None),
        'bottomup' :  (bottom_up_cut_rod, extended_bottom_up_cut_rod),
        }

    def quit(message=None):
        if message: print(message)
        sys.exit(1)

    parser = OptionParser(usage='usage: %prog [options]')

    parser.add_option('-n', dest='n', metavar='LENGTH', type='int', default=10,
                      help='length of original rod [default: %default]')
    
    parser.add_option('-P', dest='echo',  default=False, action='store_true',
                    help='echo price list to console [default: %default]')
    
    group = OptionGroup(parser, "Algorithm Options",
                        'Available algorithms: ' + ', '.join(sorted(algorithms.keys())))
    group.add_option('-a', dest='algorithm', default='bottomup',
                      choices=sorted(algorithms.keys()),
                      help='algorithm choice [default: %default]')

    group.add_option('-V', dest='verbose',  default=False, action='store_true',
                    help='generate verbose solution [default: %default]')
    
    parser.add_option_group(group)

    group = OptionGroup(parser, "Pricing Options")
    group.add_option('-f', dest='file', default=None,
                      help='read (whitespace separated) price list from file')
    
    group.add_option('-d', dest='density', type='int', default=10,
                      help='average price per unit [default: %default]')
    
    group.add_option('-s', dest='seed', type='int', default=None,
                      help='random seed for generating prices [default: %default]')

    group.add_option('-r', dest='distribution', default='gauss',
                      choices=sorted(distributions.keys()),
                      help='random distribution function for price densities [default: %default]')
    
    parser.add_option_group(group)

    group = OptionGroup(parser, "Available Distributions",
                        ', '.join(sorted(distributions.keys())))
    parser.add_option_group(group)

    (options,args) = parser.parse_args()

    if options.n <= 0:
        quit('n must be positive')
    
    if options.seed is None:
        options.seed = random.randrange(1000000)
        print('Using seed: {0}'.format(options.seed))
    random.seed(options.seed)

    alg = algorithms[options.algorithm][options.verbose]
    if alg is None:
        print('Verbose solutinos not implemented for algorithm {0}'.format(options.algorithm))
        sys.exit(1)

    if options.file is not None:
        try:
            raw = open(options.file,'r').read()
        except IOError:
            print('Unable to open file {0}'.format(options.file))
            sys.exit(1)
        try:
            prices = [0] + [int(token) for token in raw.split()]
        except ValueError:
            print('Prices must be integers.')
            sys.exit(1)
        if len(prices) < 1 + options.n:
            print('Must specify n prices')
            sys.exit(1)
    else:
        prices = [int(k*distributions[options.distribution]()) for k in range(1 + options.n)]

    if options.echo:
        print('Prices: {0}'.format(repr(prices)))


    result = alg(prices, options.n)
    if not options.verbose:
        print('optimal revenue is {0}'.format(result))
    else:
        revenue,solutions = result
        print('optimal revenue is {0}'.format(revenue[options.n]))
        pieces = build_solution(options.n, solutions)
        print('cut into pieces with size: {0}'.format(', '.join(str(p) for p in pieces)))
