0

I am trying solve the following question:

Given an array with a length of 1-9 elements consisting of digits 0-9, what is the largest number divisble by 3 that can be formed using some / all the elements in the array?

The question only accepts Java and Python, and I chose Python despite being completely inexperienced with it.

I looked around and it seems like the general idea was to kick off the smallest element to get the digits total divisible by 3, and wrote the following "subtractive" approach:

def maxdiv3(l):
    l.sort()

    tot = 0
    for i in l:
        tot += i

    if tot % 3 == 0:
        l.sort(reverse=True)
        return int(''.join(str(e) for e in l))
    elif tot % 3 == 1:
        cl = [] # A copy of the list but only for elements % 3 != 0
        acl = [] # Anti copy of the list, only for elements % 3 = 0
        for i in l:
            if i % 3 == 0:
                acl.append(i)
            else:
                cl.append(i)

        removed = False
        nl = [] # A new list for the final results
        for i in cl:
            if not removed:
                if i % 3 == 1:
                    removed = True
                else:
                    nl.append(i)
            else:
                nl.append(i)

        if removed:
            nl.extend(acl)
            nl.sort(reverse=True)
            if len(nl) > 0:
                return int(''.join(str(e) for e in nl))
            else:
                return 0
        else:
            if len(acl) > 0:
                acl.sort(reverse=True)
                return int(''.join(str(e) for e in acl))

        return 0
    elif tot % 3 == 2:
        cl = []
        acl = []
        for i in l:
            if i % 3 == 0:
                acl.append(i)
            else:
                cl.append(i)

        removed2 = False
        nl = []
        for i in cl:
            if not removed2:
                if i % 3 == 2:
                    removed2 = True
                else:
                    nl.append(i)
            else:
                nl.append(i)

        if removed2:
            nl.extend(acl)
            nl.sort(reverse=True)
            if len(nl) > 0:
                return int(''.join(str(e) for e in nl))

        removed1 = 0
        nl = []
        for i in cl:
            if removed1 < 2:
                if i % 3 == 1:
                    removed1 += 1
                else:
                    nl.append(i)
            else:
                nl.append(i)

        if removed1 == 2:
            nl.extend(acl)
            nl.sort(reverse=True)
            if len(nl) > 0:
                return int(''.join(str(e) for e in nl))

        if len(acl) > 0:
            acl.sort(reverse=True)
            return int(''.join(str(e) for e in acl))
        else:
            return 0

This approach kept gets stuck on a hidden test case, which means I can't work out what or why.

Based on this, I wrote up a new one:

def maxdiv3(l):
    l.sort()

    l0 = []
    l1 = []
    l2 = []
    for i in l:
        if i % 3 == 0:
            l0.append(i)
        elif i % 3 == 1:
            l1.append(i)
        elif i % 3 == 2:
            l2.append(i)

    tot = sum(l)

    nl = []

    if tot % 3 == 0:
        nl = l

        nl.sort(reverse=True)

        if len(nl) > 0:
            return int(''.join(str(e) for e in nl))
        
        return 0
    elif tot % 3 == 1:
        if len(l1) > 0:
            l1.remove(l1[0])

            nl.extend(l0)
            nl.extend(l1)
            nl.extend(l2)

            nl.sort(reverse=True)

            if len(nl) > 0:
                return int(''.join(str(e) for e in nl))

            return 0
        elif len(l2) > 1:
            l2.remove(l2[0])
            l2.remove(l2[0])

            nl.extend(l0)
            nl.extend(l1)
            nl.extend(l2)

            nl.sort(reverse=True)

            if len(nl) > 0:
                return int(''.join(str(e) for e in nl))

            return 0
        else:
            return 0
    elif tot % 3 == 2:
        if len(l2) > 0:
            l2.remove(l2[0])

            nl.extend(l0)
            nl.extend(l1)
            nl.extend(l2)

            nl.sort(reverse=True)

            if len(nl) > 0:
                return int(''.join(str(e) for e in nl))

            return 0
        elif len(l1) > 1:
            l1.remove(l1[0])
            l1.remove(l1[0])

            nl.extend(l0)
            nl.extend(l1)
            nl.extend(l2)

            nl.sort(reverse=True)

            if len(nl) > 0:
                return int(''.join(str(e) for e in nl))

            return 0
        else:
            return 0

And this one does pass all the test cases, including the hidden ones.

Here are some of the test cases that I ran my attempt through:

[3, 9, 5, 2] -> 93
[1, 5, 0, 6, 3, 5, 6] -> 665310
[5, 2] -> 0
[1] -> 0
[2] -> 0
[1, 1] -> 0
[9, 5, 5] -> 9

It seems to me that my attempt and the SO solution had the same idea in mind, so what did I neglect to consider? How is the SO solution different from mine and how does that catch whatever it is that my attempt didn't?

Thank you for your time.


A slightly off topic additional question: How do I find edge cases when dealing with blind test cases? I built a random input generator for this question but that didn't help anywhere near I wished it would and is likely not a good general solution.

  • 1
    Hi. I think It's better to post your question in [codereview.stackexchange.com/](https://codereview.stackexchange.com/) – Mr Alihoseiny Jan 17 '22 at 11:03
  • 1
    @MrAlihoseiny Is it? From its "what can I ask here" page: _If you are trying to figure out why your program crashes or produces a wrong result, ask on Stack Overflow instead._ – 404 Name Not Found Jan 17 '22 at 11:06
  • 2
    Divisibility by 3 rule is the sum of all digits being able to divisible by 3. So find the sum (`s`) of all given digits to check if it is divisible by 3. if not, either `s-1` or `s-2` should be. Once you find the target then find the combination of numbers that sums to the target. (AKA Subset Sum Problem) Then chose the longest subset and sort descending. – Redu Jan 17 '22 at 11:29
  • Obviously, if by chance if the sum of all given numbers, `s` is already divisible by 3 then you don't need Subset Sum work. Just sort them descending. – Redu Jan 17 '22 at 11:39
  • @Redu The algorithm you mentioned seems to fail for input `[9, 5, 5]` - the total here is 19, and your algorithm would have me searching for an 18, when the solution is actually a `9`. Or have I misunderstood something? – 404 Name Not Found Jan 17 '22 at 11:39
  • 1
    Did you try edge case `[]` as input to your algo? what it is result? what should it be? – Patrick Artner Jan 17 '22 at 11:54
  • 1
    @PatrickArtner I didn't, because the question states that there will be 1-9 digits in the array, therefore I ignored an empty array input case. But I did test my attempt and the SO-inspired one, and my attempt crashed while the SO-inspired one returned a 0. I updated the main post to reflect this constraint. – 404 Name Not Found Jan 17 '22 at 11:59
  • It doesn't guarantee that there will be a subset but it guarantees that if there is a subset it would definitelly have the answer in it. You should therefore just iterate for the `s-3` or lower if need be in case the subset is empty. – Redu Jan 17 '22 at 12:07
  • @Redu So for `[9, 5, 5]`, I would get a total of 19, search for an 18, then a 15, 12, and finally find a 9? – 404 Name Not Found Jan 17 '22 at 12:39
  • 1
    Your failed attempt never removes a digit that is different from the total modulo 3. The actual algorithm sometimes removes two such digits. – n. m. could be an AI Jan 17 '22 at 12:40
  • @Redu this problem has nothing to do whatsoever with subset sum. – n. m. could be an AI Jan 17 '22 at 12:44
  • @n.1.8e9-where's-my-sharem. Thank you for the the hint - I thought since I was off by 1, I could only remove an element with modulo 1, whereas being off by 2 I could remove either a modulo 2 element or 2 modulo 1 elements and didn't think any further. – 404 Name Not Found Jan 17 '22 at 14:05
  • Could you please clarify what *"Given an array of 1 to 9 digits"* means? Is it that the array has between 1 and 9 elements? Or is it that the digits are between 1 and 9? – Stef Jan 17 '22 at 14:19
  • @Stef The array has between 1 and 9 elements, each ranging from 0 to 9. Hopefully the main post is clearer now. – 404 Name Not Found Jan 17 '22 at 14:24

2 Answers2

2

How do I find edge cases when dealing with blind test cases?

Here is what I did as tester:

digits = list(range(10))
for k in range(1, 10):  # Try different sizes
    for _ in range(100):  # Repeat many times
        lst = random.choices(digits, k=k)  # Produce random digits
        a = maxdiv3(lst)  # Run working solution
        b = maxdiv3b(lst)  # Run solution with a problem
        if a != b:  # Found a deviation!
            print(lst)
            break

This was one of the lists I got:

[2, 2, 5, 5, 8]

Then I retraced your code with that input and came into this block:

    else:
        if len(acl) > 0:
            acl.sort(reverse=True)
            return int(''.join(str(e) for e in acl))

We are here in the case where the total has a remainder of 1 when divided by 3. We get in this else block when there is no individual digit with such a remainder. It then just outputs all the digits that are multiples of 3. But this is not always right. When there are at least two digits with a remainder of 2 (i.e. 2, 5, 8), such a pair represents a total that has a remainder of 1, i.e. you only have to remove two digits, not more.

The correction is to remove two of those digits (the smallest) and then join the two lists as you did elsewhere in the code:

    else:
        del nl[0:2]
        nl.extend(acl)
        nl.sort(reverse=True)
        if len(nl) > 0:
            return int(''.join(str(e) for e in nl))
        else:
            return 0

NB: It didn't help that the chosen names are not very descriptive. I have no clue what acl, nl, or cl are abbreviations of.

trincot
  • 317,000
  • 35
  • 244
  • 286
  • _Here's what I did as a tester_: Thank you for the tip, although I was hoping for one where I didn't already have the working solution. Either way, thank you for the insight as to where my attempt failed. Apologies for the variable names, indeed they could've been named better... PS: It's `copy list`, `anti copy list`, and `new list` - I'll add a comment where they're first used. – 404 Name Not Found Jan 17 '22 at 14:16
  • 2
    If you don't have a working solution, create a solution that uses a more brute force (inefficient) algorithm, like one that just takes all digits, then all digits with one removed (try them all), then with 2 digits removed (try them all). It would construct the numbers and pick the first one that is a multiple of 3. The brute force method could foresee in removing 3 digits and more, but that would never be needed, because it is guaranteed to find a multiple of 3 by removing at most 2 digits. – trincot Jan 17 '22 at 14:22
  • Thank you for the idea - hopefully I'll be more successful next time I come across a problem. – 404 Name Not Found Jan 17 '22 at 14:30
0

You can use recursivity to "improve" the digit selection, stopping as soon as the sum of digits is a multiple of 3 (all numbers divisible by 3 have a sum of digits that is a multiple of 3). Only removing digits that are not multiples of 3 can improve the result

The largest number is the one that has digits in decreasing order.

def div3(D):
    if sum(D)%3 == 0:
        return sum(d*10**i for i,d in enumerate(sorted(D)))
    return max(div3(D[:i]+D[i+1:]) for i,d in enumerate(D) if d%3)

d = [1,2,3,4,5]
print(div3(d))
# 54321

d = [2, 2, 5, 5, 8]
print(div3(d))
# 855

print(div3([4,1]))
# 0
Alain T.
  • 40,517
  • 4
  • 31
  • 51