1

I am working on LeetCode problem 46. Permutations:

Given an array nums of distinct integers, return all the possible permutations. You can return the answer in any order.

I thought to solve this using backtracking. My idea is to image this problem as a binary tree and step down a path. When I get to a leaf I pop the visit array and restore to a new root number.

My code below:

   class Solution:
        def permute(self, nums: List[int]) -> List[List[int]]:
            perms = []
            def dfs(curr, l):
                if len(nums) == len(curr):
                    perms.append([int(i) for i in curr])
                    return 
                visit = []
                for t in nums:
                    if str(t) not in curr:
                        visit.append(t)
                        dfs(curr + str(l), t)
                        visit.pop()
                return 
            dfs('', nums[0])
        return perms

I get wrong output for the following test case:

nums = [1,2,3]

The expected output is:

[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

But my code outputs:

[[1,1,2],[1,1,2],[1,1,3],[1,1,3],[1,2,2],[1,2,3],[1,3,2],[1,3,3]]

I don't understand why my output has lists with duplicate ones, even though I check that str(t) not in curr to avoid that duplicate use of a number.

What am I missing?

trincot
  • 317,000
  • 35
  • 244
  • 286
  • Can you draw a tree you want to traverse? – n. m. could be an AI Jan 27 '23 at 04:53
  • @n.m. yes but not sure how to share. I can create a tree for each permutarion with first integer as the root and las as a leaf example 1 as root then 2 then 3 with edges in between then another three with 2 as root and say 3 as left represents 213 permutation – peter todds Jan 27 '23 at 05:15
  • In the question you are implying that the problem is a tree and each leaf is a solution. I don't see that in your comment. You can upload an image to any free image hosting, [edit] the question and add a link or possibly an inline image. – n. m. could be an AI Jan 27 '23 at 05:25
  • The central, underlying problem is that recursively calling `dfs(curr + str(l), t)` has **no effect** on the local `visit`, because **each call to a function** (including recursive calls, which are **not in any way special**) has its own local variables, and `visit` is local. Please see the linked duplicate for details (the problem described is slightly different, but the cause and solution are the same). There are several other typos and other such minor issues in the code; but we **do not provide a debugging service**; questions are supposed to be about **one** problem. – Karl Knechtel Jan 27 '23 at 08:16
  • @KarlKnechtel, I think the dupe reference is wrong. The asker is not expecting the recursive call to *return* anything: the `None` is intended. They rely on the side effect of populating `perms`, and so the return value of the recursive function is not relevant here. Also the `visit` list you refer to is irrelevant. The code doesn't rely on it, so it has no positive nor negative effect on the algorithm, which relies on `curr` for the "visited" behaviour. – trincot Jan 27 '23 at 09:38
  • @trincot Yes, it is wrong. I had missed the fact that `perms` is populated as a side effect and the code is supposed to work on that basis. However, the question still should be closed for one reason or another. I leave it to you if you have a better duplicate link; but otherwise this simply Needs More Focus - you pointed out several problems with the code, and it's a plain debugging request. – Karl Knechtel Jan 27 '23 at 09:40
  • What is your definition of "backtracking"? How does your code implement backtracking according to that definition? – trincot Jan 28 '23 at 13:18
  • Plus one to trincot's question. – גלעד ברקן Jan 28 '23 at 13:20

2 Answers2

0

Here's the backtracking version:

def f(lst, curr = []):
  if len(lst) == len(curr):
    return [tuple(curr)]
  result = []
  for e in lst:
    if e not in curr:
      curr.append(e)
      result.extend(f(lst, curr))
      curr.pop()
  return result

lst = [1, 2, 3, 4]
print(f(lst))
גלעד ברקן
  • 23,602
  • 3
  • 25
  • 61
-1

The main reason you have duplicate 1 in your output tuples (in the example) is that in the recursive call you are not appending the right number to curr. l is already in curr once you are in a recursive call! It should be dfs(curr + str(t), t) since you have verified that t is not in curr, it should be that number that is appended to it.

And when you make this change, there is no more need for the l parameter in dfs, as l is no longer used.

There are a few other issues however:

  • return perms has a wrong indentation (probably a typo in your question?)

  • The code assumes that numbers are always single characters when converted to string, but the code challenge indicates that a number may be 10 or may be negative, and so the way you check that a number is already in curr will not be reliable. For instance, if you first visit "10" and then want to visit "1", it will not work because if str(t) not in curr: will not be true.

    Secondly, [int(i) for i in curr] will extract only one-digit numbers, and will fail if you had appended a negative number in curr, as then int('-') will raise an exception.

Not a problem, but the visited list is useless in your code. It is never used to verify anything. And also the return as last statement in dfs is not really needed, as this is the default behaviour.

I would suggest to make curr a list of numbers instead of a string.

Here is your code with the above changes applied:

class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        perms = []
        
        def dfs(curr):
            if len(nums) == len(curr):
                perms.append(curr)
                return
                
            for t in nums:
                if t not in curr:
                    dfs(curr + [t])

        dfs([])
        return perms

It would be nice to use a generator here:

class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        def dfs(curr):
            if len(nums) == len(curr):
                yield curr
                return
                
            for t in nums:
                if t not in curr:
                    yield from dfs(curr + [t])

        return list(dfs([]))

Finally, there is of course ... itertools:

import itertools

class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        return list(itertools.permutations(nums))
trincot
  • 317,000
  • 35
  • 244
  • 286
  • This talks about some aspects of the question but does not seem to provide a backtracking solution. (Did not downvote.) – גלעד ברקן Jan 28 '23 at 10:04
  • According to Wikipedia a backtracking solution is one *"that incrementally builds candidates to the solutions, and abandons a candidate ("backtracks") as soon as it determines that the candidate cannot possibly be completed to a valid solution"*. The candidates are represented by `curr`, and it is abandoned when the condition of the `if` statement is not true. The definition of "backtracking" is quite broad and can be interpreted differently. Maybe you have an issue with the fact that `curr` is dealt with as immutable and the "incrementing" consists of creating a new copy? – trincot Jan 28 '23 at 10:53
  • Neither yours or my solution seem to fit this definition since rather than determining "that the candidate cannot possibly be completed to a valid solution," they quite literally explore a candidate to its completion, then add it to a list. You make a good point and it could be that the kind of backtracking where the mutable list is altered in a "winding back" fashion is a more colloquial understanding of the term, "backtracking." – גלעד ברקן Jan 28 '23 at 13:09
  • Well, we can say that the detection of "cannot possibly be completed" occurs at the `if` statement which rejects duplicate entries even before adding them. As there are no other constraints in this particular problem, no other cases need to be rejected. But I think we are diving too deep here, as many use the term backtracking in a very loose way, more as a synonym for recursion, and it wouldn't surprise me this is the case in this question too. – trincot Jan 28 '23 at 13:17
  • Let's ask the OP! – גלעד ברקן Jan 28 '23 at 13:17
  • The if statement doesn't cut the branch, though. There's always some element to append. – גלעד ברקן Jan 28 '23 at 13:24
  • Indeed, I look at it from the viewpoint of the *considered* next state (which would have `t` appended), but the check happens before adding it. If the check would happen later, like *"does `curr` contain a duplicate, then backtrack"* it would be more real backtracking, but here it happens early enough not to make a recursive call. – trincot Jan 28 '23 at 13:27