问题一:Triangle
问题描述
给定一个三角形,找到从上到下的最小路径和,在每一步可以移动到下一行的相邻数字。
Bonus: 只使用$O(n)$额外空间,其中n是三角形的总行数
样例
[
[2],
[3,4],
[6,5,7],
[4,1,8,3]
]
输出11 (2 + 3 + 5 + 1 = 11).
问题分析
动态规划 时间$O(n2)$空间$O(1)$
点$(i,j)$的下一行的相邻数字是$(i+1,j)$和$(i+1,j+1)$。
$f(i,j)$表示从下往上走到位置$(i,j)$时的最小路径和,计算方式/状态转移方程是
$f(i,j)=minf(i+1,j),f(i+1,j+1)+(i,j)$
之所以要从下往上走的原因是因为最上面只有一个元素,就可以直接表示为我们的最终组织,如果是取最底下一行的话,还要进行判断。
复杂度分析:
直接把$f(i,j)$存在位置$(i,j)$处,不使用额外空间,因此空间复杂度为$O(1)$。
两层for loop,第一次竖着遍历,第二次横着遍历,时间复杂度为$O(n^2)$。
样例:
输入
[
[2],
[3,4],
[6,5,7],
[4,1,8,3]
]
更新后
[
[11],
[9,10],
[7,6,10],
[4,1,8,3]
]
C++代码
1 | class Solution { |
问题二:Unique Paths II
问题描述
是62题Unique Paths的进阶版,考虑在网格中加入了障碍,在网格图中,0该网格是空的,1表示该网格有障碍,例如
1 | [ |
这样一个网格,不能经过中间的格子,从左上角到右下角的路线数目为2.
样例
1 | 输入:网格的0 1矩阵 |
问题分析
(动态规划)$ O(mn)$
类似于62题Unique Paths,每一个网格都可以由该网格左边或上边的网格转移过来,因此到达某一点的路径数等于到达它上一点的路径数与它左边的路径数之和,不同的是,当某个网格有障碍时,到达该网格的路径数为0。这还是一个递推问题,考虑用动态规划。动态规划数组dp[i][j]
= 起点到点(i, j)的路径总数。于是我们就得到递推关系式:当网格为0时,dp[i][j] = dp[i][j-1] + dp[i-1][j]
;当网格为1(说明该网格是障碍物),dp[i][j]=0
。
C++代码
1 | class Solution { |
问题三:Russian Doll Envelopes
问题描述
你有一堆信封,并且已知它们的宽和长(w, h)
。如果一个信封的长和宽都小于另一个信封,那就可以将这个信封放到那个信封中。
现在以俄罗斯套娃的方式将这些信封装起来,问最多可以装几层?
样例
给定信封:[[5,4],[6,4],[6,7],[2,3]],
则最多可以套3层。
解释:[2,3] => [5,4] => [6,7]
问题分析
(动态规划) $O(n^2)$
先将所有信封按宽的长度从小到大排序,然后问题变成从左到右找一条最长的h
严格单调递增的子序列,同时满足w
也是严格单调递增的。
类似于最长上升序列问题,可以用动态规划解决。
状态表示:f[i]
表示以第i
个信封为结尾的单调序列的最大长度。
状态转移:对于f[i]
,枚举j=0∼i−1
,如果第 j
个信封的长和宽都小于第i
个信封,则用 f[j]+1
更新f[i]
。
时间复杂度分析:排序部分的时间复杂度是O(nlogn)
。动态规划部分一共有n
个状态,每个状态进行转移的计算量是O(n)
,所以动态规划的时间复杂度是$O(n^2)$。总时间复杂度是$O(n^2+nlogn)=O(n^2)$。
C++代码
1 | bool |
总结
在动态规划中一定要满足后面的状态可以从前面的状态中推得出来,这里也就是说状态的转移要具有顺序性
在这个题目中有条件j<i就可以满足顺序性,是由前面所有的状态递推此刻的状态,然后取极值。
问题四:Counting Bits
问题描述
给定一个非负整数$num$,对于所有的$i$,$0≤i≤num$,计算出$i$的二进制表示中1的个数。
进一步:
- 计算量是$O(nlogn)$的算法很简单,你能否想出计算量是$O(n)$ 的算法?
- 空间复杂度只能是$O(n)$;
- 不可以使用C++中的__builtin_popcount等内建函数;
样例
1 | 输入:5 |
问题分析
(动态规划) $O(n)$
令f[i]
表示 i
的二进制表示中1的个数。
则f[i]
可以由f[i/2]
转移过来,i
的二进制表示和⌊i/2⌋
的二进制表示除了最后一位都一样,所以f[i] = f[i/2] + (i&1)
;
时间复杂度分析:总共有n
个状态,每个状态进行转移的计算量是O(1)
,所以总时间复杂度是O(n)
。
C++代码
1 | class Solution { |
问题五:Longest Increasing Path in a Matrix
问题描述
给定一个整数矩阵,请找到最长的上升路径。
对于矩阵中的每个格子,你每次可以走到上下左右四个方向,不能斜着走,也不能走出边界。
样例1
1 | 输入:nums = |
样例2
1 | 输入:nums = |
问题分析
(记忆化搜索,动态规划)$ O(n2)$
这是动态规划里非常经典的一道题目,几乎是所有学编程的同学都会遇到的一道题目。
假设我们从最低点开始走,每次只能往更高的格子走。
状态表示:f[i][j]
表示走到(i,j)这个格子时的最大长度。
状态转移:枚举上下左右四个格子,如果某个格子(a,b)比当前格子低,则用该格子更新当前格子的最大长度:$f[i][j] = max(f[i][j], dp(a, b) + 1)$。
由于这道题目的状态依赖关系比较复杂,不容易用循环来求每个状态的值,所以可以用记忆化搜索来做:如果某个状态还没计算过,则递归计算该状态的值,如果某个状态计算过,那么直接返回该状态下的值。
时间复杂度分析:假设矩阵的边长是 $n$。则总共有$n^2$个状态,状态转移的计算量是常数,所以总时间复杂度是 $O(n2)$。
C++代码
1 | class Solution { |
问题六:Coin Change
问题描述
给定 n 种不同硬币的面值,以及需要凑出的总面值 total。请写一个函数,求最少需要多少硬币,可以凑出 total的钱。
如果不存在任何一种拼凑方案,则返回-1。
注意:
你可以假定所有硬币都有无限多个。
样例1
1 | 输入:coins = [1, 2, 5], amount = 11 |
样例2
1 | 输入:coins = [2], amount = 3 |
问题分析
(动态规划) $O(nm)$
完全背包问题。
相当于有$n$种物品,每种物品的体积是硬币面值,价值是1。问装满背包最少需要多少价值的物品?
状态表示:$f[i]$ 表示凑出$ i$价值的钱,最少需要多少个硬币。
第一层循环枚举不同硬币,第二层循环从大到小枚举所有价值(由于每种硬币有无限多个,所以要从小到大枚举),然后用第$ i$种硬币更新 $f[i]:f[i] = min(f[i], f[i - coins[i]] + 1)$。
时间复杂度分析:令$n$表示硬币种数,$m$表示总价钱,则总共两层循环,所以时间复杂度是$O(nm)$。
C++代码
1 | class Solution { |
问题七:Maximal Square
问题描述
在一个只包含0和1的二维矩阵中,找到最大的正方形,使得正方形只包含1,返回正方形的面积。
样例
1 | 输入:二维01矩阵,如 |
问题分析
(动态规划) $O(n^2)$
其实这道题可以是一个动态规划问题,用dp[i][j]
记录到达(i,j)
位置所能组成的最大正方形的边长。
我们首先来考虑边界情况,也就是当i或j为0的情况,那么在首行或者首列中,必定有一个方向长度为1,那么就无法组成长度超过1的正方形,最多能组成长度为1的正方形,条件是当前位置为1。
而对于递推公式,对于任意一点dp[i][j]
,由于该点是正方形的右下角,所以该点的右边,下边,右下边都不用考虑,关心的就是左边,上边,和左上边,只有当前(i, j)
位置为1,dp[i][j]
才有可能大于0,否则dp[i][j]
一定为0。当(i, j)
位置为1,此时要看dp[i-1][j-1], dp[i][j-1]
,和dp[i-1][j]
这三个位置,我们找其中最小的值,并加上1,就是dp[i][j]
的当前值了,这个并不难想,毕竟不能有0存在,所以只能取交集,最后再用dp[i][j]
的值来更新结果res的值即可。
时间复杂度分析:$O(n^2)$
C++代码
1 | class Solution { |
问题八:Out of Boundary Paths
问题描述
在一个m x n
的网格上有一个球,给定球的起点坐标(i, j)
,你每次可以将这个球移动到四个方向(上下左右)相邻的格子上或者移出网格边界。然而,你最多可以移动N
次。求出所有可以将球移出网格边界的路径数量。答案数可能很大,返回模$10^9+7$ 后的结果。
样例
1 | 输入: m = 2, n = 2, N = 2, i = 0, j = 0 |
解释:
输入: m = 1, n = 3, N = 3, i = 0, j = 1
输出: 12
注意
- 一旦将球移出网格边界,不可再移动回来。
- 网格长和宽在 [1, 50] 的范围内。
- N 在 [0, 50] 的范围内。
问题分析
(动态规划) $O(N⋅m⋅n)$
定义状态$f(k,x,y)$表示从网格边界格子经过$k$步,到达格子$(x, y)$的方案数。
初始时,每个位于边界的格子$(x, y)$,其$f(0,x,y)=1$;转移时,每个点可以从四个相邻的格子(如果存在)进行累加转移。
最终答案为,$f(0,i,j)+f(1,i,j)+…+f(N−1,i,j)$。由于规定边界的格子步数为 0,所以最多只能统计到 $N−1$步。
时间复杂度
状态数为$O(N⋅m⋅n)$,每个状态的转移数为$O(1)$,故总时间复杂度为$ O(N⋅m⋅n)$。
C++代码
1 | class Solution { |
问题九:Decode Ways
问题描述
一个只包含 A-Z 的消息可以用如下方式编码成数字:
1 | 'A' -> 1 |
给定一个只包含数字的非空字符串,返回共有多少种解码方案。
样例1
1 | 输入:"12" |
样例2
输入:"226"
输出:3
解释:它可以被解码成 "BZ" (2 26), "VF" (22 6),
或者 "BBF" (2 2 6)。
问题分析
(动态规划)$ O(n)$
这道题目可以用动态规划来做。
状态表示:$f[i]$表示前$ i$个数字共有多少种解码方式。
初始化:0个数字解码的方案数1,即$f[0]=1$。
状态转移:$f[i]$可以表示成如下两部分的和:
- 如果第 $i$个数字不是0,则 $i$个数字可以单独解码成一个字母,此时的方案数等于用前$i−1$个数字解码的方案数,即$ f[i−1]$;
- 如果第$i−1$个数字和第$ i$个数字组成的两位数在$10$ 到$26$ 之间,则可以将这两位数字解码成一个字符,此时的方案数等于用前$ i−2$数字解码的方案数,即$ f[i−2]$;
时间复杂度分析:状态数是$ n$个,状态转移的时间复杂度是 $O(1)$所以总时间复杂度是 $O(n)$。
C++代码
1 | class Solution { |
问题十:Ugly Number II
问题描述
找出第n大的“丑数”(ugly number),其中,丑数是指质因数只有2、3、5的正整数。
样例
1 | 输入:10 |
问题分析
(动态规划 指针)$O(n)$
由于丑数的因子也必定是丑数,它一定是某个丑数乘2、3、5得到的,因此我们可以采用动态规划的思想,利用前面已经得到的丑数序列来得到之后的丑数,而问题的关键在于如何确定状态转移方程。由于小的丑数乘5不一定比大的丑数乘2要小,我们没法直接使用目前最大的丑数来乘2、3、5顺序得到更大的丑数。不过,我们可以确定的是,小的丑陋数乘2,肯定小于大的丑陋数乘2。所以我们使用三个指针,分别记录乘2、3、5得出的目前最大丑陋数,而新的丑数就是这三个目前最大丑数中最小的那个,那么就需要更新被选中的丑数的指针,获得新的三个目前最大丑数,依次类推,从而得到最终结果。
时间复杂度分析:需要维护3个指针,从1到n遍历,复杂度为$O(n)$。
C++代码
1 | class Solution { |
问题十一:Distinct Subsequences
问题描述
给定字符串 $S$ 和 $T$,统计$S$中有多少个不同的子序列和$T$相等。
字符串的子序列是指:将原字符串删掉若干字符后,其余字符相对顺序不改变,所得到的新串(例如:”ACE”是”ABCDE”的子序列,而”AEC”不是)
样例1
1 | 输入:S = "rabbbit", T = "rabbit" |
样例2
1 | 输入:S = "babgbag", T = "bag" |
问题分析
(动态规划) $O(nm)$
可以换一种考虑问题的方式:用中的字符,按顺序匹配$T$中的字符,问有多少种方式可以匹配完$T$中的所有字符。
可以用动态规划来做:f[i][j]
表示用$S$的前$i$个字符,能匹配完$T$的前$j$ 个字符的方案数。
初始化:因为$S$可以从任意一个字符开始匹配,所以f[i][0]=1,∀i∈[0,len(S)]
。
状态转移:
- 如果
S[i−1]≠T[j−1]
,则S[i−1]
不能匹配T[j−1]
,所以f[i][j]=f[i−1][j]
;
- 如果
S[i−1]=T[j−1]
,则S[i−1]
既可以匹配T[j−1]
,也可以不匹配T[j−1]
,所以f[i][j]=f[i−1][j]+f[i−1][j−1]
;
边界情况:如果T为空串S不为空串,那么结果为1,如果T为空串S也为空串,那么结果也为1。
时间复杂度分析:假设 $S$的长度是 $n$ ,$T$的长度是 $m$,则共有$nm$个状态,状态转移的复杂度是 $O(1)$,所以总时间复杂度是$ O(nm)$
C++代码
1 | class Solution { |
问题十二:Palindrome Partitioning II
问题描述
给定一个字符串s
,请将它划分成若干部分,使得每一部分都是回文串。
求最少需要切几刀。
样例
1 | 输入:"aab" |
问题分析
(动态规划) $O(n^2)$
一共进行两次动态规划。
第一次动规:计算出每个子串是否是回文串。
状态表示:st[i][j]
表示 s[i…j]
是否是回文串;
转移方程:s[i…j]
是回文串当且仅当 s[i]
等于s[j]
并且 s[i+1…j−1]
是回文串;
边界情况:如果s[i…j]
的长度小于等于2,则st[i][j]=(s[i]==s[j])
;
在第一次动规的基础上,我们进行第二次动规。
状态表示:$f[i]$ 表示把前 $i$个字符划分成回文串,最少划分成几部分;
状态转移:枚举最后一段回文串的起点$j$,然后利用$ st[j][i]$可知 $s[j…i]$是否是回文串,如果是回文串,则$f[i] $可以从$ f[j−1]+1$ 转移;
边界情况:0个字符可以划分成0部分,所以 $f[0]=0$。
题目让我们求最少切几刀,所以答案是 $f[n]−1$。
时间复杂度分析:两次动规都是两重循环,所以时间复杂度是 $O(n^2)$。
C++代码
1 | class Solution { |
问题十三: Predict the Winner
问题描述
给定一个非负整数的数组代表得分。有两个玩家A和B,玩家 A 先开始玩,可以从数组两端取出一个数作为自己的得分,然后玩家 B 接着从数组两端取出一个数作为自己的得分,再然后是玩家 A ,以此类推。每一次一个玩家取出一个数后,这个数就会消失。交替游戏知道所有的数字都被取走,得分高的玩家获胜。
给定一个数组,预测玩家 A 是否能获胜。假设两个玩家都采用最优策略。
样例
1 | Input: [1, 5, 2] |
1 | Input: [1, 5, 233, 7] |
注意
- 1 <= 数组长度 <= 20。
- 数组中的非负整数不超过 10^7。
- 如果最后两个玩家得分相同,则玩家 A 也是赢家。
问题分析
(动态规划) $O(n2)$
从最简单的问题开始考虑,假设只有一个数字,则只能玩家 A 选择这个数字。
接着,问题的规模开始扩大,扩大后,两个玩家会有两种决策,一种是选择数组头部,一种是选择数组尾部,而这两种情况下的子问题都可以提前计算出。至此,动态规划的思路已经很明显。
令 f(i,j)
表示闭区间[i,j][i,j]
下玩家 A 所能获得的最大分数。
每次 f(i,j) 转移有两种情况:
- 当这一次是玩家 A 取数时,
f(i,j)=max(f(i+1,j)+nums[i],f(i,j−1)+nums[j])
表示从头部取或者从尾部取,二者最优; - 当这一次是玩家 B 取数时,玩家B肯定希望自己的得分最大,这必然会导致玩家 A 的得分变小,故此时
f(i,j)=min(f(i+1,j),f(i,j−1))
。
初始时,若最后一次是玩家 A 取数,则f(i,i)=nums[i]
;否则f(i,i)=0
。
最后玩家 A 能获得的最大得分就是f(0,n−1)
。
C++代码
1 | class Solution { |