Thursday, May 19, 2022

Leetcode: Dynamic programming on subsets with examples, explained | Dmitry Babichev, PhD

May 19, 2022

Here is the link. 

This is very useful technique, which uses both idea of bitmasks and dymamic programming. What is bitmask? It is specific way to represent data, where 1 means that element is taken and 0 that it is not taken. For example if nums = [1,2,8,9,10], than bitmask 01011 represents set {2, 9, 10}. Of course we can use sets as well, but bitmask make it very fast and efficient.

There is a bunch of leetcode problems (usually they are hard, but sometimes medium as well). Imagine the following problem as examples: given 2n points on the plane we need to construct n segments, using all 2n points, such that sum of length of these segments is minimal. The idea is to keep bitmask of already used nodes: for example 00011101 mask means that we use nodes number 0, 2, 3, 4. Now the question is how to go from one mask to another: we can do it in 6 ways here: 11011101, 10111101, 10011111, 01111101, 01011111, 00111111. In general we will have 2^n masks and O(n^2) ways to go from one mask to another. Now, when you get the idea of how it works let us consider several problems from leetcode, where this idea can be applied:

Problem 698: Partition to K Equal Sum Subsets

https://leetcode.com/problems/partition-to-k-equal-sum-subsets/

Denote by dp[mask] possibility to put numbers with mask mask into our subsets. In more detais: we return -1 if this partition is not possible and we return t >= 0, if it is possible, where t is reminder when we divide sum of all used numbers so far by basket: value, of how many we need to put in each group. Then to check if we can do it, we need to check the last number we put and check n possible options, where n is number of nums. Overall time complexity is (2^n * n) and space complexity is O(2^n).

class Solution:
    def canPartitionKSubsets(self, nums, k):
        N = len(nums)
        nums.sort(reverse = True)

        basket, rem = divmod(sum(nums), k)
        if rem or nums[0] > basket: return False

        dp = [-1] * (1<<N) 
        dp[0] = 0
        for mask in range(1<<N):
            for j in range(N):
                neib = dp[mask ^ (1<<j)]
                if mask & (1<<j) and neib >= 0 and neib + nums[j] <= basket:
                    dp[mask] = (neib + nums[j]) % basket
                    break

        return dp[-1] == 0

Problem 473 Matchsticks to Square

https://leetcode.com/problems/matchsticks-to-square/

Special case of Problem 698 Partition to K Equal Sum Subsets, but here we need to partition into 4 equal sums (we say, put them into buckets). So actually we can reuse exactly the same code we did in Problem 698, but here I do it in different way, using @lru_cache(None), which is used for memoisation. Time complexity is O(2^n*n), space complexity is O(2^n).

class Solution:
    def makesquare(self, nums):
        N = len(nums)
        basket, rem = divmod(sum(nums), 4)
        if rem or nums[0] > basket: return False
        
        @lru_cache(None)
        def dfs(mask):
            if mask == 0: return 0
            for j in range(N):
                if mask & 1<<j:
                    neib = dfs(mask ^ 1<<j)
                    if neib >= 0 and neib + nums[j] <= basket:
                        return (neib + nums[j]) % basket
            return -1
                    
        return dfs((1<<N) - 1) == 0

526. Beautiful Arrangement

https://leetcode.com/problems/beautiful-arrangement/

Let us use dynamic programming with bitmasks. The idea is to use function dfs(bm, pl), where:

  1. bm is binary mask for visited numbers.
  2. pl is current place we want to fill. Idea is to start from the end, and fill places in opposite direction, because for big numbers we potentially have less candidates. (if we start form pl = 0 and go in increasing direction, then it is also will work fine, like 120ms vs 60ms)

Now, let us discuss how dfs(bm, pl) will work:

  1. If we reached place 0 and procces was not interrupted so far, it means that we find beautiful arrangement.
  2. For each number 1, 2, ..., N we try to put this number on place pl: and we need to check two conditions: first, that this place is still empty, using bitmask and secondly that one of the two properties for beutiful arrangement holds. In this case we add dfs(bm^1<<i, pl - 1) to final answer.
  3. Finally, we run dfs(0, N): from the last place and with empty bit-mask.

Complexity: First of all, notice that we have 2^N possible options for bm, N possible options for pl. But in all we have only 2^N states: pl is uniquely defined by number of nonzero bits in bm. Also we have N possible moves from one state to another, so time complexity is O(N*2^N). Space complexity is O(2^N).

class Solution:
    def countArrangement(self, N):
        @lru_cache(None)
        def dfs(bm, pl):
            if pl == 0: return 1
                
            S = 0
            for i in range(N):
                if not bm&1<<i and ((i+1)%pl == 0 or pl%(i+1) == 0):
                    S += dfs(bm^1<<i, pl - 1)
            return S
                
        return dfs(0, N)

Problem 847 Shortest Path Visiting All Nodes

https://leetcode.com/problems/shortest-path-visiting-all-nodes/

Let us first find distances between all pairs of nodes, using either bfs or better Floyd-Warshall algorithm with O(n^3) complexity. (do not be afraid of this algorithm, in fact it is just usual dp, but it is not the point of interest here, we want to focus on dp on subsets.

Then our problem is equivalent to Traveling Salesman Problem (TSP): we need to find path with smallest weight, visiting all nodes. Here we have 2^n * n possible states, where state consists of two elements:

  1. mask: binary mask with length n with already visited nodes.
  2. last: last visited node.

Note, that it is wary similar to previous approach we used, but now we need to include last visited node in our state.

There will be O(n) possible transitions for each state: for example to the state (10011101, 7) we will have possible steps from (00011101, 0), (00011101, 2), (00011101, 3), (00011101, 4). Finally, time complexity is O(2^n*n^2) and space complexity is O(2^n*n).

class Solution:
    def shortestPathLength(self, graph):
        n = len(graph)
        W = [[float("inf")] * n for _ in range(n)]
        #for i in range(n): W[i][i] = 0
        for i in range(n):
            for j in graph[i]:
                W[i][j] = 1
        
        for i,j,k in product(range(n), repeat = 3):
            W[i][j] = min(W[i][j], W[i][k] + W[k][j])
                    
        dp = [[float("inf")] * n for _ in range(1<<n)]
        for i in range(n): dp[1<<i][i] = 0
            
        for mask in range(1<<n):
            n_z_bits = [j for j in range(n) if mask&(1<<j)]
            
            for j, k in permutations(n_z_bits, 2):
                cand = dp[mask ^ (1<<j)][k] + W[k][j]
                dp[mask][j] = min(dp[mask][j], cand)

        return min(dp[-1])

Problem 943 Find the Shortest Superstring

https://leetcode.com/problems/find-the-shortest-superstring/

This is actually Travelling salesman problem (TSP) problem: if we look at our strings as nodes, then we can evaluate distance between one string and another, for example for abcde and cdefghij distance is 5, because we need to use 5 more symbols fghij to continue first string to get the second. Note, that this is not symmetric, so our graph is oriented. Our state will be (bitmask, last) as in problem 847.

Complexity is O(n^2 * 2^n), because every state is bitmask + last visited node. In each dp[i][j] we will keep length of built string + built string itself.

class Solution:
    def shortestSuperstring(self, A):
        @lru_cache(None)
        def connect(w1, w2):
            return [w2[i:] for i in range(len(w1) + 1) if w1[-i:] == w2[:i] or not i][-1]
            
        N = len(A) 
        dp = [[(float("inf"), "")] * N for _ in range(1<<N)]
        for i in range(N): dp[1<<i][i] = (len(A[i]), A[i])
            
        for mask in range(1<<N):
            n_z_bits = [j for j in range(N) if mask & 1<<j]
            
            for j, k in permutations(n_z_bits, 2):
                cand = dp[mask ^ 1<<j][k][1] + connect(A[k], A[j])
                dp[mask][j] = min(dp[mask][j], (len(cand), cand))

        return min(dp[-1])[1]

Problem 1434 Number of Ways to Wear Different Hats to Each Other

https://leetcode.com/problems/number-of-ways-to-wear-different-hats-to-each-other/

Here our state is dp[mask][hat_id], where mask is bitmask for used people and hat_id is number of hats we processed so far. Number of states is O(2^n*m), where n is number of people and m is number of people. So, time complexity is O(2^n*n*m).

class Solution:
    def numberWays(self, hats):
        n, M, N_hats = len(hats), 10**9 + 7, 41

        hats_persons = defaultdict(set) 
        for person_id, person in enumerate(hats):
            hats_persons[person_id] = set(person)

        dp = [[0] * N_hats for _ in range(1<<n)] 
        dp[0][0] = 1

        for mask in range(0, 1<<n):
            for hat_id in range(N_hats):
                for person in range(n):
                    neib = mask ^ 1<<(person)
                    if neib != mask and hat_id in hats_persons[person]:    
                        dp[mask][hat_id] += dp[neib][hat_id-1]

                dp[mask][hat_id] = (dp[mask][hat_id] + dp[mask][hat_id-1]) % M

        return int(dp[-1][-1] % M)

Problem 1494 Parallel Courses II

https://leetcode.com/problems/parallel-courses-ii/

Let us denote by dp[i][j] tuple with:

  1. minumum number of days we need to finish
  2. number of non-zero bits for binary mask of the last semester
  3. binary mask of the last semester

all courses denoted by binary mask i and such that the last course we take is j. For example for dp[13][3], i=13 is represented as 1101, and it means that we take courses number 0, 2, 3 and the last one we take is number 3. (instead of starting with 1, let us subtract 1 from all courses and start from 0).

Let us also introduce bm_dep[i]: this will be binary mask for all courses we need to take, before we can take course number i. For example bm_dep[3] = 6 = 110 means, that we need to take courses 1 and 2 before we can take course number 3.

Now, let us iterate over all i in range(1<<n). Let us evaluate n_z_bit, this will be an array with all places with non-zero bits. For example for i=13=1101, n_z_bit = [0,2,3].

What we need to do next, we:

  1. First check that we really can take new course number j, using bm_dep[j] & i == bm_dep[j].
  2. Now, we want to update dp[i][j], using dp[i^(1<<j)][t], for example if we want to find answer for (1,3,4,5) courses with 3 being the last course, it means that we need to look into (1,4,5) courses, where we add course 3.
  3. We check how many courses we already take in last semester, using bits < k, and also make sure, that we can add new course to last semester. Now we have two candidates: (cand, bits + 1, mask + (1<<j)) and dp[i][j] and we need to choose the best one. In other case, we need to take new semester: so cands will be equalt to cands + 1, bits will be equal to 1 and binary mask for last semester is 1<<j.

Complexity is O(n^2*2^n), because we iterate all bitmasks and then we iterate over all pairs of non-zero bit, and we heve O(n^2) of them. Memory is O(2^n * n).
I think it can be simplified to O(n*2^n)/O(2^n) complexity, but I am not sure yet.

class Solution:
    def minNumberOfSemesters(self, n, dependencies, k):
        dp = [[(100, 0, 0)] * n for _ in range(1<<n)]
        
        bm_dep = [0]*(n)
        for i,j in dependencies:
            bm_dep[j-1]^=(1<<(i-1))

        for i in range(n):
            if bm_dep[i] == 0: dp[1<<i][i] = (1, 1, 1<<i)
        
        for i in range(1<<n):
            n_z_bits = [len(bin(i))-p-1 for p,c in enumerate(bin(i)) if c=="1"]
                    
            for t, j in permutations(n_z_bits, 2):
                if bm_dep[j] & i == bm_dep[j]:
                    cand, bits, mask = dp[i^(1<<j)][t]
                    if bm_dep[j] & mask == 0 and bits < k:
                        dp[i][j] = min(dp[i][j], (cand, bits + 1, mask + (1<<j)))
                    else:
                        dp[i][j] = min(dp[i][j], (cand+1, 1, 1<<j))
                                          
        return min([i for i, j, k in dp[-1]])

Problem 1655 Distribute Repeating Integers

https://leetcode.com/problems/distribute-repeating-integers/

Let us precalculate sums of all subsets first for speed. Let us define our state as dfs(i, mask), where i is index not of customer, but Counter(nums):

Imagine we have Counter(nums) = 10, 5 and quantities = 5,7,3, then we first try to distribute 10: on different bitmasks: 000, 001, 010, 011, 100, 101, 110, 111, here we can choose 000, 001, 010, 001, 101, 011: 000 coressponds to emty, 010 to 7, 101 to 5 + 3 and so on).

For each state and mask we need to iterate over all submasks and we can do it efficiently in the way it is shown in code. It can be proven, that time complexity of this code is O(m*3^m), space complexity is O(m*2^m).

class Solution:
    def canDistribute(self, a, customers):
        m = len(customers)
        a = sorted(Counter(a).values())[-m:]
        n = len(a)
        
        mask_sum = [0]*(1<<m)
        for mask, i in product(range(1<<m), range(m)):
            if (1 << i) & mask:
                mask_sum[mask] += customers[i]
                    
        
        @lru_cache(None)
        def dfs(i, mask):
            if mask == 0: return True
            if i == n:    return False
            
            submask = mask
            while submask:
                if mask_sum[submask] <= a[i] and dfs(i+1, mask ^ submask):
                    return True
                submask = (submask-1) & mask
            return dfs(i+1, mask)
                
        return dfs(0, (1<<m) - 1)

Problem 1659 Maximize Grid Happiness

https://leetcode.com/problems/maximize-grid-happiness/

Let us us dynamic programming with the following states:

  1. index: number of cell in our grid, going from 0 to mn-1: from top left corner, line by line.
  2. row is the next row filled with elements 0, 1 (for introvert) or 2 (for extravert): see on my diagramm.
  3. I is number of interverts we have left.
  4. E is number of extraverts we have left.

image

Now, let us fill out table element by element, using dp function:

  1. First of all, if we reached index == -1, we return 0, it is our border case.
  2. Compute R and C coordinates of our cell.
  3. Define neibs: it is parameters for our recursion: fist element is what we put into this element: 0, 1 or 2, second and third elements are new coordinates and last one is gain.
  4. Now, we have 3 possible cases we need to cover: new cell is filled with 0, 1 or 2 and for each of these cases we need to calculate ans:
    a) this is dp for our previous row, shifted by one
    b) gain we need to add when we add new intravert / extravert / empty
    c) check right neighbor (if we have any) and add fine: for example if we have 2 introverts, both of them are not happy, so we need to add -30-30, if we have one introvert and one extravert, it is 20-30 and if it is two extraverts it is 20+20.
    d) do the same for down neigbor if we have any (see illustration for help)

Finally, we just return dp(m*n-1, tuple([0]*n), I, E)

Complexity: time complexity is O(m*n*I*E*3^n), because: index goes from 0 to mn-1, row has n elements, each of them equal to 0, 1 or 2.

class Solution:
    def getMaxGridHappiness(self, m, n, I, E):
        InG, ExG, InL, ExL = 120, 40, -30, 20
        fine = [[0, 0, 0], [0, 2*InL, InL+ExL], [0, InL+ExL, 2*ExL]]
        
        @lru_cache(None)
        def dp(index, row, I, E):
            if index == -1: return 0

            R, C, ans = index//n, index%n, []
            neibs = [(1, I-1, E, InG), (2, I, E-1, ExG), (0, I, E, 0)]  
            
            for val, dx, dy, gain in neibs:
                tmp = 0
                if dx >= 0 and dy >= 0:
                    tmp = dp(index-1, (val,) + row[:-1], dx, dy) + gain
                    if C < n-1: tmp += fine[val][row[0]]  #right neighbor
                    if R < m-1: tmp += fine[val][row[-1]] #down neighbor
                ans.append(tmp)

            return max(ans)
        
        if m < n: m, n = n, m
            
        return dp(m*n-1, tuple([0]*n), I, E)

Problem 1681. Minimum Incompatibility

https://leetcode.com/problems/minimum-incompatibility/

We need to keep state in the form: bitmask, last, where:

  1. bitmask is bitmask for all numbers already taken.
  2. last is index of last taken number.

How are we going to form our groups: sort our numbers first and imagine we have numbers [1,1,2,3,3,4]: and k = 3. Then we need to create first group of 2 elements, for example it can be elements with indexes 1,2, and we have current bitmask 011000, then we need to form another group of 2 elements, for example it can be elements with indexes 0,5, so we have bitmask 111001 now. Finally we put last two elements in last group. We can interpret it as TSP problem: we choose path 1->2->0->5->3->4. Note, that in each group we only choose increasing indexes, whereas between groups than can decrease (but can increase also).

Now, what we have in our algorithm:

  1. We iterate over all possible mask
  2. Calculate places, where we have 1 in this mask.
  3. Now, we can have two cases: first, if len(n_z_bits)%(n//k) == 1: it means, that it is time to start new group, so we can choose any index we want, not neccecerily bigger than previous, so we choose permutations here: there will be exactly t(t-1)/2 pairs and for each of them we update dp[mask][l].
  4. In other case, it means, that we need to continue to build our group, so next index should be bigger than previous and we choose combinations here. Also we check that new number is not equal to last one we have and again update dp[mask][l].
  5. Finally, we return minimum from dp[-1].

Complexity: time complexity is O(n*n*2^n) as TSP have: we have 2^n bitmasks and we have O(n^2) time to process each mask. In python it is working not very fast, I can say it is barely accepted, so, I think there should be O(n*2^n) solution as well, if you know such solution (desirebly with explanations), please let me know!

class Solution:
    def minimumIncompatibility(self, nums, k):
        n = len(nums)
        if k == n: return 0
        dp = [[float("inf")] * n for _ in range(1<<n)] 
        nums.sort()
        for i in range(n): dp[1<<i][i] = 0

        for mask in range(1<<n):
            n_z_bits = [j for j in range(n) if mask&(1<<j)]
            if len(n_z_bits)%(n//k) == 1:
                for j, l in permutations(n_z_bits, 2):
                    dp[mask][l] = min(dp[mask][l], dp[mask^(1<<l)][j])
            else:
                for j, l in combinations(n_z_bits, 2):
                    if nums[j] != nums[l]:
                        dp[mask][j] = min(dp[mask][j], dp[mask^(1<<j)][l] + nums[l] - nums[j])
                        
        return min(dp[-1]) if min(dp[-1]) != float("inf") else -1

Problem 854 K-Similar Strings

https://leetcode.com/problems/k-similar-strings/

Note similarity of this problem with Problem Q765 Couples Holding Hands, but here we have more 6 different symbols. Let us look at this problem as to graph: for example if we have A = abcde and B = bcaed, then we need to find 2 loops: a -> b -> c -> a and d -> e -> d. In general we need to find minimum number of loops in directed graph on m = 6 nodes, where we can have multiple edges and we have n edges. However it is still difficult problem and it is not obvious how to handle it.

Let us assign number to each letter: a:1, b:32, c:32^2, d:32^3, e:32^4, f:32^5. Now, we evaluate difference between corresponding elements of two strings, so for example for given example we have [-31, -992, 1023, -1015808, 1015808]. Now, finding loop is equivalent to group of elements which has 0 when we sum them! Why? Because if some sum is equal to zero, it means, that numbers in both sets are equal: we choose 32 > 20, length of our string): indeed, imagine, that we have set of numbers with sum $0$, it means:

alpha_0 * 1 + alpha_1 * 32 + ... + alpha_5 * 32^5 = beta_0 * 1 + beta * 32 + ... + beta * 32^5,

where alpha_0, ... alpha_5 is corresponding number of a, b, c, d, e, f, similar for betas. From here we have

gamma_0 * 1 + gamma_1 * 32 + ... + gamma_5 * 32^5 = 0,

where gamma_i = alpha_i - beta_i lies in [-20, 20]. If we consider biggest gamma_i which is not equal to 0, then sum of all other terms is smaller than this term, so it means that all gamma_i equal to zero, that is we have the same set of letters.

Actually, this is almost equivalent to Problem Q698 Partition to K Equal Sum Subsets we already discussed. Time complexity is O(2^n * n) and space complexity is O(2^n).

To be accepted on Leetcode, we need to make couple of optimizations: first of all, if two symbols on the same place are equal, we just remove them. Also if we have loop of length 2 we also remove it: here I use trick, where we intersect count for nums and list with all opposite numbers from nums. Then for each element we repeat it as much times as needed and delete from our list.

class Solution:
    def kSimilarity(self, A, B):
        nums = [((1<<ord(i)*5) - (1<<ord(j)*5))>>(97*5) for i,j in zip(A,B) if i!=j] 

        Cnt = Counter(nums) & Counter(-n for n in nums)
        pairs = list(chain(*([i]*j for i, j in Cnt.items())))
        for num in pairs: nums.remove(num)
        
        n = len(nums)
        
        dp = [(-1,0)] * (1<<n)
        dp[0] = (0, 0)

        for mask in range(1<<n):
            for j in range(n):
                if mask & (1<<j):
                    steps, summ = dp[mask ^ (1<<j)]
                    dp[mask] = max(dp[mask], (steps + (summ==0), summ + nums[j]))

        return n - dp[-1][0] + len(pairs)//2

Problem 1799 Maximize Score After N Operations

https://leetcode.com/problems/maximize-score-after-n-operations/

Let us define by dp(bitmask) the answer for subproblem, where we can use only numbers with 1 bits, for example if nums = [1,2,3,4,5,6] and bitmask = 010111, we can use numbers 2, 4, 5, 6.

How we can calculate dp(bitmask)? We need to look at all pairs of not used yet numbers and take dp of the resulting mask. Then choose maximum among all possible candidates.

Complexity: time complexity is O(2^(2n)*n^2), because we have 2^(2n) different bitmasks and for each masks and for each of them we have O(n^2) transitions to other bitmasks. Also I precalculated all gcds, which will give you complexity O(n^2*log M), where M is the biggest number in nums. Space complexity is O(2^(2n)).

class Solution:
    def maxScore(self, nums):
        n = len(nums)
        gcds = {(j, k): gcd(nums[j], nums[k]) for j, k in combinations(range(n), 2)}

        @lru_cache(None)
        def dfs(bitmask):
            if bitmask == 0: return 0
            
            cand = 0
            n_z_bits = [j for j in range(n) if bitmask&(1<<j)]
            for j, k in combinations(n_z_bits, 2):
                q = bitmask^(1<<j)^(1<<k)
                cand = max(cand, dfs(q) + (n+2 - len(n_z_bits))//2*gcds[j, k])
            return cand

        return dfs((1<<n) - 1)

If you know more problems where you can use dp on subsets, please let me know in comments, I will add it (probably some of the premium problems are, but I do not have access to them, also I did not solve all problems on leetcode, so I probably missed some of the problems)

Problem 1879 Minimum XOR Sum of Two Arrays

https://leetcode.com/problems/minimum-xor-sum-of-two-arrays/

Here we briefly talk about solution. Let us go from left to right on array A and allocate elements to A[0], then to A[1] and so on. We have two pieces of information in our dp function:

  1. mask is binary mask of unused elements from B. For example if we have 01001, it means that we still can use element with index 0 and element with index 3.
  2. i is current index in A. Actually, it can be computed from bitmask: how many non-zero bits we have in it. But it is more simple to keep it as well.

Finally, what we do is just try to allocate already not used elements and run function recursilvey, that is all!

Why I spend 1 hour to find my mistake? It is all about bitwise operations: you NEED BRACKETS in (A[i]^B[j]), in the opposite case it will not work.

Complexity:
Time complexity is O(n*2^n), because we have 2^n states and O(n) transactions frome one state to others. Space complexity is O(2^n).

class Solution:
    def minimumXORSum(self, A, B):
        n = len(A)

        @lru_cache(None)
        def dp(mask, i):
            if i == n: return 0
            return min(dp(mask ^ (1<<j), i + 1) + (A[i]^B[j]) for j in range(n) if mask & 1<<j)

        return dp((1<<n) - 1, 0)

1349. Maximum Students Taking Exam

https://leetcode.com/problems/maximum-students-taking-exam/

Solution 1

First solution idea is to fill line by line and make sure that we do not have any collisions between line. What we can do is for every line (row) compute all possible patterns for this row first. Then we will use function dp(curr, i), where:

  1. curr is bitmask for the current row we are now.
  2. i is number of current row.

Then to evaluate dp(curr, i), we need to look at all rows from masks[i-1] and make sure that we do not have a collision: we already checked that we do not have any in each row, it is enough the check that we do not have between rows, we can do it, using if not (curr >> 1) & prev and not curr & (prev >> 1) condition: we move one of the rows to the left and check if we have ones at the same place.

Complexity

Rude estimate is O(2^n * 2^n * m), because we have 2^n * m different states and O(2^n) transitions from one state to others. Space complexity is O(2^n * m). In fact, there will be no more than O(phi^n) states for each row (where phi approximately 1.6 is golden ratio), so final time complexity can be estimated as O(phi^{2n} * m), space complexity is O(phi^n * m).

Code

class Solution:
    def maxStudents(self, seats):
        m, n = len(seats), len(seats[0])
        masks = []

        for row in seats:
            mask = sum((1 << j)*(v==".") for j, v in enumerate(row))
            masks.append([i for i in range(1<<n) if i & mask == i and not i & (i>>1)])
            
        @lru_cache(None)
        def dp(curr, i):
            if i == -1: return 0
            
            res = 0
            for prev in masks[i-1]:
                if not (curr >> 1) & prev and not curr & (prev >> 1):
                    res = max(res, dp(prev, i - 1))

            return res + bin(curr).count("1")

        return max(dfs(mask, m-1) for mask in masks[m-1])

Solution 2

There is smarter solution, using idea of dp with broken profile, see also similar problem 1659.

image

Here we need to keep broken row with total n+1 elements. Now, we can try to fill element in index place either with 0 or with 1. We always have option to put 0. If we want to put 1 we need to make sure, that we do not have conflict:

  1. We need to make sure, that if we have neighbour to the right, then value of this element is not equal to 1: first bit of row can be taken with row & (1<<n) formula.
  2. We need to make sure, that if we have neighbour to the right bottom, then its value is not equal to 1. To get this element we can use row & 1, which will give the last bit of row.
  3. We need to make sure, that if we have neighbour to the left bottom, then its value is not equal to 1. To get this element we need to take row & 4, that is the 3-rd element from the end of row.
  4. Finally, we need to make sure, that S[y][x] is not equal to #.

In the end we return dp(m*n-1, 0).

Complexity

We can estimate number of states as O(mn* 2^{n+1}, and we have 2 transactions from one state to another. Hence time and space complexity is O(mn* 2^n). It can be shown that in fact it is O(mn* phi^n).

Code

class Solution:
    def maxStudents(self, S):
        m, n = len(S), len(S[0])
        
        @lru_cache(None)
        def dp(index, row):
            if index == -1: return 0
            y, x = index//n, index%n
            c = (x < n-1 and row & 1<<n) or (x < n-1 and y < m-1 and row & 1)
            c = c or (x > 0 and y < m-1 and row & 4) or (S[y][x] == "#")
            cands = [dp(index-1, row//2)]
            if not c: cands.append(1 + dp(index-1, row//2 + (1<<n)))
            return max(cands)
 
        return dp(m*n-1, 0)

Remark

There is in fact O(m^2 n^2) time complexity algorithm for this problem, because our graph in fact bipartite: odd and even columns can be seen as two parts of our graph and the question is to find maximum independent set in this graph.

Problem 1125. Smallest Sufficient Team

https://leetcode.com/problems/smallest-sufficient-team/

Solution 1

Let dp(i, mask) is minimum number of people needed to cover bitmask mask if we already covered i persons. In fact we will keep two values: (minimum number of people, last taken).

  1. First we need to create masks: set of skils for each person
  2. If i == -1, and mask = 0, we return (0, -1), it means that wedo not use any person yet and -1 means that we reached the bottom. If i == -1 and mask != 0, we return (inf, -1), it means that we reached bottom but did not covered all skills.
  3. Finally, we have two choices: either take person i or do not take it. Here mask & ~masks[i] is used for subtraction of sets.
  4. In the end use backtracking to create solution.

Complexity

Time and space complexity is O(k*2^n). Space complexity can be reduced to O(2^n).

Code

class Solution:
    def smallestSufficientTeam(self, req_skills, people):
        d = {s:i for i, s in enumerate(req_skills)}
        masks = [sum(1<<d[skill] for skill in p) for p in people]
        n, k = len(req_skills), len(people)
        
        @lru_cache(None)
        def dp(i, mask):
            if i == -1: return (mask*1000, -1)
            cand1 = dp(i-1, mask)
            cand2 = (dp(i-1, mask & ~masks[i])[0] + 1, i)
            return min(cand1, cand2)

        mask, ans =  (1<<n) - 1, []
        while mask:
            steps, last = dp(k - 1, mask)
            ans.append(last)
            mask &= ~masks[last]

        return ans

Solution 2

Actually, we can write it in more shorter way if we keep not the last person used, but bitmask of used one (we have at most 60, so it will fit in long not only in python)

Complexity

It is the same as in previous approach.

Solution

class Solution:
    def smallestSufficientTeam(self, req_skills, people):
        d = {s:i for i, s in enumerate(req_skills)}
        masks = [sum(1<<d[s] for s in p) for p in people]
        n, k = len(req_skills), len(people)
        
        @lru_cache(None)
        def dp(i, mask):
            if i == -1: return (mask*1000, 0)
            steps, peop = dp(i-1, mask & ~masks[i])
            return min(dp(i-1, mask), (steps + 1, peop + (1<<i)))

        M = dp(k - 1, (1<<n) - 1)[1]
        return [i for i in range(k) if M & (1<<i) != 0]

If you have any question, feel free to ask. If you like the explanations, please Upvote!

dynamic programmingsubsetsexplainedbitmask
Comments: 14
joric's avatar
Read More

This one should be in the LeetCode’s pick!

29
Reply
Share
Report
Bigyikes's avatar
3
Show 3 replies
Reply
Share
Report
skandari's avatar
Read More

for example 00011101 mask means that we use nodes number 0, 2, 3, 4. can someone please explain what this means?

3
Show 2 replies
Reply
Share
Report
Bigyikes's avatar
Read More

Spasibo Dmitry.

2
Reply
Share
Report
mniverthi's avatar
Read More

Thank you this is super useful!

1
Reply
Share
Report
dtralara's avatar
Read More

Thank you (I doubt a key chain will be enough to properly reward this post).
I recently came across "1799. Maximize Score After N Operations" which can be solved this way too. I didn't know it was named "DP on subsets".

1
Show 1 reply
Reply
Share
Report
tshpak's avatar
Read More

Thanks a lot!
It is a very useful bunch of problems discussed. It is much clearer now.

1
Reply
Share
Report
Umbra_'s avatar
Read More

Awesome, thanks!

1
Reply
Share
Report
smpopa01's avatar
Read More

hi, one question, what is the meaning of neib variable in the first 2 problems ?

1
Show 1 reply
Reply
Share
Report
rowe1227's avatar
Read More

Nice compilation, thanks for sharing!

Wednesday, May 18, 2022

PSFE stock: Seekingalpha.com article - Felix Fung

 

Summary

  • The payment company is down over 85% from its high last January after going public through a SPAC merger.
  • The company is already profitable but is struggling to grow its revenue despite online payment tailwinds.
  • Its latest earnings report shows that revenue is still decelerating and is facing multiple headwinds on the FX and regulatory end.
  • The company is a sell as valuations are still elevated compared to peers while growth is almost non-existent.

Investment Thesis

Paysafe (NYSE:PSFE) is a UK-based fintech company that provides payment services to customers that range from individuals to enterprises. It mainly operates in three segments, Integrated processing, Digital wallet, and Ecash solutions. The company went public early last year through a SPAC merger with Foley Trasimene Acquisition Corp, a blank check company founded by famous investor Bill Foley. The stock shot up to almost $20 initially due to the hype surrounding it then kept plummeting after de-SPAC. The stock is now trading at $2.48, down almost 90% from its all-time high last year. Despite the huge drop in price, I believe the stock is still a sell. The company recently reported its first-quarter result for FY22. It is EBITDA positive but is struggling to grow revenue while peers are still reporting growth. It is facing strong competition, regulatory and FX headwinds and the i-gaming segment appears to be overrated. Valuation is also high when compared to its peers which have better fundamentals and financials.


Valuation

Despite the huge drop in stock price, the company is still by no means cheap. At the current price of $2.48, it translates to an EV/EBITDA of 22.01. The number looks quite reasonable alone but when you factor in its growth and prospects it is actually quite expensive. From the chart below, it is also shown that the company's valuation is elevated compared to other payment companies like PayPal (PYPL) and Global Payments (GPN). All of the peers below are also growing quicker, PayPal, Global Payments, and Nuvei (NVEI) grew revenue by 7.46%, 8.35%, and 42.75% respectively despite the weakening macro environment. Not to mention that all of these companies have a much lower debt load. Paysafe's Net debt to LTM Adj. EBITDA is 5.6x while PayPal, Global Payments are only 2.9x, 0.4x and Nuvei has no net debt. All of these companies are a lot more attractive compared to Paysafe when considering growth, balance sheet, valuation, and fundamentals.

PSFE stock: My quick study on May 18, 2022

 

Paysafe Limited (NYSE:PSFE) Receives Average Recommendation of “Buy” from Brokerages


Posted by  on May 17th, 2022

Shares of Paysafe Limited (NYSE:PSFE – Get Rating) have been given a consensus rating of “Buy” by the eleven research firms that are presently covering the stock, MarketBeat reports. One analyst has rated the stock with a sell rating, three have given a hold rating and seven have given a buy rating to the company. The average 1 year price target among brokerages that have issued a report on the stock in the last year is $8.20.

A number of research analysts have issued reports on PSFE shares. Bank of America cut shares of Paysafe from a “buy” rating to an “underperform” rating and decreased their price target for the stock from $6.00 to $4.00 in a report on Wednesday, March 2nd. Susquehanna decreased their price target on shares of Paysafe from $13.00 to $9.00 and set a “positive” rating for the company in a report on Thursday, March 3rd. Zacks Investment Research raised shares of Paysafe from a “sell” rating to a “hold” rating in a report on Saturday. Credit Suisse Group decreased their price target on shares of Paysafe from $4.00 to $3.50 and set a “neutral” rating for the company in a report on Thursday, March 3rd. Finally, Cowen raised shares of Paysafe from a “market perform” rating to an “outperform” rating and set a $7.00 target price for the company in a report on Thursday, January 20th.

A number of hedge funds have recently added to or reduced their stakes in PSFE. State Street Corp grew its position in Paysafe by 112.4% during the fourth quarter. State Street Corp now owns 6,883,131 shares of the company’s stock valued at $26,913,000 after buying an additional 3,641,853 shares during the period. BlackRock Inc. grew its position in Paysafe by 43.1% during the third quarter. BlackRock Inc. now owns 11,956,024 shares of the company’s stock valued at $92,660,000 after buying an additional 3,600,577 shares during the period. Indaba Capital Management L.P. acquired a new stake in Paysafe during the third quarter valued at $26,350,000. Renaissance Technologies LLC grew its position in Paysafe by 646.9% during the first quarter. Renaissance Technologies LLC now owns 2,448,400 shares of the company’s stock valued at $8,300,000 after buying an additional 2,120,600 shares during the period. Finally, Parametric Portfolio Associates LLC grew its position in Paysafe by 3,475.4% during the fourth quarter. Parametric Portfolio Associates LLC now owns 1,950,126 shares of the company’s stock valued at $7,625,000 after buying an additional 1,895,583 shares during the period. Institutional investors own 65.63% of the company’s stock.

NYSE PSFE opened at $2.27 on Tuesday. The company has a fifty day simple moving average of $3.09 and a two-hundred day simple moving average of $3.69. Paysafe has a 12-month low of $1.84 and a 12-month high of $13.35. The company has a quick ratio of 1.41, a current ratio of 1.41 and a debt-to-equity ratio of 1.01.

Paysafe Company Profile (Get Rating)

Paysafe Limited provides digital commerce solutions to online businesses, small and medium-sized business merchants, and consumers through its Paysafe Network worldwide. The company operates in two segments, US Acquiring and Digital Commerce. It provides PCI-compliant payment acceptance and transaction processing solutions for merchants and integrated service providers, including merchant acquiring, transaction processing, online solutions, fraud and risk management tools, data and analytics, and point of sale systems and merchant financing solutions under the Paysafe and Petroleum Card Services brands.


Three lessons in my incomplete 2019 mortgage application in RBC bank

May 18, 2022

Introduction

I just checked the cheapest condo in Chilliwack went up from $150,000 Canadian dollars to $270,000 dollars. I tried to get mortgage in 2019, and visited RBC branch in 2019. But somehow I did not complete the application. 

2019 January | RBC specialist 


Three lessons learned 

I should understand that it is important to get a loan and have a small condo in Chilliwack. If inflation is really a big problem, then the condo price should go up over 80%. 

At that time, I did not have a lot of savings in my checking account. I think that I only have $35,000 dollars. I did not invest on stock market at all from 2010 to 2019. I did not invest on anything. 

Three lessons:

  1. Do not shy away from condo market. Specially smallest and cheapest one can bring me a best tool to hedge against inflation; 
  2. I should learn how to get mortgage quickly, and it is important to purchase early. I was 53 year old in 2019. I should get mortgage early when I have a full time job. 
  3. RBC specialist told me that it is still possible for me to get a loan even though I cannot drive to home from Chilliwack everyday. 
  4. The reason I chose to visit RBC is that RBC can loan money using USA home as an asset to borrow from. 

$100,000 Canadian dollar opportunity loss | 2019 -> 2022, price went up over $100,000 dollars

 Other Condos For Sale in 45749 Spadina Avenue, Chilliwack | 2019 $150,000 -> 2022 $274,900, $120,000 dollars gain



Other Condos For Sale in 45749 Spadina Avenue, Chilliwack | 2019 $150,000 -> 2022 $274,900, $120,000 dollars gain

 

Other Condos For Sale in 45749 Spadina Avenue, Chilliwack

 
DateAddressBedBathAsking PriceSqft$/SqftDOMBrokerage
04/26/2022104 45749 Spadina Ave11$274,900525$52422Royal Lepage Wheeler Cheam
03/18/2022203 45749 Spadina Ave11$269,900552$48961Royal Lepage Wheeler Cheam
   Avg:$272,400539$5060

Other Condos For Sale in 45744 Spadina Avenue, Chilliwack

 

DateAddressBedBathAsking PriceSqft$/SqftDOMBrokerage
05/16/2022202 45744 Spadina Ave11$251,500581$4332Sutton Group-West Coast Realty (Abbotsford)
   Avg:$251,500581$4330 

1632. Rank Transform of a Matrix

 I like to work on this hard level algorithm. 

Here is the link. 

C# | Quick learner | Union find algorithm | Illustration using diagrams

May 19, 2022
What is the best way to learn this hard algorithm? The algorithm can be solved using Union find algorithm, and then it can be illustrated using a case study by the following diagram:

image

Highlights of my quick learning process:

  1. Warmup union find algorithm, disjoint set, ranking, and parent with path compression - Union find algorithm
  2. Put every row's items in one disjoint set, and every column's items in one disjoint set;
  3. Preprocess a hashmap, so that each root node in disjoint sets can have a list of children nodes
  4. Sort all items in matrix by order of value, row number, column number in ascending order.
  5. Use extra space to store two integer arrays, rowRanks, colRanks to store latest rank with maximum value.
  6. Review the above diagram and study the test case:
  7. Smallest value is 47, row = 2, col = 1, set it's rank as 1, and mark rowRanks, colRanks accordingly in second step;
  8. Next smallest value is -21, row = 0, col = 1, set it's rank as 2, and mark rowRanks, colRanks accordingly in third step.

Nothing can compare to my own practice | Learn from a failed test case
I did read votrubac analysis but I could not fully understand the idea to solve the problem. I did read a few other discuss post. So I chose to write a C# solution by studying one of C# discuss posts.

I rewrote C# code using more meaningful variable names. I use Tuple<int,int,int> and then I forgot to add a statement in the following:

data.Sort();  // caught by Leetcode online judge

So, I came cross this failed test case, and then I started to debug code using the test case:
3 / 40 test cases passed.
Input:
[[20,-21,14],[-19,4,19],[22,-47,24],[-19,4,19]]
Output:
[[1,2,3],[2,3,4],[3,4,5],[2,3,4]]
Expected:
[[4,2,3],[1,3,4],[5,1,6],[1,3,4]]

So it is important to sort all numbers in the matrix first, and then work on smallest value -47.

The following C# code passes online judge.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace _1632_rank_transformation
{
    class Program
    {
        static void Main(string[] args)
        {
            var matrix = new int[3][];
            matrix[0] = new int[] { 20, -21, 14};
            matrix[1] = new int[] { -19, 4, 19};
            matrix[2] = new int[] { 22, -47, 24};
            matrix[3] = new int[] { -19, 4, 19};

            var result = MatrixRankTransform(matrix);
        }

        public class Unions
        {
            private readonly int[] parents;
            private readonly int[] ranks;

            public Unions(int n)
            {
                parents = new int[n];
                ranks = new int[n];
                for (int i = 0; i < n; i++)
                {
                    parents[i] = i;
                }
            }

            public int Find(int x)
            {
                if (x != parents[x])
                {
                    x = Find(parents[x]);
                }

                return parents[x];
            }

            public bool Union(int x, int y)
            {
                int px = Find(x);
                int py = Find(y);

                if (px == py)
                {
                    return false;
                }

                if (ranks[px] > ranks[py])
                {
                    parents[py] = px;
                    ranks[px]++;
                }
                else
                {
                    parents[px] = py;
                    ranks[py]++;
                }

                return true;
            }
        }

        /// <summary>
        /// study code:
        /// https://leetcode.com/problems/rank-transform-of-a-matrix/discuss/909152/Accepted-C-Solution
        /// </summary>
        /// <param name="matrix"></param>
        /// <returns></returns>
        public static int[][] MatrixRankTransform(int[][] matrix)
        {
            var rows = matrix.Length;
            var columns = matrix[0].Length;

            var result = new int[rows][];
            for (int i = 0; i < rows; i++)
            {
                result[i] = new int[columns];
            }

            var disjointSet = new Unions(rows * columns);
            for (int row = 0; row < rows; row++)
            {
                var map = new Dictionary<int, IList<int>>();

                for (int column = 0; column < columns; column++)
                {
                    var current = matrix[row][column];
                    if (!map.ContainsKey(current))
                    {
                        map[current] = new List<int>();
                    }

                    map[current].Add(column);
                }

                foreach (var pair in map)
                {
                    for (int k = 1; k < pair.Value.Count; k++)
                    {
                        disjointSet.Union(row * columns + pair.Value[k], row * columns + pair.Value[k - 1]);
                    }
                }
            }

            // every column - put row's item in a disjoint set. 
            for (int column = 0; column < columns; column++)
            {
                var map = new Dictionary<int, IList<int>>();

                for (int row = 0; row < rows; row++)
                {
                    var val = matrix[row][column];

                    if (!map.ContainsKey(val))
                    {
                        map[val] = new List<int>();
                    }

                    map[val].Add(row);
                }

                foreach (var pair in map)
                {
                    for (int k = 1; k < pair.Value.Count; k++)
                    {
                        disjointSet.Union( pair.Value[k] * columns + column, pair.Value[k - 1] * columns + column);
                    }
                }
            }

            var root2Indices = new Dictionary<int, IList<int>>();
            for (int row = 0; row < rows; row++)
            {
                for (int column = 0; column < columns; column++)
                {
                    var link = row * columns + column;
                    var root = disjointSet.Find(link);

                    if (!root2Indices.ContainsKey(root))
                    {
                        root2Indices[root] = new List<int>();
                    }

                    root2Indices[root].Add(link);
                }
            }

            var data = new List<Tuple<int, int, int>>(rows * columns);

            for (int row = 0; row < rows; row++)
            {
                for (int column = 0; column < columns; column++)
                {
                    data.Add(new Tuple<int, int, int>(matrix[row][column], row, column));
                }
            }

            data.Sort();  // caught by Leetcode online judge

            var rowRanks = new int[rows];
            var colRanks = new int[columns];

            int idx = 0;

            while (idx != rows * columns)
            {
                var tuple = data[idx];
                var row = tuple.Item2;
                var col = tuple.Item3;

                var link = row * columns + col;

                if (result[row][col] != 0)
                {
                    idx++;
                    continue;
                }

                int maxRank = 0;
                var root = disjointSet.Find(link);

                foreach (var index in root2Indices[root])
                {
                    var r = index / columns;
                    var c = index % columns;

                    maxRank = Math.Max(maxRank, Math.Max(rowRanks[r], colRanks[c]));
                }

                var thisRank = maxRank + 1;
                foreach (var index in root2Indices[root])
                {
                    var r = index / columns;
                    var c = index % columns;

                    result[r][c] = thisRank;

                    rowRanks[r] = thisRank;
                    colRanks[c] = thisRank;
                }

                idx++;
            }

            return result;
        }
    }
}

BEA systems, inc

 BEA Systems, Inc. was a company that specialized in enterprise infrastructure software products which was wholly acquired by Oracle Corporation on April 29, 2008.[1]

History[edit]

BEA began as a software company, founded in 1995 and headquartered in San Jose, California. It grew to have 78 offices worldwide at the time of its acquisition by Oracle.

The company's name is an initialism of the first names of the company's three founders: Bill Coleman, Ed Scott, and Alfred Chuang. All were former employees of Sun Microsystems, and launched the business in 1995 by acquiring Information Management and Independence Technologies. These firms were the largest resellers of Tuxedo, a distributed transaction management system sold by Novell. BEA soon acquired the Tuxedo product itself,[2][3] and went on to acquire other middleware companies and products.

In 1998, BEA acquired the San Francisco start-up WebLogic, which had built the first standards-based Java application server. WebLogic's application server became the impetus for the Sun Microsystems' J2EE specification and formed the basis of BEA's WebLogic application server sold today.[citation n


They were a sponsor for Team Rahal (now Rahal Letterman Lanigan Racing) from 2002 to 2008, which included Buddy Rice's 2004 Indianapolis 500 win and Vitor Meira's 2005 Indianapolis 500 runner-up finish.

In 2005, BEA launched a new brand identity with the slogan "Think Liquid". This was considered cutting Edge marketing.[4] BEA also announced a new product line called AquaLogic, which is an infrastructure software family for service-oriented architecture (SOA). The same year, it made its entrance into telecommunications infrastructure through the acquisition of Incomit, a Swedish telecommunications software provider.[5] In late 2005, the company announced the acquisitions of Compoze Software, a provider of collaboration software,[6] M7, an Eclipse-based tools company,[7] and SolarMetric, editors of the Kodo persistence engine.[8]

The acquisitions continued in 2006 with Plumtree Software, an enterprise portal company; Fuego, a business process management (BPM) software company; and Flashline, a metadata repository company. These acquisitions have since become parts of the AquaLogic SOA product stack.

On October 12, 2007, Oracle announced their intent to buy BEA Systems for $6.7 billion.[9] As a result of the offer, BEA's stock price rose over five dollars upon the opening of trading for the day.[10] BEA turned the offer down the same day, saying that the company is "worth substantially more".[11] On January 16, 2008, Oracle signed a definite agreement to buy BEA for $8.5 billion. It is believed that Carl Icahn, one of the company's most prominent shareholders, was the main reason that the deal happened.[12]

On April 29, 2008, Oracle completed its acquisition of BEA.[13]