Strategies for dynamic programming
Longest increasing subsequence
Given an integer array nums, return the length of the longest strictly increasing subsequence.
A subsequence is a sequence that can be derived from an array by deleting some or no elements without changing the order of the remaining elements. For example, [3,6,2,7] is a subsequence of the array [0,3,1,6,2,2,7].
brute force is you make a dp=[], perform a double loop, and use the nums array to determine if the value you are at is greater than the value at the nums, then we use the dp array to determine what the new value at that num index should be
time: O(n^2), space: O(N)
- —’’’
- —Once you realize the first iteration does nothing, you can optimize to range(1, len(nums)), but don’t worry about that at first,
- —bc you are more likely to fuck up, the first implementation does not have to be 100% right, just needs to work
- —’’’
- —def answer1(self, nums):
- ——-dp = [1]*len(nums)
- ——-m=1
- ——-for i in range(len(nums)):
- ———–for j in range(len(nums[:i])):
- —————if nums[i]>nums[j]:
- ——————-dp[i]=max(dp[i], dp[j]+1)
- ——————-m=max(m, dp[i])
- ——-return m
1. after you see the brute solution, you can see we are doing some type of search on a list, this search can be improved with binary search if instead of a dp array we used a custom array
2. make an array with the first number in it to start
3. if the num in the list is greater than the last index of our array (the largest value at any time), we append it
4. if not, we search the array for the first index that is larger than it and replace it
5. At this point you might think what if we have [1,3,4,2] and we replace 3 with 2 with this method, it ultimately does not affect the end result be we really only care about the end index and the total length at the end. if we have [1,3] and we find 2, we want to replace 3 with 2; this is because later if we see 3, we want to append that to increase the total length. But if we have 1,3,4 and see 2, replacing 3 with 2 has no effect. It does alter the “ordering” but that isn’t what we care about
time: O(log(n)*n), we search through the entire num list and do a binary search each time
space: O (n), we can have a list of all the numbers at worst
Longest common subsequence
Given two strings text1 and text2, return the length of their longest common subsequence. If there is no common subsequence, return 0.
A subsequence of a string is a new string generated from the original string with some characters (can be none) deleted without changing the relative order of the remaining characters.
For example, “ace” is a subsequence of “abcde”.
A common subsequence of two strings is a subsequence that is common to both strings.
brute, get a set of all possible combinations of both strings, check 1 subset is in the other, then compare them to the max length, return max length
time: 2^len(strings), space = 2^len(string). the total number of a subsequence of a string is 2^len(string)
!!! if you use lru_cache it needs to be a nested function or else lru_cache caches self too
time: O(NM), the length of both strings
space: O(nm), the grid
time: O(N*M), the length of both strings
space: O(min(M, N)), 2 lists that have the min length between them
Integer break
Given an integer n, break it into the sum of k positive integers, where k >= 2, and maximize the product of those integers.
Return the maximum product you can get.
Dynamic programming
Time: O N^2 without dynamic programing, but with dp we can get closer to linear time
Space: O N, linear space bc we store each value for n-2 until it is 0
class IntegerBreak:
Number of ways to climb a staircase
Hi, here’s your problem today. This problem was recently asked by LinkedIn:
You are given a positive integer N which represents the number of steps in a staircase. You can either climb 1 or 2 steps at a time. Write a function that returns the number of unique ways to climb the stairs.
# def staircase(n): # # Fill this in.
# print staircase(4) # # 5 # print staircase(5) # # 8
Brute force is recognize it is a fib sequence and return stanard fib,
time: O(n), we must loop through the range once
space: constant, we do not store any additional data
coin change
You are given an integer array coins representing coins of different denominations and an integer amount representing a total amount of money.
Return the fewest number of coins that you need to make up that amount. If that amount of money cannot be made up by any combination of the coins, return -1.
You may assume that you have an infinite number of each kind of coin.
if amount is 0, you can return 0 or 1 (some people want you to return 1, just add an initial if statement)
time: O(amount* coins), for each index we go through each coin
space: O(n), linear space since we instantiate an array
time: target^coins. worst case each value of the target sees each coin. you might think it is factorial, but that is not true. Think 100 with 1,2,3 coins, worst case there are about 100 solutions per coin, and each coin will have to see each other coin’s solutions each time they loop
space = target, the depth of the recursion tree at any given time
—-def brute(self, coins, target):
——–answer = self.brute_helper(coins, target)
——–if answer==float(‘inf’):
————return -1
——–else:
————return answer
——–
—-def brute_helper(self, coins, target):
——–if target==0:
————return 0
——–count=float(‘inf’)
——–for coin in coins:
————if coin<=target:
—————-count=min(count, 1+self.brute_helper(coins, target-coin))
——–return count
word break
Given a string s and a dictionary of strings wordDict, return true if s can be segmented into a space-separated sequence of one or more dictionary words.
Note that the same word in the dictionary may be reused multiple times in the segmentation.
Example 1:
Input: s = “leetcode”, wordDict = [“leet”,”code”]
Output: true
Explanation: Return true because “leetcode” can be segmented as “leet code”.
Example 2:
Input: s = “applepenapple”, wordDict = [“apple”,”pen”]
Output: true
Explanation: Return true because “applepenapple” can be segmented as “apple pen apple”.
Note that you are allowed to reuse a dictionary word.
Example 3:
Input: s = “catsandog”, wordDict = [“cats”,”dog”,”sand”,”and”,”cat”]
Output: false
Time complexity : O(2^n). Given a string of length n, there are about n ways to split it into two parts.
At each step, we have a choice: to split or not to split. In the worse case,
when all choices are to be checked, that results in O(2^n). This is because we split each word into 2, then each word from that into 2
, and so on,
recognize that each entry can propagate into many more entries, and if they all do not return anything we have to go back to the start,
increment 1, and do it again, these are usually exponential in some fashion
Space complexity : O(n). The depth of the recursion tree can go upto n.
time: o n^3, n searches the initial string, and the recurion tree can become n^2, we reduce this from exponential because each combination of the strings will only be hit once bc of the memo in theory, n because we search the initial string, and n^2 because we have to search that string for each of its combinations at each character, so n*n^2 = n^3.
IN PRACTICE this is better than an O n^2 solution bc of the dynamic programming aspect (according to time on LC)
space: O(n), bc the recursion depth is only going to be the length of the initial string
Pow(x, n)
Implement pow(x, n), which calculates x raised to the power n (i.e., xn).
brute is just multiplying x by itself n times
time: O(log(n)), since determines how many times we do this, and n//2 each iteration, we get log(n)
space: log(n) as well, the depth of the recursion tree
class Solution:
time: O(log(n)), since determines how many times we do this, and n//2 each iteration, we get log(n)
space: constant, we do not create new objects
—-def optimal_iterative(self, x, n):
——–if n < 0:
————x = 1 / x
————n = -n
——–r=1
——–c=x
——–while n:
————if n%2==1:
—————-n-=1
—————-r *= c
# can have another if statement for even, not needed
————c *= c
————n//=2
——–return r
square root x
Given a non-negative integer x, compute and return the square root of x.
Since the return type is an integer, the decimal digits are truncated, and only the integer part of the result is returned.
Note: You are not allowed to use any built-in exponent function or operator, such as pow(x, 0.5) or x ** 0.5.
Example 1:
Input: x = 4
Output: 2
time: log(n), binary search is log n time
space: constant
class Solution: