1. 除数博弈
爱丽丝和鲍勃一起玩游戏,他们轮流行动。爱丽丝先手开局。
最初,黑板上有一个数字 N 。在每个玩家的回合,玩家需要执行以下操作:
1) 选出任一 x,满足 0 < x < N 且 N % x == 0 。
2) 用 N - x 替换黑板上的数字 N 。
如果玩家无法执行这些操作,就会输掉游戏。
只有在爱丽丝在游戏中取得胜利时才返回 True,否则返回 false。假设两个玩家都以最佳状态参与游戏。
示例 1:
输入:2
输出:true
解释:爱丽丝选择 1,鲍勃无法进行操作。
示例 2:
输入:3
输出:false
解释:爱丽丝选择 1,鲍勃也选择 1,然后爱丽丝无法进行操作。
提示:
1 <= N <= 1000
j解释:
方法一:规律(偶数先手比胜);
-
如果N是奇数,因为奇数的所有因数都是奇数,因此 N 进行一次 N-x 的操作结果一定是偶数,所以如果 a 拿到了一个奇数,那么轮到 b 的时候,b拿到的肯定是偶数,这个时候 b 只要进行 -1, 还给 a 一个奇数,那么这样子b就会一直拿到偶数,到最后b一定会拿到最小偶数2,a就输了。
-
所以如果游戏开始时Alice拿到N为奇数,那么她必输,也就是false。如果拿到N为偶数,她只用 -1,让bob 拿到奇数,最后bob必输,结果就是true。
class Solution: def divisorGame(self, N: int) -> bool: if N % 2 ==0: return True else: return False
方法二: 动态规划,N个时间阶段,计算么一个时间阶段的状态变量,后面的状态依赖前面的状态值
class Solution:
def divisorGame(self, N: int) -> bool:
f = [0 for i in range(N+1)]
f[1] =0
if N <= 1:
return 0
f[2] =1
for i in range(3,N+1):
for j in range(1,i//2): # 状态转移方法
if i %j ==0 and f[i-j]==0:
f[i] =1
break
return f[N] == 1
2. 区域和检索-数组不可变
给定一个整数数组 nums,求出数组从索引 i 到 j (i ≤ j) 范围内元素的总和,包含 i, j 两点。
示例:
给定 nums = [-2, 0, 3, -5, 2, -1],求和函数为 sumRange()
sumRange(0, 2) -> 1
sumRange(2, 5) -> -1
sumRange(0, 5) -> -3
说明:
你可以假设数组不可变。
会多次调用 sumRange 方法。
方法一:当然可以直接sum(nums[i:j+1]),但是没有体现动态规划的思想
方法二:利用动态规划的思想,提前先将nums里的每个元素换成相应前面所有元素之和,这样状态转移到最后会得到一系列的累计值,然后根据判断i大于零与否进行相减即可获得中间部分元素之和。
class NumArray:
def __init__(self, nums: List[int]):
self.nums = nums
for i in range(1, len(nums)):
self.nums[i] = self.nums[i]+self.nums[i-1] #计算每一个i位置的前面所有元素之和,并保存
def sumRange(self, i: int, j: int) -> int:
if i > 0:
return self.nums[j] -self.nums[i-1] # 计算I到j的元素之和,相减
else:
return self.nums[j]
3. 买股票的最佳时间
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。
注意你不能在买入股票前卖出股票。
示例 1:
输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。
示例 2:
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
思路:这是一个典型到的动态规划问题DP,前i天的最大收益=max{前i-1天的最大收益,第i天的价格减去前i-1天的最小价格}
1)先计算今天买入之前的最小值
2)今天之前最小值的买入,今天卖出的最大的最大收益,即为今天的获利
3)比较每天的最大获利,即max值即可。
class Solution:
def maxProfit(self, prices: List[int]) -> int:
min_p,max_p = 9999,0
for i in range(len(prices)):
min_p = min(min_p, prices[i]) # 最小值的买入值
max_p = max(max_p, prices[i]-min_p) # 比较之前的最大收益和今天卖出的收益
return max_p
4. 最大子序和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
进阶:
如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的分治法求解。
思路:这也是一个动态规划DP的问题,需要计算之前的最大子序和与加入当前元素之后的子序和的最大值。主义比较最大值返回结果
方法:1)定义一个函数f(n),以第n个元素为结束点的子数列的最大和,存在一个递推关系:
2) 将这些数保存下来,然后取最大值即为最大子数组和。【也可以遍历过程中逐一比较,然后返回最大值】
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
if not nums:
return NULL
res = nums[0]
f = -1
for i in range(len(nums)):
f = max(f+nums[i], nums[i]) # 第一步,计算当前节点是单独重新开始计算和,然后累积到前面的最大子序和中?
res = max(f, res)
return res
5. 跳台阶
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 1:
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
示例 2:
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶
方法一:找规律 累加原则:1 2 3 5 8 ……
class Solution:
def climbStairs(self, n: int) -> int:
a = 1
b = 1
for i in range(n):
a,b = b, a+b
return a
方法二: 这其实是一个动态规划,找到规律发现:这是一个斐波那契数列的规律。
class Solution:
def climbStairs(self, n: int) -> int:
i = 1 # 爬到1台阶仅有1种方法
j = 2 # 爬到2台阶有2种方法
for _ in range(3, n): # 自底向上递推 F(n)=F(n-1)+F(n-2)
i, j = j, i + j # 每次仅保留前两个值,依次往后推算
return i + j if n > 2 else n # 注意当n=1,n=2时的情况
6. 使用最小花费爬楼梯
数组的每个索引做为一个阶梯,第 i个阶梯对应着一个非负数的体力花费值 cost[i](索引从0开始)。
每当你爬上一个阶梯你都要花费对应的体力花费值,然后你可以选择继续爬一个阶梯或者爬两个阶梯。
您需要找到达到楼层顶部的最低花费。在开始时,你可以选择从索引为 0 或 1 的元素作为初始阶梯。
示例 1:
输入: cost = [10, 15, 20]
输出: 15
解释: 最低花费是从cost[1]开始,然后走两步即可到阶梯顶,一共花费15。
示例 2:
输入: cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1]
输出: 6
解释: 最低花费方式是从cost[0]开始,逐个经过那些1,跳过cost[3],一共花费6。
思路:
方法一:任仍然可以找规律 2^(n-1)
方法二:DP思想:到达当前台阶时判断下从前一个台阶过来节省体力,还是从前两个过来省体力,一直累加到最后一个台阶完,最小值就是最省体力的。 用p1和p2表示前两个和前一个台阶所耗费的体力,一遍循环就可以了。【DP思想在于:需要累计计算每一个台阶怎么省体力,是从前一个还是前两个进行跳】
class Solution:
def minCostClimbingStairs(self, cost: List[int]) -> int:
p1,p2 = 0, 0
for i in range(2, len(cost)+1):
p1,p2 =p2, min(p2 + cost[i-1], p1 + cost[i-2])
return p2
7. 打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
示例 1:
输入: [1,2,3,1]
输出: 4
解释: 偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入: [2,7,9,3,1]
输出: 12
解释: 偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。
思想: 这也是一个DP的问题。与最近小红书的第二道笔试题的笔记精选是类似的。这道题可以选择用动态规划的思想,遍历房屋金额的列表,计算当前是否需要进行打劫(上一次打劫是前两个房屋),还是当前房屋不进行打劫(上一个房屋一定被打劫),即:到第n个房屋时,打劫的最大金额数是f(n) = max(f(n-2)+num[n], f(n-1))
注:注意dp的代码是怎样写的,首先由两个0 0 的初始化,然后不断进行交替更新,其中一个不断进行变化!
class Solution:
def rob(self, nums: List[int]) -> int:
last = 0
now = 0
for i in nums:
last,now = now,max(last+i, now) # last不断记住上一个now,便于后续使用,now的更新计算DP思想
return now
8. 比特位计数
给定一个非负整数 num。对于 0 ≤ i ≤ num 范围中的每个数字 i ,计算其二进制数中的 1 的数目并将它们作为数组返回。
示例 1:
输入: 2
输出: [0,1,1]
示例 2:
输入: 5
输出: [0,1,1,2,1,2]
注 : 偶数二进制的末尾是0,奇数二进制的末尾是1,分析时:请写出前10个数的二进制表示:
思路:当写出前10个数的二进制表示时,发现:奇数的二进制中1个数是n//2中1的分数加1;偶数二进制中1个数是n//2中1的个数。因为:当n是偶数时,去掉的末尾的0后就是n//2【与n//2中1个数相同】,当n是奇数时,去掉末尾的1之后就是n//2【比N//2中1的个数多1】.
方法一:
class Solution:
def countBits(self, num: int) -> List[int]:
res = [0] * (num+1)
for i in range(1, len(res)):
if i %2 == 0: # 当i为偶数时,去掉的是最末尾的0,1的个数不会变
res[i] = res[i//2]
else: # 当i为奇数时,去掉的是最末尾的1,1的个数会减少,同理是在二分的子数上加1
res[i] = res[i//2] +1
return res
方法二:
i & (i - 1)可以去掉i最右边的一个1(如果有),因此 i & (i - 1)是比 i 小的,而且i & (i - 1)的1的个数已经在前面算过了,所以i的1的个数就是 i & (i - 1)的1的个数加上1。
public int[] countBits(int num) {
int[] res = new int[num + 1];
for(int i = 1;i<= num;i++){ //注意要从1开始,0不满足
res[i] = res[i & (i - 1)] + 1;
}
return res;
}
9. 石头游戏 【没懂】
亚历克斯和李用几堆石子在做游戏。偶数堆石子排成一行,每堆都有正整数颗石子 piles[i] 。
游戏以谁手中的石子最多来决出胜负。石子的总数是奇数,所以没有平局。
亚历克斯和李轮流进行,亚历克斯先开始。 每回合,玩家从行的开始或结束处取走整堆石头。 这种情况一直持续到没有更多的石子堆为止,此时手中石子最多的玩家获胜。
假设亚历克斯和李都发挥出最佳水平,当亚历克斯赢得比赛时返回 true ,当李赢得比赛时返回 false 。
示例:
输入:[5,3,4,5]
输出:true
解释:
亚历克斯先开始,只能拿前 5 颗或后 5 颗石子 。
假设他取了前 5 颗,这一行就变成了 [3,4,5] 。
如果李拿走前 3 颗,那么剩下的是 [4,5],亚历克斯拿走后 5 颗赢得 10 分。
如果李拿走后 5 颗,那么剩下的是 [3,4],亚历克斯拿走后 4 颗赢得 9 分。
这表明,取前 5 颗石子对亚历克斯来说是一个胜利的举动,所以我们返回 true 。
思想:
看很多人说这题可以直接return True。但应该不是的,[1,10,2]这一输入就会使得压输掉比赛。
但愧疚的是我没看懂这个题的解题思路。 石子游戏
class Solution:
def stoneGame(self, piles: List[int]) -> bool:
N=len(piles)
dp=[[0 for _ in range(N)] for _ in range(N)]#动态规划数组,实际上应用的是上三角矩阵,dp[i][j]表示从第i到j堆的得分情况
for i in range(N):#堆数为1时的得分
dp[i][i]=piles[i]
for l in range(1,N):#堆数从二遍历到N
for i in range(0,N-l):#起始位置从0遍历到N-l
dp[i][i+l]=max(piles[i]-dp[i+1][i+l],piles[i+l]-dp[i][i+l-1])
return dp[0][N-1]>0
10. 最小路径和
给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
示例:
输入:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
输出: 7
解释: 因为路径 1→3→1→1→1 的总和最小。
思路:该题是一个典型的动态规划建立表格到的方法,要计算n*m处的最小路径和,移动方向只能是向下或者向后,所以当前出要想获取最小的路径和,应当取左边和上边中的最小累积和+当前位置的值。
class Solution:
def minPathSum(self, grid: List[List[int]]) -> int:
row = len(grid)
col = len(grid[0])
dp =[[0 for _ in range(col)] for _ in range(row)] # 初始化空矩阵
for i in range(col):
dp[0][i] = dp[0][i-1] + grid[0][i] # 先放置第一行的元素,累计的结果
for i in range(1, row):
dp[i][0] = dp[i-1][0] + grid[i][0] # 从第二行起的每一行的第一个元素的计算更新
for j in range(1,col):
dp[i][j] = min(dp[i-1][j]+grid[i][j],dp[i][j-1]+grid[i][j]) # 计算左边和右边最小,并加上当前位置对当前元素进行更新。
return dp[row-1][col-1]
11.
给定一个整数 n,求以 1 ... n 为节点组成的二叉搜索树有多少种?
示例:
输入: 3
输出: 5
解释:
给定 n = 3, 一共有 5 种不同结构的二叉搜索树:
1 3 3 2 1
\ / / / \ \
3 2 1 1 3 2
/ / \ \
2 1 2 3
思路:假设n个节点的二叉排序树个数为G(n),令f(i)是以i为根节点的二叉搜素树个数,所以G(n)=f(1)+f(2)+……f(n)
以i为节点时,右子树节点有i-1个节点,右子树有n-i个节点,即:f(i) = G(i-1)*G(n-i)
则最终结果G(n) = G(0)*G(n-1)+ G(1)*G(n-2)+……+G(n-1)*G(0)
class Solution:
def numTrees(self, n: int) -> int:
dp = [0] * (n+1)
dp[0] = 1
dp[1] = 1
for i in range(2,n+1):
for j in range(1,i+1):
dp[i] += dp[j-1] * dp[i-j]
return dp[n]