This was an interview question I had and I was embarrassingly pretty stumped by it. Wanted to know if anyone could think up an answer to it and provide the big O notation for it
This came to mind, it's the brute force approach influenced by the bars and stars problem.
Let's say our number is "12345" and we have 2 * operators we need to use. We can look at the string 12345 as
1_2_3_4_5
Where we can put the two * operators on any of the underscores. Since there are 4 underscores and 2 * operators, there are 4 choose 2 (or 6) different ways to place the operators. Compare those 6 possibilities and grab the largest number. A similar approach can be used for larger strings and larger number of * operators.
here's another Java solution. (I know it's correct for "312" and 1 multiplication and I think it works for others...
You'll have to remember how to obtain the complexity of recursive methods on your own, haha.
package test;
import java.util.ArrayList;
import java.util.List;
public class BiggestNumberMultiply {
private static class NumberSplit{
String[] numbers;
long result;
NumberSplit(String[] numbers){
this.numbers=numbers.clone();
result=1;
for(String n:numbers){
result*=Integer.parseInt(n);
}
}
@Override
public String toString() {
StringBuffer sb=new StringBuffer();
for(String n:numbers){
sb.append(n).append("*");
}
sb.replace(sb.length()-1, sb.length(), "=")
.append(result);
return sb.toString();
}
}
public static void main(String[] args) {
String numbers = "312";
int numMults=1;
int numSplits=numMults;
List<NumberSplit> splits = new ArrayList<NumberSplit>();
splitNumbersRecursive(splits, new String[numSplits+1], numbers, numSplits);
NumberSplit maxSplit = splits.get(0);
for(NumberSplit ns:splits){
System.out.println(ns);
if(ns.result>maxSplit.result){
maxSplit = ns;
}
}
System.out.println("The maximum is "+maxSplit);
}
private static void splitNumbersRecursive(List<NumberSplit> list, String[] splits, String numbers, int numSplits){
if(numSplits==0){
splits[splits.length-1] = numbers;
return;
}
for(int i=1; i<=numbers.length()-numSplits; i++){
splits[splits.length-numSplits-1] = numbers.substring(0,i);
splitNumbersRecursive(list, splits, numbers.substring(i), numSplits-1);
list.add(new NumberSplit(splits));
}
}
}
I found the above DP solution helpful but confusing. The recurrence makes some sense, but I wanted to do it all in one table without that final check. It took me ages to debug all the indices, so I've kept some explanations.
To recap:
The complexity is O(N2k) because maximizing over a is O(N), and we do it O(k) times for each digit (O(N)).
public class MaxProduct {
public static void main(String ... args) {
System.out.println(solve(args[0], Integer.parseInt(args[1])));
}
static long solve(String digits, int k) {
if (k == 0)
return Long.parseLong(digits);
int N = digits.length();
long[][] T = new long[N][k+1];
for (int i = 0; i < N; i++) {
T[i][0] = Long.parseLong(digits.substring(0,i+1));
for (int j = 1; j <= Math.min(k,i); j++) {
long max = Integer.MIN_VALUE;
for (int a = 0; a < i; a++) {
long l = Long.parseLong(digits.substring(a+1,i+1));
long prod = l * T[a][j-1];
max = Math.max(max, prod);
}
T[i][j] = max;
}
}
return T[N-1][k];
}
}
I am assuming here that the required number m of multiplication operators is given as part of the problem, along with the string s of digits.
You can solve this problem using the tabular method (aka "dynamic programming") with O(m |s|2) multiplications of numbers that are O(|s|) digits long. The optimal computational complexity of multiplication is not known, but with the schoolbook multiplication algorithm this is O(m |s|4) overall.
(The idea is to compute the answer for each subproblem consisting of a tail of the string and a number m′ ≤ m. There are O(m |s|) such subproblems and solving each one involves O(|s|) multiplications of numbers that are O(|s|) digits long.)
In Python, you could program it like this, using the @memoized decorator from the Python decorator library:
@memoized
def max_product(s, m):
"""Return the maximum product of digits from the string s using m
multiplication operators.
"""
if m == 0:
return int(s)
return max(int(s[:i]) * max_product(s[i:], m - 1)
for i in range(1, len(s) - m + 1))
If you're used to the bottom-up form of dynamic programming where you build up a table, this top-down form might look strange, but in fact the @memoized decorator maintains the table in the cache
property of the function:
>>> max_product('56789', 1)
51102
>>> max_product.cache
{('89', 0): 89, ('9', 0): 9, ('6789', 0): 6789, ('56789', 1): 51102, ('789', 0): 789}
This implementation is for @lars.
from __future__ import (print_function)
import collections
import sys
try:
xrange
except NameError: # python3
xrange = range
def max_product(s, n):
"""Return the maximum product of digits from the string s using m
multiplication operators.
"""
# Guard condition.
if len(s) <= n:
return None
# A type for our partial solutions.
partial_solution = collections.namedtuple("product",
["value", "expression"])
# Initialize the best_answers dictionary with the leading terms
best_answers = {}
for i in xrange(len(s)):
term = s[0: i+1]
best_answers[i+1] = partial_solution(int(term), term)
# We then replace best_answers n times.
for prev_product_count in [x for x in xrange(n)]:
product_count = prev_product_count + 1
old_best_answers = best_answers
best_answers = {}
# For each position, find the best answer with the last * there.
for position in xrange(product_count+1, len(s)+1):
candidates = []
for old_position in xrange(product_count, position):
prior_product = old_best_answers[old_position]
term = s[old_position:position]
value = prior_product.value * int(term)
expression = prior_product.expression + "*" + term
candidates.append(partial_solution(value, expression))
# max will choose the biggest value, breaking ties by the expression
best_answers[position] = max(candidates)
# We want the answer with the next * going at the end of the string.
return best_answers[len(s)]
print(max_product(sys.argv[1], int(sys.argv[2])))
Here is a sample run:
$ python mult.py 99287 2
product(value=72036, expression='9*92*87')
Hopefully the logic is clear from the implementation.
Yet another Java implementation. This is DP top down, aka memoization. It also prints out the actual components besides the max product.
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class MaxProduct {
private static Map<Key, Result> cache = new HashMap<>();
private static class Key {
int operators;
int offset;
Key(int operators, int offset) {
this.operators = operators;
this.offset = offset;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + offset;
result = prime * result + operators;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof Key)) {
return false;
}
Key other = (Key) obj;
if (offset != other.offset) {
return false;
}
if (operators != other.operators) {
return false;
}
return true;
}
}
private static class Result {
long product;
int offset;
Result prev;
Result (long product, int offset) {
this.product = product;
this.offset = offset;
}
@Override
public String toString() {
return "product: " + product + ", offset: " + offset;
}
}
private static void print(Result result, String input, int operators) {
System.out.println(operators + " multiplications on: " + input);
Result current = result;
System.out.print("Max product: " + result.product + " = ");
List<Integer> insertions = new ArrayList<>();
while (current.prev != null) {
insertions.add(current.offset);
current = current.prev;
}
List<Character> inputAsList = new ArrayList<>();
for (char c : input.toCharArray()) {
inputAsList.add(c);
}
int shiftedIndex = 0;
for (int insertion : insertions) {
inputAsList.add(insertion + (shiftedIndex++), '*');
}
StringBuilder sb = new StringBuilder();
for (char c : inputAsList) {
sb.append(c);
}
System.out.println(sb.toString());
System.out.println("-----------");
}
public static void solve(int operators, String input) {
cache.clear();
Result result = maxProduct(operators, 0, input);
print(result, input, operators);
}
private static Result maxProduct(int operators, int offset, String input) {
String rightSubstring = input.substring(offset);
if (operators == 0 && rightSubstring.length() > 0) return new Result(Long.parseLong(rightSubstring), offset);
if (operators == 0 && rightSubstring.length() == 0) return new Result(1, input.length() - 1);
long possibleSlotsForFirstOperator = rightSubstring.length() - operators;
if (possibleSlotsForFirstOperator < 1) throw new IllegalArgumentException("too many operators");
Result maxProduct = new Result(-1, -1);
for (int slot = 1; slot <= possibleSlotsForFirstOperator; slot++) {
long leftOperand = Long.parseLong(rightSubstring.substring(0, slot));
Result rightOperand;
Key key = new Key(operators - 1, offset + slot);
if (cache.containsKey(key)) {
rightOperand = cache.get(key);
} else {
rightOperand = maxProduct(operators - 1, offset + slot, input);
}
long newProduct = leftOperand * rightOperand.product;
if (newProduct > maxProduct.product) {
maxProduct.product = newProduct;
maxProduct.offset = offset + slot;
maxProduct.prev = rightOperand;
}
}
cache.put(new Key(operators, offset), maxProduct);
return maxProduct;
}
public static void main(String[] args) {
solve(5, "1826456903521651");
solve(1, "56789");
solve(1, "99287");
solve(2, "99287");
solve(2, "312");
solve(1, "312");
}
}
Bonus: a bruteforce implementation for anyone interested. Not particularly clever but it makes the traceback step straightforward.
import java.util.ArrayList;
import java.util.List;
public class MaxProductBruteForce {
private static void recurse(boolean[] state, int pointer, int items, List<boolean[]> states) {
if (items == 0) {
states.add(state.clone());
return;
}
for (int index = pointer; index < state.length; index++) {
state[index] = true;
recurse(state, index + 1, items - 1, states);
state[index] = false;
}
}
private static List<boolean[]> bruteForceCombinations(int slots, int items) {
List<boolean[]> states = new ArrayList<>(); //essentially locations to insert a * operator
recurse(new boolean[slots], 0, items, states);
return states;
}
private static class Tuple {
long product;
List<Long> terms;
Tuple(long product, List<Long> terms) {
this.product = product;
this.terms = terms;
}
@Override
public String toString() {
return product + " = " + terms.toString();
}
}
private static void print(String input, int operators, Tuple result) {
System.out.println(operators + " multiplications on: " + input);
System.out.println(result.toString());
System.out.println("---------------");
}
public static void solve(int operators, String input) {
Tuple result = maxProduct(input, operators);
print(input, operators, result);
}
public static Tuple maxProduct(String input, int operators) {
Tuple maxProduct = new Tuple(-1, null);
for (boolean[] state : bruteForceCombinations(input.length() - 1, operators)) {
Tuple newProduct = getProduct(state, input);
if (maxProduct.product < newProduct.product) {
maxProduct = newProduct;
}
}
return maxProduct;
}
private static Tuple getProduct(boolean[] state, String input) {
List<Long> terms = new ArrayList<>();
List<Integer> insertLocations = new ArrayList<>();
for (int i = 0; i < state.length; i++) {
if (state[i]) insertLocations.add(i + 1);
}
int prevInsert = 0;
for (int insertLocation : insertLocations) {
terms.add(Long.parseLong(input.substring(prevInsert, insertLocation))); //gradually chop off the string
prevInsert = insertLocation;
}
terms.add(Long.parseLong(input.substring(prevInsert))); //remaining of string
long product = 1;
for (long term : terms) {
product = product * term;
}
return new Tuple(product, terms);
}
public static void main(String[] args) {
solve(5, "1826456903521651");
solve(1, "56789");
solve(1, "99287");
solve(2, "99287");
solve(2, "312");
solve(1, "312");
}
}