算法详解

文章目录

  • 动态规划
    • 1.最长递升子序列(LIS)
    • 2.编辑距离
    • 3.[扔鸡蛋]
    • 4.动态规划子序列问题解题模板
      • 最长回文子序列
    • 5.博弈问题
    • 6.贪心算法
      • 6.1贪心算法之调度问题
    • 7.leetcode股票问题
    • 8.打家劫舍问题
    • 9.四键键盘

2020年发生了许多历史性的事件,最糟糕的是今年居然是自己的求职年,各种不确定因素夹杂在一起感觉自己的求职过程会很艰辛,唯有尽人事听天命,做好自己,踏实准备,此篇博文为准备各类算法而写,望在未来的日子里能有所收获----记。

动态规划

动态规划都是从一张表格开始,DP三要素:

  1. dp table 的具体含义;
  2. 状态转移方程;
  3. dp初始化
    动态规划的核心思想是数学归纳法,在求解dp[i]时假设 i < n ii<n 时都成立,然后尝试证明当 i = n i = n i=n时也成立;

1.最长递升子序列(LIS)

输入: [10,9,2,5,3,7,101,18]
输出:4
解释:最长上升子序列 [2,3,7,101] (不唯一,但长度唯一)

最长上升子序列是求长度而不是具体的子序列,因此我们的dp table的具体含义可以设置为在当前位置 i 处的最长递升子序列为dp[i];当我们考虑dp时,由于假设的是 x < i x < i x<i处的dp[x]是已知的,确定dp[i]时存在两种情况:

  1. 当L[i]>L[x]时,则dp[i] = dp[x] + 1; x ∈ ( 0 , i − 1 ) x \in(0,i-1) x(0,i1)
  2. 当L[i] <= L[x]时,dp[i] = dp[x]; x ∈ ( 0 , i − 1 ) x \in(0,i-1) x(0,i1)

最后求得max(dp[i])即在当前位置的最长递升子序列

strs = '2 7 1 5 6 4 3 8 9'
str1 = list(map(int,strs.split(' ')))
init = [1 for i in range(len(str1))] //table 初始化,至少为1
for i in range(2,len(str1) + 1):
	for j in range(i-1):
		if(str1[i-1] > str1[j]):
			init[i-1] = max(init[i-1],init[j] + 1)
lis = max(init)
>>init :[1,2,1,2,3,2,2,4,5]

当前解法的复杂度为 O ( n 2 ) O(n^2) O(n2),利用二分法可以将复杂度降低至 O ( n l o g n ) O(nlogn) O(nlogn)

设置一个数组low,该数组的元素更新原则是寻找第一个大于 L ( i ) L(i) L(i)的元素,将 L ( i ) L(i) L(i)替换该元素。

def binary_search(strs, R, x):
    L = 0
    while (L < R):
        mid = (L + R) // 2
        if (strs[mid] < x):
            L = mid + 1
        elif(strs[mid] > x):
            R = mid - 1
        else:
            L = mid
    return L
strs = [1, 4, 7, 6, 5, 9, 10, 3]
Low = [-1 for i in range(len(strs))]
Low[0] = strs[0]
ans = 1
for i in range(1, len(strs)):
    if strs[i] > Low[ans - 1]:
        Low[ans] = strs[i]
        ans += 1
        print(Low)
    else:
        Low[binary_search(Low, ans, strs[i])] = strs[i]

print(Low)

2.编辑距离

给定两个字符串s1和s2,计算出将s1转换为s2所使用最少的操作数,可以使用以下三种操作:
1.插入一个字符;
2.删除一个字符;
3.替换一个字符;

举例:将‘rad’ -> ‘apple’

思路:若想知道‘rad’ -> ‘apple’ 的距离,则可以通过’ra’ -> ‘apple’的距离推测出来,若想知道’ra’ -> ‘apple’ 的距离,则可以通过‘r’ -> ‘apple’ 的距离推测出来;

* a p p l e
* 0 1 2 3 4 5
r 1 1 2 3 4 5
a 2 1 2 3 4 5
d 3 2 2 3 4 5

由于有三种操作方法:

  1. 插入:以’r’ -> ‘ap’为例,由于‘r’ -> ‘a’ 的操作数是已知的,所以在’r’->‘a’ 后面进行插入操作,也就是’r’->‘a’ 的操作数 + 1,即dp[1][1] + 1;
  2. 删除:即以’’ -> 'ap’已知,当欲求‘r’ -> ‘ap’时,可以对’r’进行删除操作,进而转化为’’ -> ‘ap’,即dp[0][2] + 1;
  3. 替换: 若想求‘r’->‘ap’ ,由于’*’ -> ‘a’是已知的,所以可以用’p’ 替换’r’,即dp[0][1] + 1;由于’p’ != ‘r’ 所以加1,若最后一位元素相等,则不需要+1;

故状态转移方程为:
dp[i][j] = min(dp[i-1][j] + 1,dp[i][j-1] + 1, dp[i-1][j-1] + str1[i] != str2[j])

def editDistance(str1, str2):
    len1, len2 = len(str1) + 1, len(str2) + 1
    dp = [[0 for i in range(len2)] for j in range(len1)]
    for i in range(len1):
        dp[i][0] = i
    for j in range(len2):
        dp[0][j] = j
    for i in range(1, len1):
        for j in range(1, len2):
            dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + (str1[i - 1] != str2[j - 1]))
    return dp[-1][-1]
 
 
while True:
    try:
        print(editDistance(input(), input()))
    except:
        break

3.[扔鸡蛋]

讲解

有从1到N共N层的楼。现手中有k个鸡蛋。现在确定这栋楼存在楼层 0 < = F < = N 0<=F<=N 0<=F<=N,在这层楼将鸡蛋扔下去,鸡蛋恰好没摔碎(高于 F 的楼层都会碎,低于 F 的楼层都不会碎)。问,在最坏的情况下,至少要扔几次鸡蛋才能确定F?

解决动态规划问题的基本框架:

  1. 有什么状态 -->对于状态转换方程;
  2. 有什么选择 -->也就是操作,即改变状态的行为;
  3. 穷举;
  4. 利用备忘表dp table进行避免重复计算;

【状态】:每次都会变话的参数为鸡蛋的个数k 和 需要测试的楼层数;
【选择】:会改变状态的行为,在哪一层扔鸡蛋。
【穷举】:从第一层到第N层,每一次都会有一个操作数,即从这些操作数中选出最小的;

动态规划最重要的就是确定状态如何转移:该题中我们选择在第 i i i层进行扔鸡蛋,会产生两种不同的状态:

  1. 鸡蛋碎了,则我们需要将状态转变为(k-1,i-1),在 i − 1 i-1 i1层楼中拿k-1个鸡蛋进行扔鸡蛋实验;
  2. 鸡蛋没碎,则状态转变为(k,N - i),即拿着k个鸡蛋在 [ i + 1 , N ] [i+1,N] [i+1,N] N − i N-i Ni层楼进行扔鸡蛋实验;
def superEggDrop(k,N):
	memo = dict()
	def dp(k,N):
		if k == 1:
			return N
		if N == 0:
			return 0
		if (k,N) in memo:
			return memo[(k,N)]
		res = float('INF')
		#穷举所有的可能性
		for i in range(1,N+1):
			res = min(res, max(dp(k,N-i),dp(k-1,i-1)) + 1)

		mome[(k,N)] = res
		return res
	return dp(k,N)
//二分法:
def superEggDrop(K, N):
        
    memo = dict()
    def dp(K, N):
        if K == 1: return N
        if N == 0: return 0
        if (K, N) in memo:
            return memo[(K, N)]
                            
        # for 1 <= i <= N:
        #     res = min(res, 
        #             max( 
    #                     dp(K - 1, i - 1), 
    #                     dp(K, N - i)      
        #                 ) + 1 
        #             )

        res = float('INF')
        # 用二分搜索代替线性搜索
        lo, hi = 1, N
        while lo <= hi:
            mid = (lo + hi) // 2
            broken = dp(K - 1, mid - 1) # 碎
            not_broken = dp(K, N - mid) # 没碎
            # res = min(max(碎,没碎) + 1)
            if broken > not_broken:
                hi = mid - 1
                res = min(res, broken + 1)
            else:
                lo = mid + 1
                res = min(res, not_broken + 1)

        memo[(K, N)] = res
        return res
    
    return dp(K, N)

4.动态规划子序列问题解题模板

一旦涉及到子序列和最值,则一般都是考察动态规划;动态规划的核心就是定义dp table,找状态转移关系
一、两种思路

  1. 一维的dp table ,具体含义详见前文的LIS
for i in range(2,n+1):
	for j in range(i-1):
		if str1[i-1] > str2[j]:
			dp[i-1] = max(dp[i-1], dp[j] + 1)
  1. 一个二维的dp数组:
int n = arr.length;
int[][] dp = new dp[n][n];

for (int i = 0; i < n; i++) {
    for (int j = 0; j < n; j++) {
        if (arr[i] == arr[j]) 
            dp[i][j] = dp[i][j] + ...
        else
            dp[i][j] = 最值(...)
    }
}

该思路中有包含这两种情况:

  • 2.1一种是涉及两个字符串时(如最长公共子序列)
    此时dp含义如下:
    在子数组 a r r 1 [ 0 , . . . , j ] arr1[ 0,...,j ] arr1[0,...,j] 和子数组 a r r 2 [ 0 , . . , j ] arr2[0,..,j ] arr2[0,..,j] 中,我们要求的子序列长度为dp[i][j];
  • 2.2 只涉及一个字符串(如最长回文子序列)

最长回文子序列

在子串 s [ i . . j ] s[i..j ] s[i..j] 中,最长回文子序列的长度为 dp[i][j], 动态规划的核心思想就是寻找状态转移,而状态转移的核心思想就是归纳思维,简单的说就是从已知的部分推出未知的部分。假如知晓 d p [ i + 1 ] [ j − 1 ] dp[i+1][j-1] dp[i+1][j1] 就可以通过通过str[i] = str[j] ? 推测出来:

  • 若str[i] = str[j]:则 d p [ i ] [ j ] = d p [ i + 1 ] [ j − 1 ] + 2 dp[i][j]=dp[i+1][j-1] + 2 dp[i][j]=dp[i+1][j1]+2
  • 若str[i] != str[j] : 则 d p [ i ] [ j ] = m a x ( d p [ i ] [ j − 1 ] , d p [ i − 1 ] [ j ] ) dp[i][j]=max(dp[i][j-1], dp[i-1][j]) dp[i][j]=max(dp[i][j1],dp[i1][j])

代码实现为:

if (s[i] == s[j]):
	dp[i][j] = dp[i+1][j-1] + 2
else:
	dp[i][j] = max(dp[i+1][j],dp[i][j-1])

base_case:

  • i = j i=j i=j 时:dp[i][j] = 1;
  • i > j i>j i>j时:dp[i][j] = 0;

算法详解_第1张图片

  • 当 i < j 时:dp[i][j] 是根据dp[i+1][j-1], dp[i+1][j], dp[i][j-1]这三个位置确定;
    算法详解_第2张图片
def longestPalindSubseq(s):
	n = len(s)
	dp = [0 for i in range(n)]
	for i in range(n):
		dp[i][i] = 1
	for i in range(n-2,-1,-1):
		for j in range(i+1,n):
			if (s[i] == s[j]):
				dp[i][j] = dp[i+1][j-1] + 2
			else:
				dp[i][j] = max(dp[i+1][j],dp[i][j-1])
				
	return dp[0][n-1]

5.博弈问题

有一排石头堆,用数组piles表示,piles[i]表示第i堆石头有多少个。两个人轮流拿石头,一次拿一堆,但是只能拿最左边或者最右边的石头堆。等所有石头被拿完后,谁拥有的石头数多谁就获胜;

动态规划两大核心:状态与选择
该题中选择是选择最左边的石头还是最右边的石头;通过选择发生的变化就是状态的转移,该题中的状态是开始的索引 i ,结束的索引 j, 当前轮到的人;

确定dp数组的含义:
创建一个存储元组的二维数组dp table,其中dp[i][j]表示从i开始到j结束的范围内从左开始和从右开始的最高分数;

dp[i][j].fir 表示,对于 piles[i…j] 这部分石头堆,先手能获得的最高分数。
dp[i][j].sec 表示,对于 piles[i…j] 这部分石头堆,后手能获得的最高分数。
举例理解一下,假设 piles = [3, 9, 1, 2],索引从 0 开始
dp[0][1].fir = 9 意味着:面对石头堆 [3, 9],先手最终能够获得 9 分。
dp[1][3].sec = 2 意味着:面对石头堆 [9, 1, 2],后手最终能够获得 2 分。

状态转移方程:

dp[i][j].fir = max(piles[i] + dp[i+1][j].sec,piles[j] + dp[i][j-1].sec)
解释:在从 i   j i~j i j中先手选石头堆有两种选择,从左边开始和从右边开始,其中从左边开始的结果就是piles[i] + i + 1   j i+1 ~ j i+1 j石头堆中的后手(这是由于两人轮流进行),从左边开始推理类似。再从左边开始和右边开始之间选取最大值;此时fir的状态转移方程已出现;下面是sec的状态转移方程
if 先手选择左边:
dp[i][j].sec = dp[i+1][j].fir
if 先手选择右边:
dp[i][j].sec = dp[i][j-1].fir

设定dp的初始值:

if 0<=i == j <=n:
dp[i][j].fir = piles[i]
dp[i][f].sec = 0
只有一堆石头时先手必赢

遍历dp
根据状态转移方程,计算dp[i][j]时需要用到dp[i+1][j] 和dp[i][j-1],
算法详解_第3张图片算法详解_第4张图片
遍历的顺序不止一种,但都是斜着遍历的;整个程序代码如下:

//返回最后先手和后手的得分差
def stoneGame(piles):
	n = len(piles)
	dp = [[(0,0) for i in range n] for j in range(n)]
	for i in range(n):
		dp[i][i] = (piles[i],0)
	for i in range(n-2,-1,-1):
		for j in range(i+1,n):
			left = piles[i] + dp[i+1][j][1]
			right = piles[j] + dp[i][j-1][1]
			//状态转移方程
			if (left > right):
				dp[i][j][0] = left
				dp[i][j][1] = dp[i+1][j][0]
			else:
				dp[i][j][0] = right
				dp[i][j][1] = dp[i][j-1][0]
	res = dp[0][n-1]
	return res[0] - res[1]

6.贪心算法

贪心算法是特殊的动态规划,具有更多的约束条件,所谓的贪心算法就是:每一步都做出一个局部最优的选择,最终的结果就是最优的,但只有一部分问题拥有这个性质;

6.1贪心算法之调度问题

给出一系列形如[start,end]的闭区间,算出最多有几个互不相交的区间

该题的目的是求出最多的区间,解题思路如下:

  1. 从区间集合intvs中选择一个区间x,这个x在所有区间中结束最早;
  2. 把所有与x冲突的区间从集合intvs中删除;
  3. 重复步骤1和2,直到intvs为空;

解题代码如下:

def intervalSchedule(invts):
	if (len(invts) == 0):
		return 0
	sorted(invts, key = lambda x:x[1])//key 是一个函数对象,参数就是列表中每一个元素
	count = 1
	x_end = invts[0][1]
	for i in invts[1:]:
		start = i[0]
		if start >= x_end:
			count += 1
			x_end = i[1]
	return count

7.leetcode股票问题

题目

给定一个数组,他的第 i i i个元素表示股票第 i i i天的价格,设计一个算法来实现获取的最大利润,其中你可以有k次交易(即可以有k次买入卖出),注意:股票不得在买进之前卖出

利用‘状态机’框架来完成,所谓的‘状态机’也就是dp table,使用动态规划首先确定状态选择
选择: buy(买入), sell(卖出), rest(啥也不干)

  • sell 之前必须有buy;
  • buy时交易次数k>0;
  • rest分为buy 之后的rest和sell之后的rest;

状态: i(天数),k(允许交易的次数),rest(0:表示没有持有股票,1:持有股票)

明确状态之后开始穷举所有的状态:

dp[ i ][ k ][0 or 1]
0<=i<=n-1, 1<=k<=K
其中n为天数,大K为最多交易数,总共的状态为nK2

 for i in range(0,n):
 	for k in range(K):
 		for s in {0,1}:
 			dp[i][k][s] = max{buy, sell, rest}

>>dp[2][3][1] : 在第三天,我手里有股票,现在进行了三次交易
>>最终欲求的是dp[n-1][K][0]

确定状态转移方程:
股票问题归根结题就两种状态,一种是手里有股票,一种是手里没股票:

dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
手里没股票有两种可能:
1.前一天就没股票,今天保持原样;
2.前一天有股票,但今天我把他卖了;
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
手里有股票有两种可能:
1.前一天就有,今天保持原样;
2.前一天没有,今天刚买入

确定base case:

dp[-1][k][0] = 0: 还没开始,利润必定为0
dp[-1][k][1] = -inf ,还没开始是不可能买入的,所以用负无穷表示不可能
dp[i][0][1] = -inf, k = 0,表示不可交易,则不可能会有股票
dp[i][0][0] = 0: 不可交易当然利润为0

状态转移方程:

//base case:
dp[-1][k][0] = dp[i][0][0] = 0
dp[-1][k][1] = dp[i][0][1] = -inf

//状态转移方程
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])

股票问题之-- k = 1

给定一个数组,他的第 i i i个元素表示股票第 i i i天的价格,设计一个算法来实现获取的最大利润,其中你可以有1次交易(即可以有k次买入卖出),注意:股票不得在买进之前卖出

将k=1代入状态转移方程中:

dp[i][k][0] = max(dp[i-1][1][0], dp[i-1][1][1] + prices[i])
dp[i][k][1] = max(dp[i-1][1][1], dp[i-1][0][0] - prices[i]) = max(dp[i-1][1][1], - prices[i])

此时k对整个状态转移方程已经没有影响了,所以可以进一步简化状态转移方程:

dp[i][k][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][k][1] = max(dp[i-1][1], - prices[i])

代码如下:

n = len(prices)
dp = [[0 for i in range(n)] for j in range(2)]
for i in range(n):
	if(i -1 == -1):
		dp[i][0] = 0
		dp[i][1] = -prices[i]
	else:
		dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
		dp[i][1] = max(dp[i-1][1],-prices[i])

//dp[i]的状态实际上只跟dp[i-1]有关,所以可以继续简化,将空间复杂度降为O(1)
dp_i_0 = 0
dp_i_1 = -inf
for i in range(n):
	//dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
	dp_i_0 = max(dp_i_0,dp_i_1 + prices[i])
	//dp[i][1] = max(dp[i-1][1],-prices[i])
	dp_i_1 = max(dp_i_1,-prices[i])
return dp_i_0

股票问题之-- k = + inf
若K为无穷大,则可以认为k对整个状态转移方程式没有影响的

dp[i][k][0] = max(dp[i-1][k][0],dp[i-1][k-1][1] + princes[i])
dp[i][k][1] = max(dp[i-1][k][1],dp[i-1][k-1][0] - prince[i])//此时k = k-1

由于k对整个状态转移方程已经没有影响了,故可以简化方程为:
dp[i][k][0] = max(dp[i-1][0],dp[i-1][1] + princes[i])
dp[i][k][1] = max(dp[i-1][1],dp[i-1][0] - prince[i])

解题代码为:

n= len(prices)
dp_i_0 = 0
dp_i_1 = -inf
for i in range(n):
	tmp = dp_i_0 
	dp_i_0 = max(dp_i_0,dp_i_1 + princes[i])
	dp_i_1 = max(dp_i_1,tmp - princes[i])
return dp_i_0

股票问题之-- k = + inf + cooldown
也就是每次卖出之后必须等一天才能继续交易

dp[i][k][0] = max(dp[i-1][0],dp[i-1][1] + princes[i])
dp[i][k][1] = max(dp[i-1][1],dp[i-2][0] - prince[i])
//解释:第 i 天选择 buy 的时候,要从 i-2 的状态转移,而不是 i-1

解题代码为:

n= len(prices)
dp_i_0 = 0
dp_i_1 = -inf
dp_pre_0 = 0
for i in range(n):
	tmp = dp_i_0 
	dp_i_0 = max(dp_i_0,dp_i_1 + princes[i])
	dp_i_1 = max(dp_i_1,dp_pre_0 - princes[i])
	dp_pre_0 = temp
return dp_i_0

股票问题之-- k = + inf with fee
每次交易需要手续费

dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i] - fee)
//解释:相当于买入股票的价格升高了。
//在第一个式子里减也是一样的,相当于卖出股票的价格减小了

解题代码:

n= len(prices)
dp_i_0 = 0
dp_i_1 = -inf
for i in range(n):
	tmp = dp_i_0 
	dp_i_0 = max(dp_i_0,dp_i_1 + princes[i])
	dp_i_1 = max(dp_i_1,tmp - princes[i] - fee)
return dp_i_0

股票问题之-- k = 2
原始状态转移方程在这种情况就无法简化了:

dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])

解题代码:

n = len(prices)
max_k = 2
dp = [[0 for i in range(n)] for j in range(2)]
for i in range(n):
	for k in range(max_k,0,-1):
	if(i -1 == -1):
		dp[i][k][0] = 0
		dp[i][k][1] = -prices[i]
	else:
		dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
		dp[i][k][1] = max(dp[i-1][k][1],dp[i-1][k-1][0] - prices[i])
return dp[n-1][max_k ][0]

股票问题之-- k = any int
若简单的利用上一题的解法,则容易超内存,这是由于传入的k会非常大,这样分析,当有n天的股票时,一次完整的交易(买进,卖出)至少需要两天,所以k最大可以达到n/2,若k>n/2时则与k 为无穷是等效的。所以该题分为两部分,一部分是小于n/2,一部分是大于n/2

if k >n/2:
	--k = + inf 解法--
else:
	--k = 2 的解法--

总结,该题型的核心为:

//base case:
dp[-1][k][0] = dp[i][0][0] = 0
dp[-1][k][1] = dp[i][0][1] = -inf

//状态转移方程
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])

8.打家劫舍问题

一个强盗,从一排房子走过,在每间房子前都有两种选择:抢或者不抢。如果选择抢,则不能抢相邻的下一间房子,只能从下下间房子开始做选择。若步枪,则可以从下一间房子继续做选择,当走过最后一间房子后,就没有得抢。

动态规划问题核心:状态和选择。该题的状态就是房子的索引,选择就是抢或者不抢;

def dp(num,start):
	if start>=len(num):
		return 0
	else:
		res = max(dp(num,start+1),num[start] + dp(num,start+2))
	return res

常规的递归解法,缺点是存在大量的重复计算,因此可以引入备忘录dp table来记录已计算过得值;

def rob(num):
	memo = [-1 for i in range(len(num))]
	return dp(num,0)
def dp(num,start):
	if start>= len(num):
		return 0
	elif memo[start] != -1:
		return memo[start]
	res = max(dp(num,start+1), num[start] + dp(num,start+2))
	memo[start] = res
	return res

递归 + 备忘录 = 自顶向下的动态规划算法

若这一排的房子是首位相接的一个环,则怎么抢可以抢到最多的钱,原则与上题一致

两种选择:第一间抢最后一间不抢,最后一间抢第一间不抢

def rob (nums):
	n = len(nums)
	if n == 1:
		return nums[0]
	else:
		return max(robRange(nums,0,n-2),robRange(nums,1,n-1))

def robRange(nums, start, end):
	n = len(nums)
	dp_i_1 = dp_i_2 = 0
	dp dp_i = 0
	for i in range(end,start-1,-1):
		dp_i = max(dp_i_1,nums[i] + dp_i_2)
		dp_i_2 = dp_i_1
		dp_i_1 = dp_i
	return dp_i

若房子是按照二叉树排列,且相连的两座房子是不能连续被抢

memo = {}
def rob(TreeNode root):
	if root == NULL:
		return 0
	if root in memo.keys():
		return memo[root]
	do_it = root.val + 
			(0 if root.left == NULL else rob(root.left.left) + rob(root.left.right))
	not_do = rob(root.left) + rob(root.right)
	res = max(do_it,not_do)
	memo[root] = res
	return res

9.四键键盘

有一个键盘,只有四个按钮: A, ctrl + A, ctrl + C, ctrl + V,其中A表示在屏幕中打印一个‘A’,ctrl + A 表示全选屏幕,ctrl + C, ctrl + V分别表示复制和粘贴,请问经过N此操作之后A的个数最多可以达到多少个?

动态规划的基本思想:状态 + 选择;
该题的选择有四种,实际上也可以看成三种,‘A’,‘ctrl + A 和 ctrl + C’, ‘ctrl + V’;
该题的状态有三种:剩下的次数n,屏幕上’A’的个数a_num,和剪切板上‘A’的个数copy

确定了状态和选择后可以写出状态转移方程:

dp(n-1,a_num+1,copy)//按下‘A’
dp(n-1,a_num + copy,copy)//按下‘ctrl + V’
dp(n-2,a_num,a_num)//按下‘ctrl + A 和 ctrl + C’

基本框架确定好后,再加上备忘录dp table来避免重复计算子问题!解题代码如下:

dp max_A(n):
	memo = {}
	def dp(n,a_num,copy):
		if (n,a_num,copy) in memo.keys():
			return memo[(n,a_num,copy)]
		if n<=0:
			return a_num
		memo[(n,a_num,copy)] = max(
				dp(n - 1, a_num + 1, copy),    # A
                dp(n - 1, a_num + copy, copy), # C-V
                dp(n - 2, a_num, a_num)        # C-A C-C
					)
		return memo[(n,a_num,copy)]
	return dp(N,0,0)

这种状态选择导致算法复杂度过高,消耗时间过长,选择一种的新的状态,实际上最优按键序列一定是只有两种情况的:

  1. 一直按‘A’:A,A,…,A(当N比较小时)
  2. 先按‘A’,后接‘C+A C+C’ 然后跟数个‘C+V’,依次循环

两种情况最后一个按键一定是‘A’或者‘C+V’,所以我们用一个变量,‘j’作为若干‘C+V’的起点,则’j-2’代表‘C+A,和C+C’的操作了;整个思路类似于 ‘LIS’。
解题代码为:

def maxA(N):
	dp = [0 for i in range(N+1)]
	for i in range(1,N+1):
		dp[i] = dp[i-1] + 1
		if i>3:
			for j in range(3,i+1):
				dp[i] = max(dp[i], dp[j-3]*(i-j + 1))
	return dp[N]
		

你可能感兴趣的:(学习)