1
0
mirror of https://github.com/dholerobin/Lecture_Notes.git synced 2025-03-16 14:19:58 +00:00
This commit is contained in:
riyabansal98 2019-10-15 14:55:41 +05:30
commit 4939458348
5 changed files with 291 additions and 512 deletions
Recursion and Backtracking
Sorting

@ -1,237 +0,0 @@

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="400" />
__Time Complexity__ (assuming all multiplications are O(1))? $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="400"
/>
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="400"
/>
<img src="https://user-images.githubusercontent.com/35702912/66468119-42098600-eaa3-11e9-8f24-237be2a91d12.jpg" width="400"
/>
```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)$
__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="400"
/>
```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. <br>
__Time Complexity__: O(2^n)
<br>
__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_0040](https://user-images.githubusercontent.com/35702912/66470200-c27db600-eaa6-11e9-8744-ca572d6000e1.jpg)
```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)_

@ -1,164 +0,0 @@
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.
```

@ -1,111 +0,0 @@
Number of Squareful Arrays
--------------------------
> Given A[N]
> array is squareful if for every pair of adjacent elements, their sum is a perfect square
> Find and return the number of permutations of A that are squareful
>
Example:
A = [2, 2, 2]
output: 1
A = [1, 17, 8]
output: 2
[1, 8, 17], [17, 8, 1]
```python
def check(a, b):
sq = int((a + b) ** 0.5)
return (sq * sq) == (a + b)
if len(A) == 1: # corner case
return int(check(A[0], 0))
count = 0
def permute_distinct(S, i):
global count
if i == len(S):
count += 1
for j in range(i, len(S)):
if S[j] in S[i:j]: # prevent duplicates
continue
if i > 0 and (not check(S[j], S[i-1])): # invalid solution - branch and bound
continue
S[i], S[j] = S[j], S[i]
permute_distinct(S, i+1)
S[i], S[j] = S[j], S[i] # backtrack
permute_distinct(A, 0)
return count
```
Gray Code
---------
> Given a non-negative integer N representing the total number of bits in the code, print the sequence of gray code. A gray code sequence must begin with 0.
> The gray code is a binary numeral system where two successive values differ in only one bit.
G(n+1) can be constructed as:
0 G(n)
1 R(n)
```
Example G(2) to G(3):
0 00
0 01
0 11
0 10
----
1 10
1 11
1 01
1 00
```
```python
def gray(self, n):
codes = [0, 1] # length 1
for i in range(1, n):
new_codes = [s | (1 << i) for s in reversed(codes)]
codes += new_codes
return codes
```
N Queens
--------
[NQueens - InterviewBit](https://www.interviewbit.com/problems/nqueens/)
Backtracking
- Place one queen per row
- backtrack if failed
Word Break II
-------------
> Given a string A and a dictionary of words B, add spaces in A to construct a sentence where each word is a valid dictionary word.
```
Input 1:
A = "catsanddog",
B = ["cat", "cats", "and", "sand", "dog"]
Output 1:
["cat sand dog", "cats and dog"]
```
```python
def wordBreak(A, B):
B = set(B)
sents = []
def foo(i, start, sent):
word = A[start:i+1]
if i == len(A):
if word in B:
sents.append((sent + ' ' + word).strip())
return
if word in B:
foo(i+1, i+1, sent + ' ' + word)
foo(i+1, start, sent)
foo(0, 0, '')
```

152
Sorting/1.md Normal file

@ -0,0 +1,152 @@
# Sorting
- define sorting: permuting the sequence to enforce order. todo
- brute force: $O(n! \times n)$
Stability
---------
- definition: if two objects have the same value, they must retain their original order after sort
- importance:
- preserving order - values could be orders and chronological order may be important
- sorting tuples - sort on first column, then on second column
-- --
Insertion Sort
--------------
- explain:
- 1st element is sorted
- invariant: for i, the array uptil i-1 is sorted
- take the element at index i, and insert it at correct position
- pseudo code:
```c++
void insertionSort(int arr[], int length) {
int i, j, key;
for (i = 1; i < length; i++) {
key = arr[i];
j = i-1;
while (j >= 0 && arr[j] > key) {
arr[j+1] = arr[j];
j--;
}
arr[j + 1] = key;
}
}
```
- **Stablility:** Stable, because swap only when strictly >. Had it been >=, it would be unstable
- **Complexity:** $O(n^2)$
-- --
Bubble Sort
-----------
- explain:
- invariant: last i elements are the largest one and are in correct place.
- why "bubble": largest unsorted element bubbles up - just like bubbles
- pseudo code:
```c++
void bubbleSort(int arr[], int n) {
for (int i = 0; i < n-1; i++)
for (int j = 0; j < n-i-1; j++)
if (arr[j] > arr[j+1])
swap(&arr[j], &arr[j+1]);
}
```
- **Stability:** Stable
- **Complexity:** $O(n^2)$
-- --
Bubble Sort with window of size 3
---------------------------------
- explain bubble sort as window of size 2
- propose window of size 3
- does this work?
- no - even and odd elements are never compared
-- --
Counting Sort
-------------
- explain:
- given array, first find min and max in O(n) time
- create space of O(max-min)
- count the number of elements
- take prefix sum
- constraint: can only be used when the numbers are bounded.
- pseudo code:
```c++
void counting_sort(char arr[]) {
// find min, max
// create output space
// count elements
// take prefix sum
// To make it stable we are operating in reverse order.
for (int i = n-1; i >= 0; i--) {
output[count[arr[i]] - 1] = arr[i];
-- count[arr[i]];
}
}
```
- **Stability:** Stable, if imlpemented correctly
- **Complexity**: $O(n + \max(a[i]))$
- why not just put the element there? if numbers/value, can do. Else, could be objects
-- --
Radix Sort
----------
- sort elements from lowest significant to most significant values
- explain: basically counting sort on each bit / digit
- **Stability:** inherently stable - won't work if unstable
- **complexity:** $O(n \log\max a[i])$
-- --
Partition Array
---------------
> Array of size $n$
> Given $k$, $k <= n$
> Partition array into two parts $A, ||A|| = k$ and $B, ||B|| = n-k$ elements, such that $|\sum A - \sum B|$ is maximized
- Sort and choose smallest k?
- Counterexample
```
1 2 3 4 5
k = 3
bad: {1, 2, 3}, {4, 5}
good: {1, 2}, {3, 4, 5}
```
- choose based on n/2 - because we want the small sum to be smaller, so choose less elements, and the larger sum to be larger, so choose more elements
-- --
Sex-Tuples
----------
> Given A[n], all distinct
> find the count of sex-tuples such that
> $$\frac{a b + c}{d} - e = f$$
> Note: numbers can repeat in the sextuple
- Naive: ${n \choose 6} = O(n^6)$
- Optimization. Rewrite the equation as $ab + c = d(e + f)$
- Now, we only need ${n \choose 3} = O(n^3)$
- Caution: $d \neq 0$
- Once you have array of RHS, sort it in $O(\log n^3)$ time.
- Then for each value of LHS, count using binary search in the sorted array in $\log n$ time.
- Total: $O(n^3 \log n)$
-- --
Anagrams
--------

139
Sorting/2.md Normal file

@ -0,0 +1,139 @@
# Sorting 2
-- --
Merge Sort
----------
- Divide and Conquer
- didive into 2
- sort individually
- combine the solution
- Merging takes $O(n+m)$ time.
- needs extra space
- code for merging:
```c++
// arr1[n1]
// arr2[n2]
int i = 0, j = 0, k = 0;
// output[n1+n2]
while (i<n1 && j <n2) {
if (arr1[i] <= arr2[j]) // if <, then unstable
output[k++] = arr1[i++];
else
output[k++] = arr2[j++];
}
// only one array can be non-empty
while (i < n1)
output[k++] = arr1[i++];
while (j < n2)
output[k++] = arr2[j++];
```
- stable? Yes
- in-place? No
- Time complexity recurrence: $T(n) = 2T(n/2) + O(n)$
- Solve by Master Theorem.
- Solve by algebra
- Solve by Tree height ($\log n$) * level complexity ($O(n)$)
-- --
Intersection of sorted arrays
-----------------------------
> 2 sorted arrays
> ```
> 1 2 2 3 4 9
> 2 3 3 9 9
>
> intersection: 2 3 9
> ```
- calculate intersection. Report an element only once
- Naive:
- Search each element in the other array. $O(n \log m)$
- Optimied:
- Use merge.
- Ignore unequal.
- Add equal.
- Move pointer ahead till next element
-- --
Merging without extra space
---------------------------
- can use extra time
- if a[i] < b[j], i++
- else: swap put b[i] in place of a[i]. Sorted insert a[i] in b array
- so, $O(n^2)$ time
-- --
Count inversions
---------------
> inversion:
> i < j, but a[i] > a[j] (strict inequalities)
- naive: $O(n^2)$
- Split array into 2.
- Number of inversions = number of inversions in A + B + cross terms
- count the cross inversions by example
- does number of cross inversions change when sub-arrays are permuted?
- no
- can we permute so that it becomes easier to count cross inversions?
- sort both subarrays and count inversions in A, B recursively
- then, merge A and B and during the merge count the number of inversions
- A_i B_j
- if A[i] > B[j], then there are inversions
- num inversions for A[i], B[j] = |A| - i
- intra array inversions? Counted in recursive case.
-- --
Find doubled-inversions
-----------------------
> same as inversion
> just i < j, a[i] > 2 * a[j]
- same as previous. Split and recursilvely count
- while merging, for some b[j], I need to find how many elements in A are greater than 2 * b[j]
- linear search for that, but keep index
- linear search is better than binary search
-- --
Sort n strings of length n each
- $T(n) = 2T(n/2) + O(n^2) = O(n^2)$ is wrong
- $T(n) = 2T(n/2) + O(n) * O(m) = O(nm\log n)$ is correct. Here m = the initial value of n
-- --
> .
>
> I G N O R E
>
> .
Bounded Subarray Sum Count
--------------------------
> given A[N]
> can have -ve
> given lower <= upper
> find numbe of subarrays such that lower <= sum <= upper
- naive: $O(n^2)$ (keep prefix sum to calculate sum in O(1), n^2 loop)
- if only +ve, $O(n\log n)$ using prefix sum
- but what if -ve?
-
-- --