leetcode 动态规划(一)

目录

338. 比特位计数

 面试题47. 礼物的最大价值

1277. 统计全为 1 的正方形子矩阵

1227. 飞机座位分配概率


338. 比特位计数

https://leetcode-cn.com/problems/counting-bits/submissions/

给定一个非负整数 num。对于 0 ≤ i ≤ num 范围中的每个数字 i ,计算其二进制数中的 1 的数目并将它们作为数组返回。

示例 1:    输入: 2    输出: [0,1,1]
示例 2:   输入: 5    输出: [0,1,1,2,1,2]
进阶:给出时间复杂度为O(n*sizeof(integer))的解答非常容易。但你可以在线性时间O(n)内用一趟扫描做到吗?要求算法的空间复杂度为O(n)。你能进一步完善解法吗?要求在C++或任何其他语言中不使用任何内置函数(如 C++ 中的 __builtin_popcount)来执行此操作。

思路

方法一:先扫描一遍,将2的整数幂置为1,2的整数幂之间的数字可由该整数幂及其前面的数字相加得到,则1的位数也是二者之和,例如4-8之间的5可以由4+1得到,1的位数是2。

class Solution(object):
    def countBits(self, num):
        """
        :type num: int
        :rtype: List[int]
        """
        if num == 0:
            return [0]
        res = [-1] * (num + 1)
        res[0] = 0

        i = 1
        # 先扫描一遍,将2的整数幂置为1
        while i <= num:
            res[i] = 1
            i = 2 * i
        # pre用来表示i前最接近i的一个2的整数幂
        pre, j = 0, 1
        for i in range(1, num + 1):
            if res[i] == 1:
                pre, j = i, 1
            else:
                # 2的整数幂之间的数字可由该整数幂及其前面的数字相加得到
                res[i] = res[pre] + res[j]
                j += 1

        return res

方法一的仅扫描一遍的版本,https://leetcode-cn.com/problems/counting-bits/solution/bi-te-wei-ji-shu-by-leetcode/ 的方法二,动态规划+最高有效位,例如8-16之间的15,与7只差一个8。我们可以使用 [0,7] 作为蓝本来得到 [8,15]。

class Solution(object):
    def countBits(self, num):
        res = [0] * (num + 1)
        if num == 0:
            return [0]
        i, res[0] = 1, 0
        while i <= num:
            res[i] = 1
            last_idx = min(2 * i, num) 
            for j in range(i + 1, last_idx + 1):
                res[j] = res[j - i] + 1
            i *= 2
        return res

方法二:动态规划+最低有效位,观察x 和 x′=x/2 的关系:

x=(1001011101)2 =(605) 10

x ′ =(100101110) 2 =(302) 10
​可以发现 x ′与x 只有一位不同,这是因为x ′可以看做 x 移除最低有效位的结果。

这样,我们就有了下面的状态转移函数:P(x)=P(x/2)+(xmod2)

class Solution(object):
    def countBits(self, num):
        res = [0] * (num + 1)

        for i in range(0, num + 1):
            res[i] = res[i // 2] + (i % 2)

        return res

 面试题47. 礼物的最大价值

https://leetcode-cn.com/problems/li-wu-de-zui-da-jie-zhi-lcof/

在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?

示例 1:输入: 
[
  [1,3,1],
  [1,5,1],
  [4,2,1]
]
输出: 12,解释: 路径 1→3→5→2→1 可以拿到最多价值的礼物
提示:0 < grid.length <= 200。0 < grid[0].length <= 200

思路

一:动态规划二维DP,转移方程res[i][j] = max(res[i - 1][j], res[i][j - 1]) + grid[i][j],其上边的和左边的取最大加上该值即可。

class Solution(object):
    def maxValue(self, grid):
        """
        :type grid: List[List[int]]
        :rtype: int
        """
        if not grid or not grid[0]:
            return 0

        m, n = len(grid), len(grid[0])
        res = [[0] * n for _ in range(m)]
        res[0][0] = grid[0][0]
        for i in range(1, m):
            res[i][0] = res[i - 1][0] + grid[i][0]
        for j in range(1, n):
            res[0][j] = res[0][j - 1] + grid[0][j]    

        for i in range(1, m):
            for j in range(1, n):
                res[i][j] = max(res[i - 1][j], res[i][j - 1]) + grid[i][j]
            
        return res[m - 1][n - 1]

二:对一进行空间上的优化。res[i][j] = max(res[i - 1][j], res[i][j - 1]) + grid[i][j],res[i - 1][j]是上一行的值,当只有一行时,则是未更新的时候的值,即此处的res[j],res[i][j - 1]是该行已更新的左侧的值,当只有一行时,则亦是已更新的左侧的值,即此处的res[j-1]。

class Solution(object):
    def maxValue(self, grid):
        if not grid or not grid[0]:
            return 0

        m, n = len(grid), len(grid[0])
        res = [0] * n 

        for i in range(0, m):
            res[0] += grid[i][0]
            for j in range(1, n):
                res[j] = max(res[j], res[j - 1]) + grid[i][j]
            
        return res[n - 1]

1277. 统计全为 1 的正方形子矩阵

https://leetcode-cn.com/problems/count-square-submatrices-with-all-ones/

给你一个 m * n 的矩阵,矩阵中的元素不是 0 就是 1,请你统计并返回其中完全由 1 组成的 正方形 子矩阵的个数。

示例 1:输入:matrix =[[0,1,1,1], [1,1,1,1], [0,1,1,1]]
输出:15,解释: 边长为 1 的正方形有 10 个。边长为 2 的正方形有 4 个。边长为 3 的正方形有 1 个。正方形的总数 = 10 + 4 + 1 = 15.
示例 2:输入:matrix = [[1,0,1],[1,1,0],[1,1,0]]
输出:7,解释:边长为 1 的正方形有 6 个。 边长为 2 的正方形有 1 个。正方形的总数 = 6 + 1 = 7。

提示:1 <= arr.length <= 300,1 <= arr[0].length <= 300,0 <= arr[i][j] <= 1

思路

一:暴力法,对每一个下标(i,j)的元素若为1,则开始统计以该下标为右下角的正方形的个数(判断是正方形,对每一个可能的边长,看其区域中是否均为1,若是,则标为正方形,个数加一;否则退出循环,判断下一个下标)。

class Solution(object):
    def countSquares(self, matrix):
        """
        :type matrix: List[List[int]]
        :rtype: int
        """
        m, n = len(matrix), len(matrix[0])
        res, cnt = [[0] * n for _ in range(m)], 0

        for i in range(m):
            for j in range(n):
                if matrix[i][j] != 1:
                    continue
                length = min(i, j) + 1
                for l in range(0, length):
                    flag = True
                    for x in range(i - l, i + 1):
                        if not flag:
                            break
                        for y in range(j - l, j + 1):
                            if matrix[x][y] != 1:
                                flag = False
                                break
                    if flag:
                        res[i][j] += 1
                    else:
                        break
                cnt += res[i][j] 
        return cnt

二:对一进行改进,改进判断正方形的方法(先用sum_rec对matrix求和,其中sum_rec[i][j],表示一i,j为下标的左上角的和,只要该区域的面积与其中1的个数相同,则是一个正方形)。

class Solution(object):
    def countSquares(self, matrix):
        m, n = len(matrix), len(matrix[0])
        sum_rec, res, cnt = [[0] * n for _ in range(m)], [[0] * n for _ in range(m)], 0
        sum_rec[0][0] = matrix[0][0]

        for i in range(1, m):
            sum_rec[i][0] = sum_rec[i - 1][0] + matrix[i][0]
        for j in range(1, n):
            sum_rec[0][j] = sum_rec[0][j - 1] + matrix[0][j]

        for i in range(1, m):
            for j in range(1, n):
                sum_rec[i][j] = sum_rec[i - 1][j] + sum_rec[i][j - 1] - sum_rec[i- 1][j - 1] + matrix[i][j]

        for i in range(m):
            for j in range(n):
                if matrix[i][j] != 1:
                    continue
                # res[i][j] = 1
                length = min(i + 1, j + 1) 
                for l in range(0, length):
                    x, y = i - l, j - l
                    area = (l + 1) ** 2
                    num_1 = sum_rec[i][j]
                    if x - 1 >= 0:
                        num_1 -= sum_rec[x - 1][j] 
                    if y - 1 >= 0:
                        num_1 -= sum_rec[i][y - 1]
                        if x - 1 >= 0:
                            num_1 +=  sum_rec[x - 1][y - 1]
                    if area == num_1:
                        res[i][j] += 1
                    else:
                        break
                cnt += res[i][j]
        return cnt       

三:动态规划,转自https://leetcode-cn.com/problems/count-square-submatrices-with-all-ones/solution/tong-ji-quan-wei-1-de-zheng-fang-xing-zi-ju-zhen-f/,首先,暴力解就是以矩阵每一个点为起点,依次判断边长为1,2,3,...,min(矩阵长, 矩阵宽)的区域是否是正方形,显然复杂度是过不了。很容易知道,上述过程在判断较大区域是否为正方形的时候,并没有用到前面计算的结果,每一次判断都从头开始。这也是复杂度过高的原因。

那么怎么利用之前判断过的结果呢?举个例子,比如我要判断以(2, 3)为右下角边长为3的正方形区域(红色边框区域)是否是全为1:

image.png

先判断(i, j)位置是否为1,如果否,则显然不满足;如果是,进行下一步判断
判断分别以(i - 1, j), (i - 1, j - 1), (i, j - 1)为右下角的区域是否能构成边长为2的正方形,如果能,那就满足条件。

基于上述,我们可以看出思路大致跟最大正方形那题差不多,设dp[i][j][k]表示以(i, j)为右下角,边长为k的正方形区域是否全为1,那么易得出如下状态转移方程:dp[i][j][k] = (matrix[i][j] == 1 \&\& dp[i - 1][j][k - 1] \&\& dp[i][j - 1][k - 1] \&\& dp[i - 1][j - 1] [k - 1])

class Solution(object):
    def countSquares(self, matrix):
        m, n = len(matrix), len(matrix[0])
        
        rec, res = [[0] * n for _ in range(m)], 0
        
        for i in range(m):
            rec[i][0] = matrix[i][0]
            res += matrix[i][0]
        
        for j in range(1, n):
            rec[0][j] = matrix[0][j]
            res += matrix[0][j]
        
        for i in range(1, m):
            for j in range(1, n):
                if matrix[i][j] != 1:
                    continue 
                rec[i][j] = min([rec[i][j - 1], rec[i - 1][j], rec[i - 1][j - 1]]) + 1
                res += rec[i][j]
        return res 

1227. 飞机座位分配概率

https://leetcode-cn.com/problems/airplane-seat-assignment-probability/

有 n 位乘客即将登机,飞机正好有 n 个座位。第一位乘客的票丢了,他随便选了一个座位坐下。剩下的乘客将会:如果他们自己的座位还空着,就坐到自己的座位上,当他们自己的座位被占用时,随机选择其他座位第 n 位乘客坐在自己的座位上的概率是多少?

示例 1:输入:n = 1,输出:1.00000,解释:第一个人只会坐在自己的位置上。
示例 2:输入: n = 2,输出: 0.50000,解释:在第一个人选好座位坐下后,第二个人坐在自己的座位上的概率是 0.5。

提示:1 <= n <= 10^5

思路

一:参考各路题解,该题定位是数学+脑筋急转弯+动态规划。

先分析第一个乘客的可能选择

  1. 选择正确的位置(即自己的位置),其概率是1/n,则后面所有乘客均会坐在正确的位置,在第一个选择正确的条件下,后面所有的均会正确(即此条件下,概率为1),自然第 n 位乘客必定坐在自己的座位上。(1/n * 1.0)
  2. 选择坐在第n个乘客的位置上,其概率是1/n,则无论如何第n个乘客均坐不到自己座位(即此条件下,概率为0.0),(1/n * 0.0)
  3. 其在剩余的n-2个座位中的一个,设其坐在第K个乘客的位置上,那么会造成第K个乘客不能坐在自己的位置上,但是第2至K-1个乘客均是坐在自己的位置上。此时相当于求解一个子问题,而且前K-1个人的选择不会对后续结果产生影响,这个字问题就是,将第K个乘客当作新的第一个乘客,能这么做的原因是因为他和第一位选择情况完全一样,不过他的正确的座位(可以理解为不影响别人的座位)是第一个乘客的座位,翻译一下原题,“有 n-K+1 位乘客即将登机,飞机正好有 n -K+1个座位。第一(原来的第K)位乘客的票丢了,他随便选了一个座位坐下。剩下的乘客将会:如果他们自己的座位还空着,就坐到自己的座位上,当他们自己的座位被占用时,随机选择其他座位最后一位乘客坐在自己的座位上的概率是多少?”

用列表dp来存放概率,其中dp[i]-表示n=i,即有i名乘客,最后一名乘客座位正确的概率。

dp[1] = 1.0

dp[2] = 1/2 * 1 + 1/2 * 0 = 0.5

dp[3] = 1/3 * 1 + 1/3 * 0 + 1/3 * dp[2] = 0.5

dp[4] = 1/4 * 1 + 1/4 * 0 + 1/4 * dp[3] + 1/4 * dp[2]= 0.5

(1/4 * 1,第一个乘客坐正确的情况;1/4 * 0,第一个乘客坐在第四个乘客的位置,则不可能正确;1/4 * dp[3],第一个乘客坐在第二个乘客的位置,则字问题规模为3,故有dp[3]; 1/4 * dp[3],第一个乘客坐在第三个乘客的位置,则字问题规模为2,故有dp[2])

递推下去我们发现当n>=2时,概率一直为0.5。

class Solution(object):
    def nthPersonGetsNthSeat(self, n):
        """
        :type n: int
        :rtype: float
        """
        if n == 1:
            return 1.0
        return 0.5

300. 最长上升子序列

https://leetcode-cn.com/problems/longest-increasing-subsequence/

给定一个无序的整数数组,找到其中最长上升子序列的长度。

示例:输入: [10,9,2,5,3,7,101,18],输出: 4 ,解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。你算法的时间复杂度应该为 O(n2) 。
进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?

思路

一:动态规划,dp[i]-表示以下标为i的元素结尾的最长上升子序列的长度,每次都要遍历i之前的元素,看看是否能接在其后面。

class Solution(object):
    def lengthOfLIS(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        if not nums:
            return 0

        dp = [1] * len(nums)

        for i in range(len(nums)):
            for j in range(i):
                if nums[i] > nums[j]:
                    dp[i] = max(dp[i], dp[j] + 1)
        return max(dp)

二:贪心+二分查找,转自官方题解https://leetcode-cn.com/problems/longest-increasing-subsequence/solution/zui-chang-shang-sheng-zi-xu-lie-by-leetcode-soluti/,考虑一个简单的贪心,如果我们要使上升子序列尽可能的长,则我们需要让序列上升得尽可能慢,因此我们希望每次在上升子序列最后加上的那个数尽可能的小。

基于上面的贪心思路,我们维护一个数组 res[i] ,表示长度为 i 的最长上升子序列的末尾元素的最小值,用 len 记录目前最长上升子序列的长度,起始时 len 为 1,res[0]=nums[0]。同时我们可以注意到 res[i] 是关于 i 单调递增的。因为如果 res[j]≥res[i] 且 j<i,我们考虑从长度为 i 的最长上升子序列的末尾删除 i−j 个元素,那么这个序列长度变为 j ,且第 j 个元素 x(末尾元素)必然小于 res[i],也就小于 res[j]。那么我们就找到了一个长度为 j 的最长上升子序列,并且末尾元素比 res[j] 小,从而产生了矛盾。因此数组 d[] 的单调性得证。

我们依次遍历数组 nums[] 中的每个元素,并更新数组res[] 和 len 的值。如果 nums[i]>res[len] 则更新 len=len+1,否则在 res[1…len]中找满足 res[i−1]<nums[j]<res[i] 的下标 i,并更新 res[i]=nums[j]。根据 d数组的单调性,我们可以使用二分查找寻找下标 i,优化时间复杂度。

最后整个算法流程为:

设当前已求出的最长上升子序列的长度为 len(初始时为 1),从前往后遍历数组 nums,在遍历到 nums[i] 时:

如果 nums[i]>res[len] ,则直接加入到 res 数组末尾,并更新 len=len+1;

否则,在 res 数组中二分查找,找到第一个比 nums[i] 小的数 res[k] ,并更新 res[k+1]=nums[i]。

以输入序列 [0,8,4,12,2] 为例:

第一步插入 0,res=[0];

第二步插入 8,res=[0,8];

第三步插入 4,res=[0,4];

第四步插入 12,res=[0,4,12];

第五步插入 2,res=[0,2,12]。

最终得到最大递增子序列长度为 3。

class Solution(object):
    def lengthOfLIS(self, nums):
        if not nums:
            return 0

        res, cnt = [nums[0]], 0
        for i in range(1, len(nums)):
            if nums[i] > res[-1]:
                res.append(nums[i])
            else:
                # [l,r]
                l, r, target = 0, len(res) -1, nums[i]
                loc = -1
                # 找到第一个比nums[i]大的替换掉
                while l <= r:
                    mid = l + (r - l) // 2
                    if target <= res[mid]:
                        loc = mid
                        r = mid - 1
                    else:
                        l = mid + 1
                res[loc] = target             
        return len(res)
发布了46 篇原创文章 · 获赞 1 · 访问量 5039

猜你喜欢

转载自blog.csdn.net/qq_xuanshuang/article/details/105023133