代码随想录(二)

(Python)

代码均可在Leetcode上AC

由于是复习考研机试,所以不会太深入,比如回溯的去重问题就没有涉及。

1.回溯理论基础

1.1什么是回溯法

        回溯法又可以叫做回溯搜索法,是一种搜索的方式。回溯与递归是好兄弟,有回溯就有递归。回溯函数与递归函数一般都是一回事。回溯法的效率其实并不太好,本质上仍然是暴力穷举算法,但是可以通过一些独到的剪枝方法,从而提高效率。

1.2回溯能够解决的问题

        一般我们看到一些关键的字眼,我们就要想起用回溯。比如组合问题、切割问题、子集问题、排列问题、棋盘问题(N皇后、解数独)。

1.3回溯法模板

        回溯法所解决的问题都可以抽象为树形结构,由于回溯法一般解决的都是在递归中查找子集的问题,此时集合的大小就构成了树的宽度,递归的深度,都构成的树的深度。回溯法的主要特点为:一定会有终止条件,然后会在return回去时撤回进行递归前加入的那个状态。这一特点就是回溯算法与普通递归的区别。

        回溯法模板:

def backtracking(参数):
    if 终止条件:
        存放结果
        return
    for 选择 in 本层集合中元素:
        处理节点
        backtracking(路径,选择列表) // 递归
        回溯,撤销处理结果

        for循环是对同一层元素的遍历,递归是纵向遍历。

2.回溯思想具体题目应用

2.177. 组合 - 力扣(LeetCode)

        这道题就是让你求可能的组合,套模板!

        我认为是用一个res_1存放单次递归的结果,用一个res_2存放总体结果。当然这两个list都是全局变量。然后退出条件为len(res_1) == k。for循环内主要是将当前节点加入res_1中,然后递归下一个节点,然后进行撤销操作。具体代码如下:

        注意python将一个列表加入另外一个列表必须要列表名[:]的方式。

def func(n, k, startindex, res_1, res):
    if len(res_1) == k:
        res.append(res_1[:])
        return
    for i in range(startindex, n + 1):
        res_1.append(i)
        func(n, k, i + 1, res_1, res)
        res_1.pop()

def solution(n, k):
    res_1 = []
    res = []
    func(n, k, 1, res_1, res)
    return res

if __name__ == "__main__":
    n = eval(input())
    k = eval(input())
    print(solution(n, k))

        代码随想录给出了一个剪枝的方法,就是说如果我的for循环的开始位置到n以及不足k个了,那我就没必要遍历下去了,因为已经无法凑足k个了。

def func(n, k, startindex, res_1, res):
    if len(res_1) == k:
        res.append(res_1[:])
        return
    for i in range(startindex, n - (k - len(res_1)) + 2):
        res_1.append(i)
        func(n, k, i + 1, res_1, res)
        res_1.pop()

def solution(n, k):
    res_1 = []
    res = []
    func(n, k, 1, res_1, res)
    return res

if __name__ == "__main__":
    n = eval(input())
    k = eval(input())
    print(solution(n, k))

2.2216. 组合总和 III - 力扣(LeetCode):

        不会写,始终想不到递归的终止条件。

        看代码随想录的代码恍然大悟,而且我忘记每个res的长度一定为k,所以就没写出。如果没有k长度的限制,则只要判断cur的总和是否等于该值。是从1-9选k个,不是从1-k选数字去观察和是否等于n。

def func1(n, k, res_1, res, start_inx,):
    if len(res_1) == k:
        res.append(res_1[:])
        return
    for i in range(start_inx, 10):
        res_1.append(i)
        func1(n, k, res_1, res, i + 1)
        res_1.pop()
class Solution:
    def combinationSum3(self, k: int, n: int) -> List[List[int]]:
        res_1 = []
        res = []
        func1(n, k, res_1, res, 1)
        res_1.clear()
        for i in range(len(res)):
            if sum(res[i]) == n:
                res_1.append(res[i])
        return res_1

        我是先穷举,穷举完一个一个遍历,是否sum等于n,虽然这样很慢,但是也能得出结果。毕竟k是2-9。如果k会很大的话,就必须要用随想录的办法。

        即用一个cur_sum记录当前sum,和res_1同时进行"出入栈"操作,对于cur_sum是加减法

def func1(n, k, res_1, res, start_inx,cur_sum):
    if cur_sum > n:
        return
    if len(res_1) == k:
        if cur_sum == n:
            res.append(res_1[:])
        return
    for i in range(start_inx, 10):
        res_1.append(i)
        cur_sum += i
        func1(n, k, res_1, res, i + 1, cur_sum)
        cur_sum -= i
        res_1.pop()

def solution(n, k):
    res_1 = []
    res = []
    func1(n, k, res_1, res, 1, 0)
    return res

2.3分割问题:分割回文串: 

        这题需要判断加入的字符串是否为回文串。所以我们需要额外设置一个函数去判断加入res_1的字符是否为回文串。与组合问题的差别就是,在横向遍历时我们需要入栈的是一个回文串,而不是随意的单个字符。为什么不在退出递归的时候判断呢?这是因为如果你在退出递归时遍历res_1,所需的时间复杂度要高一点,如果题目的s长度为15、16,这样遍历起来的代码也比较复杂,所以我们需要在加入res_1之前就要判断是否为回文串。

def func1(s, res_1, res, startinx):
    if startinx == len(s):
        res.append(res_1[:])
        return
    for i in range(startinx, len(s)):
        if func2(startinx, i, s):
            res_1.append(s[startinx: i + 1])
            func1(s, res_1, res, i + 1)
            res_1.pop()
def func2(startinx, i, s):
    left,right = startinx,i
    while left <= right:
        if s[left] != s[right]:
            return False
        left += 1
        right -= 1
    return True
def solution(s):
    res_1 = []
    res = []
    func1(s, res_1, res, 0)
    return res
if __name__ == "__main__":
    s = input()
    print(solution(s))

2.4子集问题:78. 子集 - 力扣(LeetCode)

        这题空集也是计算在里面的,单个元素也是包含在里面的。所以其实和前面的组合问题没有多大区别,但是要找对递归结束条件,想半天实在想不出来,看了随想录的思路,对啊,其实可以不要退出递归条件的,因为求子集实际上是在遍历整个树结构。分割问题与组合问题实际上实在收集树的某一部分路径,而子集问题是在搜集树的每一个路径。

def func1(nums, res_1, res, startinx):
    res.append(res_1[:])
    for i in range(startinx, len(nums)):
        res_1.append(nums[i])
        func1(nums, res_1, res, i + 1)
        res_1.pop()
def solution(nums):
    res_1 = []
    res = []
    func1(nums, res_1, res, 0)
    return res
if __name__ == "__main__":
    nums = list(map(int, input().strip().split()))
    print(solution(nums))

2.5全排列:46. 全排列 - 力扣(LeetCode)

        什么是全排列呢,就是A_{N}^{N}

        就是在N个里面取N个,有多少种可能,这个题我们可以认为递归条件就是len(res_1)==len(nums),是因为只有当取到了N个数,我们才能退出循环。只用将for循环的start改成0就好了。这一点比较难理解,我们只需要知道for需要是控制同一层元素的遍历,我们就可以很轻松的理解该变化。比如对于N=3,第一次我选择1,然后第二次我只能选择2和3,如果我选择3,难道2就不能选了吗。对于全排列来说,是肯定可以的,所以需要使得for循环从0开始。但是这样子肯定也会出现问题,就是重复元素的加入,所以此时我们需要一个标记数组来判断是否当前元素已经在res_1中。

def func1(nums, res_1, res, visited):
    if len(res_1) == len(nums):
        res.append(res_1[:])
    for i in range(0, len(nums)):
        if visited[i]:
            continue
        visited[i] = True
        res_1.append(nums[i])
        func1(nums, res_1, res, visited)
        visited[i] = False
        res_1.pop()
def solution(nums):
    res_1 = []
    res = []
    visited = [False] * len(nums)
    func1(nums, res_1, res, visited)
    return res
if __name__ == "__main__":
    nums = list(map(int, input().strip().split()))
    print(solution(nums))

3.贪心理论基础

3.1什么是贪心

        贪心本质是选择每一阶段的局部最优,从而达到全局最优。比如有一堆钞票、想取走最大的金额,应该怎么拿?肯定是每次拿走最大数额的钱,每次拿走最大的就是局部最优,从而全局最优

3.2贪心的使用时机以及套路

        基本上没有固定的使用时机,也没有固定的题型。不像回溯思想一样,看到什么分割、组合、排列等字眼就明白要用回溯。贪心没有提示语,只有通过对题目进行深入理解后,才能决定是否要用贪心。

        也没有套路,这是因为贪心的题目类型太繁杂。只有题目难度之分,没有题目类型之分。如何找到局部最优到整体最优呢?也没有,是能去尝试,发现贪心可以那么就用贪心,如果不行可能就是动态规划了。

        比较严谨的方式是去进行数学证明,但就像carl说的一样,大部分时候贪心都是常识性推导,如果真要证明,在机试或者面试中是来不及的!

3.3贪心解题步骤

        一句话如何推导出局部最优从而推出全局最优。

4.贪心思想题目具体应用:

4.1455. 分发饼干 - 力扣(LeetCode)

        我们的局部最优是:尽量用较小的饼干尺寸满足胃口小的孩子,从而达到全局最优的目标(满足数量越多的孩子)。

        如何实现尽量用较小的饼干尺寸满足胃口小的孩子呢,就是将g胃口值和s饼干升序排序,然后用s饼干尺寸去一个一个遍历就好了。

class Solution:
    def findContentChildren(self, g: List[int], s: List[int]) -> int:
        max_num = 0
        g.sort(key=None)
        s.sort(key=None)
        i, j = 0, 0
        while i <= len(g) - 1 and j <= len(s) - 1:
            if s[j] < g[i]:
                j += 1
            elif s[j] >= g[i]:
                i += 1
                j += 1
                max_num += 1
        return max_num

        随想录中提到用大胃口满足大的,也是一种好思路,代码和上面思想一样的,就不贴出来了。

4.2376. 摆动序列 - 力扣(LeetCode) :

        这道题是一道中等题,我的想法是直接把每两个元素的差值求出来,然后判断是否为正负正负循环....的顺序,返回最长的长度就好了。果不其然,有问题!

        随想录的方法为:

        局部最优:删除单调坡度上的节点(不包括单调坡度两端的节点),那么这个坡度就可以有两个局部峰值。

        整体最优:整个序列有最多的局部峰值,从而达到最长摆动序列。

       但由于是返回长度而不是序列,并不用删除数组元素。我们设置一个curdiff和prediff,不断更新curdiff,只要prediff<=0且curdiff>=0或者prediff>=0且curdiff<0就更新prediff=curdiff,然后将res加1。其实还挺好理解,相比于我的代码多了一个pre == 0的情况,为什么prediff可以等于0呢,这是因为prediff指的是上一次的差值,若给出的序列是[0,0,0],此时的结果是2,如果prediff不等于0,则res一定是为0的,肯定就ac不了代码。prediff理解为上一次符合题意的两个数的差值,curdiff理解为当前两个数的差值,curdiff自然是不能为0的,为0就不符题意了,但是prediff可以为0,因为对于最开始的两个数而言,相减是可以为0的。prediff=curdiff的意思是,遇到一个符合题意的序列,必须要更新prediff,相当于删除中间不符合题意得序列。当然res为1,默认最开始的两个数字一定是摆动序列。

        res = 1
        prediff = 0
        for i in range(len(nums) - 1):
            curdiff = nums[i + 1] - nums[i]
            if (prediff >= 0 and curdiff < 0) or (prediff <= 0 and curdiff > 0):
                prediff = curdiff
                res += 1
        return res

        总之体验了一下贪心,太难了要做很多很多道题才能说有机会写出没见过的题目。

5.DP理论基础:

5.1什么是动态规划

        DP全称Dynamic Programming,即动态规划。动态规划也是用于求解最优解问题,不过DP的一个状态一定是由上一个状态推导出来的,而不是像贪心的单纯局部选最优的思路。

        例如:有N件物品和一个最多能背重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

        我们用贪心就是每次装最大价值的物品,但是又有重量限制,可能价值高的物品重量大,装不了几个就满了,而价值低的物品重量特别小,可以依靠数量获得很高的价值,此时的贪心思想肯定就有问题了。DP会利用递归公式,虽然也是取价值最大的物品,但是会加入重量的影响因素在里面,每次都选取价值高且重量合适的物品。

5.2求解步骤    

        确定dp数组(dp table)以及下标的含义

        确定递推公式

        dp数组如何初始化

        确定遍历顺序

        举例推导dp数组

        打印dp数组

        carl说是五步,我认为是六步哈哈。

6.DP思想与具体题目

6.1509. 斐波那契数 - 力扣(LeetCode)

        这道题可以说是我算法入门题,我们用一个递归就可以很容易的算出来,如果不用递归,用DP的思想:

        首先确定dp数组含义,这道题的dp数组可以直接理解为一个结果数组

        递推公式:已经给出了

        遍历顺序:按序遍历即可

        dp数组初值,dp[0] = 0,dp[1] = 1,其他为0即可。这是因为当n>1时,递归公式才能用。

        第i个下标对应数列的第j个值,j=0,1,2...。

        其他步骤可能不用,因为这题比较熟悉了。

        代码如下:

class Solution:
    def fib(self, n: int) -> int:
        if n == 0:
            return 0
        dp = [0] * (n + 1)
        dp[0], dp[1] = 0, 1
        for i in range(2, n + 1):
            dp[i] = dp[i - 1] + dp[i - 2]
        return dp[n]

 6.270. 爬楼梯 - 力扣(LeetCode):

        爬楼梯,一次只能爬1或2个台阶,爬到楼顶的方法肯定不止一种。假如说有3阶到楼顶,有三种可能,就是都爬一层,或先爬一层再爬两层,或者先爬两层再爬一层。如果有2阶到楼顶,有2中可能。如果只有一阶到楼顶,只有一种可能。所以3=2+1,就是到第3层的方法可以由第1层和第2层推导出来。这就和上题类似了:

class Solution:
    def climbStairs(self, n: int) -> int:
        dp = [0] * (n + 1)
        dp[0], dp[1] = 1, 1
        for i in range(2, n + 1):
            dp[i] = dp[i - 1] + dp[i - 2]
        return dp[n]

        只不过要将dp[0]换成1,因为n已经大于等于1了。

        这样我们可以发现,DP题多数是在找规律!或者说找递归公式。

6.362. 不同路径 - 力扣(LeetCode)

        不会写,思路错了,这题需要用一个二维DP数组来考虑。


突然调剂的专业又不用机试了,以后有机会继续更新。4.7更新线.....................

  • 41
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值