mirror of
https://github.com/dholerobin/Lecture_Notes.git
synced 2025-03-16 06:10:00 +00:00
Fix: Updated the merge conflicts
This commit is contained in:
parent
4939458348
commit
37c9596d36
235
Recursion and Backtracking/1.md
Normal file
235
Recursion and Backtracking/1.md
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
|
||||||
|
Recursion
|
||||||
|
----------
|
||||||
|
Recursion - process of function calling itself
|
||||||
|
directly or indirectly.
|
||||||
|
|
||||||
|
__Steps involved:__
|
||||||
|
- Base case
|
||||||
|
- Self Work
|
||||||
|
- Recursive Calls
|
||||||
|
|
||||||
|
```python
|
||||||
|
def fib(n):
|
||||||
|
if n <= 1 : return n # base case
|
||||||
|
return fib(n-1) + fib(n-2) # recursive calls
|
||||||
|
|
||||||
|
fib(10) # initial call
|
||||||
|
```
|
||||||
|
In the recursive program, the solution to the base case is provided and the solution of the bigger problem is expressed in terms of smaller problems.
|
||||||
|
In the above example, base case for n < = 1 is defined and larger value of number can be solved by converting to smaller one till base case is reached.
|
||||||
|
|
||||||
|
__Intuition__
|
||||||
|
|
||||||
|
The main idea is to represent a problem in terms of one or more smaller problems, and add one or more base conditions that stop the recursion.
|
||||||
|
_For example_, we compute factorial n if we know factorial of (n-1). The base case for factorial would be n = 0. We return 1 when n = 0.
|
||||||
|
|
||||||
|
|
||||||
|
Power
|
||||||
|
-----
|
||||||
|
|
||||||
|
> Given n, k. Find $n^k$
|
||||||
|
|
||||||
|
```python
|
||||||
|
def pow(n, k):
|
||||||
|
if k == 0: return 1
|
||||||
|
return n*pow(n, k - 1)
|
||||||
|
```
|
||||||
|
__Time Complexity__: O(n)
|
||||||
|
|
||||||
|
__Optimised solution:__
|
||||||
|
```python
|
||||||
|
def pow(n, k):
|
||||||
|
if k == 0: return 1
|
||||||
|
|
||||||
|
nk = pow(n, k//2)
|
||||||
|
if k % 2 == 0:
|
||||||
|
return nk * nk
|
||||||
|
else:
|
||||||
|
return nk * nk * n
|
||||||
|
```
|
||||||
|
|
||||||
|
Why not f(n, k/2) * f(n, k/2+1) in the else condition?
|
||||||
|
To allow reuse of answers.
|
||||||
|
|
||||||
|
<img src="https://user-images.githubusercontent.com/35702912/66316190-d30e1f00-e934-11e9-8089-85c6dc69baa7.jpg" data-canonical-src="https://user-images.githubusercontent.com/35702912/66316190-d30e1f00-e934-11e9-8089-85c6dc69baa7.jpg" width="500" />
|
||||||
|
|
||||||
|
__Time Complexity__ (assuming all multiplications are O(1))? O(\log_2 k)$O(\log_{2}k)$
|
||||||
|
|
||||||
|
|
||||||
|
Break it into 3 parts? k//3 and take care of mod1 and mod2.
|
||||||
|
|
||||||
|
Binary is still better, just like in binary search.
|
||||||
|
|
||||||
|
-- --
|
||||||
|
All Subsets
|
||||||
|
-----------
|
||||||
|
|
||||||
|
> Given A[N], print all subsets
|
||||||
|
|
||||||
|
The idea is to consider two cases for every element.
|
||||||
|
(i) Consider current element as part of current subset.
|
||||||
|
(ii) Do not consider current element as part of current subset.
|
||||||
|
|
||||||
|
Number of subsets? $2^{n}$
|
||||||
|
|
||||||
|
Explain that we want combinations, and not permutations. [1, 4] = [4, 1]
|
||||||
|
|
||||||
|
Number of permutations will be much larger than combinations.
|
||||||
|
|
||||||
|
```python
|
||||||
|
def subsets(A, i, aux):
|
||||||
|
if i == len(A):
|
||||||
|
print(aux)
|
||||||
|
return
|
||||||
|
take = subsets(A, i+1, aux + [A[i]]) # Case 1
|
||||||
|
no_take = subsets(A, i+1, aux) # Case 2
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
<img src="https://user-images.githubusercontent.com/35702912/66323471-7a914e80-e941-11e9-84a9-11a333ac4f77.jpg" width="500"
|
||||||
|
/>
|
||||||
|
|
||||||
|
How many leaf nodes? $2^n$ - one for each subset
|
||||||
|
How many total nodes? $2^{n+1} - 1$
|
||||||
|
|
||||||
|
__Time Complexity__: $O(2^n)$
|
||||||
|
|
||||||
|
Subsets using Iteration
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
Look at recursion Tree.
|
||||||
|
Going left = 0
|
||||||
|
Going right = 1
|
||||||
|
|
||||||
|
Basically, for each element, choose = 1, skip = 0
|
||||||
|
|
||||||
|
So, generate numbers from 0 to $2^n-1$ and look at the bits of the numbers. Each subset is formed using each number.
|
||||||
|
```
|
||||||
|
For A = [1 2 3]
|
||||||
|
|
||||||
|
000 []
|
||||||
|
001 [c]
|
||||||
|
010 [b]
|
||||||
|
011 [b c]
|
||||||
|
100 [a]
|
||||||
|
101 [a c]
|
||||||
|
110 [a b]
|
||||||
|
111 [a b c]
|
||||||
|
```
|
||||||
|
|
||||||
|
Lexicographic subsets
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
Explain what is lexicographic order.
|
||||||
|
|
||||||
|
```
|
||||||
|
For Array [0,1,2,3,4] Subsets in Lexicographical order,
|
||||||
|
[]
|
||||||
|
[0]
|
||||||
|
[0, 1]
|
||||||
|
[0, 1, 2]
|
||||||
|
[0, 1, 2, 3]
|
||||||
|
[0, 1, 3]
|
||||||
|
[0, 2]
|
||||||
|
[0, 2, 3]
|
||||||
|
[0, 3]
|
||||||
|
[1]
|
||||||
|
[1, 2]
|
||||||
|
[1, 2, 3]
|
||||||
|
[1, 3]
|
||||||
|
[2]
|
||||||
|
[2, 3]
|
||||||
|
[3]
|
||||||
|
```
|
||||||
|
- The idea is to sort the array first.
|
||||||
|
- After sorting, one by one fix characters and recursively generates all subsets starting from them.
|
||||||
|
- After every recursive call, we remove current character so that next permutation can be generated.
|
||||||
|
- Basically, we're doing DFS. Print when encountering node
|
||||||
|
But don't print when going left - because already printed in parent.
|
||||||
|
|
||||||
|
|
||||||
|
<img src="https://user-images.githubusercontent.com/35702912/66468106-3d44d200-eaa3-11e9-96e7-c6a050be1219.jpg" width="500"
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
<img src="https://user-images.githubusercontent.com/35702912/66468119-42098600-eaa3-11e9-8f24-237be2a91d12.jpg" width="500"
|
||||||
|
/>
|
||||||
|
```python
|
||||||
|
def subsets(A, i, aux, p):
|
||||||
|
if p: print(aux)
|
||||||
|
if i == len(A):
|
||||||
|
return
|
||||||
|
take = subsets(A, i+1, aux + [A[i]], True)
|
||||||
|
no_take = subsets(A, i+1, aux, False)
|
||||||
|
```
|
||||||
|
|
||||||
|
__Time Complexity__: $O(2^n)$ <br>
|
||||||
|
__Space Complexity__: $O(n^2)$, because we're creating new aux arrays.
|
||||||
|
|
||||||
|
-- --
|
||||||
|
Number of Subsets with a given Sum
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
> Given an array of integers and a sum, the task is to print all subsets of given array with sum equal to given sum.
|
||||||
|
> A = [2, 3, 5, 6, 8, 10] Sum = 10
|
||||||
|
> Output: [5, 2, 3] [2, 8] [10]
|
||||||
|
|
||||||
|
The subsetSum problem can be divided into two subproblems.
|
||||||
|
- Include the current element in the sum and recur (i = i + 1) for the rest of the array
|
||||||
|
- Exclude the current element from the sum and recur (i = i + 1) for the rest of the array.
|
||||||
|
|
||||||
|
|
||||||
|
<img src="https://user-images.githubusercontent.com/35702912/66469192-11c2e700-eaa5-11e9-9094-252ce842464a.jpg" width="500"
|
||||||
|
/>
|
||||||
|
```python
|
||||||
|
def subsetSum(A,N,cur_sum, i, target):
|
||||||
|
if i == N:
|
||||||
|
if cur_sum == target:
|
||||||
|
return 1
|
||||||
|
else :
|
||||||
|
return 0
|
||||||
|
take = subsetSum(A,N,cur_sum + A[i], i+1, target)
|
||||||
|
no_take = subsetSum(A,N,cur_sum, i+1, target)
|
||||||
|
return take + no_take
|
||||||
|
```
|
||||||
|
|
||||||
|
Why can't terminate earlier when cur_sum == target?
|
||||||
|
Because we can have negative values in the array as well and this condition will prevent us from considering negative values.
|
||||||
|
For eg,
|
||||||
|
target = 6
|
||||||
|
1, 2, 3 is good, but
|
||||||
|
1, 2, 3, -1, 1 is also good.
|
||||||
|
__Time Complexity__: $O(2^n)$
|
||||||
|
__Space Complexity__: $O(n)$
|
||||||
|
|
||||||
|
Number of Subsets with a given Sum (Repetition Allowed)
|
||||||
|
---------------
|
||||||
|
> Given a set of m distinct positive integers and a value ‘N’. The problem is to count the total number of ways we can form ‘N’ by doing sum of the array elements. Repetitions and different arrangements are allowed.
|
||||||
|
> All array elements are positive.
|
||||||
|
|
||||||
|
The subsetSum2 problem can be divided into two subproblems.
|
||||||
|
- Include the current element in the sum and recur for the rest of the array. Here the value of i is not incremented to incorporate the condition of including multiple occurances of a element.
|
||||||
|
- Exclude the current element from the sum and recur (i = i + 1) for the rest of the array.
|
||||||
|
|
||||||
|
<img src="https://user-images.githubusercontent.com/35702912/66470200-c27db600-eaa6-11e9-8744-ca572d6000e1.jpg" width="500"
|
||||||
|
/>
|
||||||
|
|
||||||
|
```python
|
||||||
|
def subsetSum2(A,N,cur_sum, i, target):
|
||||||
|
if i == N:
|
||||||
|
if cur_sum == target:
|
||||||
|
return 1
|
||||||
|
else :
|
||||||
|
return 0
|
||||||
|
elif cur_sum > target:
|
||||||
|
return 0;
|
||||||
|
take = subsetSum2(A,N,cur_sum + A[i], i, target)
|
||||||
|
no_take = subsetSum2(A,N,cur_sum, i+1, target)
|
||||||
|
return take + no_take
|
||||||
|
```
|
||||||
|
__Time Complexity__ : $O(2 ** (Target/MinElement))$ <br>
|
||||||
|
__Space Complexity__: $O(Target/Min Element)$
|
||||||
|
|
||||||
|
|
||||||
|
|
164
Recursion and Backtracking/2.md
Normal file
164
Recursion and Backtracking/2.md
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
Backtracking
|
||||||
|
------------
|
||||||
|
|
||||||
|
Backtracking is a methodical way of trying out various sequences of decisions, until you find one that “works”. It is a systematic way to go through all the possible configurations of a search space.
|
||||||
|
- do
|
||||||
|
- recurse
|
||||||
|
- undo
|
||||||
|
|
||||||
|
Backtracking is easily implemented with recursion because:
|
||||||
|
- The run-time stack takes care of keeping track of the choices that got us to a given point.
|
||||||
|
- Upon failure we can get to the previous choice simply by returning a failure code from the recursive call.
|
||||||
|
|
||||||
|
Backtracking can help reduce the space complexity, because we're reusing the same storage.
|
||||||
|
|
||||||
|
__Backtracking Algorithm__:
|
||||||
|
Backtracking is really quite simple--we “explore” each node, as follows:
|
||||||
|
```python
|
||||||
|
To “explore” node N:
|
||||||
|
1. If N is a goal node, return “success”
|
||||||
|
2. If N is a leaf node, return “failure”
|
||||||
|
3. For each child C of N,
|
||||||
|
3.1. Explore C
|
||||||
|
3.1.1. If C was successful, return “success”
|
||||||
|
4. Return “failure”
|
||||||
|
```
|
||||||
|
Print all Permutations of a String
|
||||||
|
-------------
|
||||||
|
> A permutation, also called an “arrangement number” or “order,” is a rearrangement of the elements of an ordered string S into a one-to-one correspondence with S itself. <br>
|
||||||
|
String: ABC <br>
|
||||||
|
Permutations: ABC ACB BAC BCA CBA CAB
|
||||||
|
|
||||||
|
Total permutations = n!
|
||||||
|
|
||||||
|
<img src="https://user-images.githubusercontent.com/35702912/66570095-7e63e180-eb8a-11e9-8e3c-31d8e04f2d67.jpg" width="500"
|
||||||
|
/>
|
||||||
|
<img src="https://user-images.githubusercontent.com/35702912/66570104-83c12c00-eb8a-11e9-802d-f0f0ede4a14a.jpg" width="500"
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
```python
|
||||||
|
def permute(S, i):
|
||||||
|
if i == len(S):
|
||||||
|
print(S)
|
||||||
|
for j in range(i, len(S)):
|
||||||
|
S[i], S[j] = S[j], S[i]
|
||||||
|
permute(S, i+1)
|
||||||
|
S[i], S[j] = S[j], S[i] # backtrack
|
||||||
|
```
|
||||||
|
__Time Complexity:__ _O(n*n!)_ because there are n! permutations and it requires _O(n)_ to print a permutation.
|
||||||
|
<br>
|
||||||
|
__Space Complexity:__ _O(n)_
|
||||||
|
|
||||||
|
_Note: Output not in Lexicographic Order._
|
||||||
|
|
||||||
|
Print all Unique Permutations of a String
|
||||||
|
--------------------
|
||||||
|
> String: AAB
|
||||||
|
> Permutations: AAB ABA BAA
|
||||||
|
|
||||||
|
Basically, if we're swapping S[i] with S[j], but S[j] already occured earlier from S[i] .. S[j-1], then swapping will result in repetition.
|
||||||
|
|
||||||
|
|
||||||
|
<img src="https://user-images.githubusercontent.com/35702912/66570690-bc153a00-eb8b-11e9-8a00-9dfb728df5f9.jpg" width="500"
|
||||||
|
/>
|
||||||
|
|
||||||
|
```python
|
||||||
|
def permute_distinct(S, i):
|
||||||
|
if i == len(S):
|
||||||
|
print(S)
|
||||||
|
for j in range(i, len(S)):
|
||||||
|
if S[j] in S[i:j]:
|
||||||
|
continue
|
||||||
|
S[i], S[j] = S[j], S[i]
|
||||||
|
permute_distinct(S, i+1)
|
||||||
|
S[i], S[j] = S[j], S[i] # backtrack
|
||||||
|
```
|
||||||
|
__Time Complexity:__ _O(n*n!)_ <br>
|
||||||
|
__Space Complexity:__ _O(n)_
|
||||||
|
|
||||||
|
Print Permutations Lexicographically
|
||||||
|
---
|
||||||
|
> Given a string, print all permutations of it in sorted order. <br>
|
||||||
|
For example, if the input string is “ABC”, then output should be “ABC, ACB, BAC, BCA, CAB, CBA”.
|
||||||
|
|
||||||
|
- Right shift the elements before making the recursive call.
|
||||||
|
- Left shift the elements while backtracking.
|
||||||
|
|
||||||
|
```python
|
||||||
|
def permute(S, i):
|
||||||
|
if i == len(S):
|
||||||
|
print(S)
|
||||||
|
for j in range(i, len(S)):
|
||||||
|
S[i], S[j] = S[j], S[i]
|
||||||
|
permute(S, i+1)
|
||||||
|
S[i], S[j] = S[j], S[i] # backtrack
|
||||||
|
```
|
||||||
|
|
||||||
|
__Time Complexity:__ _O(n* n*n!)_ <br>
|
||||||
|
__Space Complexity:__ _O(n)_
|
||||||
|
|
||||||
|
Kth Permutation Sequence (Optional)
|
||||||
|
----
|
||||||
|
> Given a string of length n containing lowercase alphabets only. You have to find the k-th permutation of string lexicographically.
|
||||||
|
|
||||||
|
|
||||||
|
$\dfrac{k}{(n-1)!}$ will give us the index of the first digit. Remove that digit, and continue.
|
||||||
|
|
||||||
|
```python
|
||||||
|
def get_perm(A, k):
|
||||||
|
perm = []
|
||||||
|
while A:
|
||||||
|
# get the index of current digit
|
||||||
|
div = factorial(len(A)-1)
|
||||||
|
i, k = divmod(k, div)
|
||||||
|
perm.append(A[i])
|
||||||
|
# remove handled number
|
||||||
|
del A[index]
|
||||||
|
return perm
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Sorted Permutation Rank (Optional)
|
||||||
|
--
|
||||||
|
> Given S, find the rank of the string amongst its permutations sorted lexicographically.
|
||||||
|
Assume that no characters are repeated.
|
||||||
|
|
||||||
|
```python
|
||||||
|
Input : 'acb'
|
||||||
|
Output : 2
|
||||||
|
The order permutations with letters 'a', 'c', and 'b' :
|
||||||
|
abc
|
||||||
|
acb
|
||||||
|
bac
|
||||||
|
bca
|
||||||
|
cab
|
||||||
|
cba
|
||||||
|
```
|
||||||
|
**Hint:**
|
||||||
|
If the first character is X, all permutations which had the first character less than X would come before this permutation when sorted lexicographically.
|
||||||
|
|
||||||
|
Number of permutation with a character C as the first character = number of permutation possible with remaining $N-1$ character = $(N-1)!$
|
||||||
|
|
||||||
|
**Approach:**
|
||||||
|
rank = number of characters less than first character * (N-1)! + rank of permutation of string with the first character removed
|
||||||
|
```
|
||||||
|
Lets say out string is “VIEW”.
|
||||||
|
Character 1 : 'V'
|
||||||
|
All permutations which start with 'I', 'E' would come before 'VIEW'.
|
||||||
|
Number of such permutations = 3! * 2 = 12
|
||||||
|
Lets now remove ‘V’ and look at the rank of the permutation ‘IEW’.
|
||||||
|
|
||||||
|
Character 2 : ‘I’
|
||||||
|
All permutations which start with ‘E’ will come before ‘IEW’
|
||||||
|
Number of such permutation = 2! = 2.
|
||||||
|
Now, we will limit ourself to the rank of ‘EW’.
|
||||||
|
|
||||||
|
Character 3:
|
||||||
|
‘EW’ is the first permutation when the 2 permutations are arranged.
|
||||||
|
So, we see that there are 12 + 2 = 14 permutations that would come before "VIEW".
|
||||||
|
Hence, rank of permutation = 15.
|
||||||
|
```
|
||||||
|
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user