算法套路十八——区间 DP

news2024/11/15 11:44:17

算法套路十八——区间 DP

  • 线性DP: 具有前缀/后缀结构的问题,其中每个阶段只依赖于前一阶段的状态
  • 区间DP:需要确定给定区间内所有可能状态的问题,并从较小区间向较大区间进行转移。

区间DP介绍:https://oi-wiki.org/dp/interval/

算法示例:LeetCode516. 最长回文子序列

给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。
子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
在这里插入图片描述

法一:递归+记忆化搜索

递归定义:递归函数dfs(i,j),返回s[i:j+1]这个子串的最长回文子序列长度。
递归过程

  • 如果s[i]和s[j]相等,说明这两个字符可以成为回文子序列的一对,因此我们可以继续考虑子串s[i+1:j-1]所对应的最长回文子序列,再将长度加二。
  • 如果s[i]和s[j]不相等,那么必须从s[i+1:j]和s[i:j-1]两种情况中选择一个较长的回文子序列,作为s[i:j+1]子串的回文子序列。

边界条件:- 如果已经越界(即i>j),那么返;- 如果只有一个字符(即i==j),那么返回1
返回值:返回整个输入字符串s的最长回文子序列长度,即调用dfs(0, n-1)。

class Solution:
    def longestPalindromeSubseq(self, s: str) -> int:
        n=len(s)
        @cache
        def dfs(i:int,j:int):
            if i>j:
                return 0
            if i==j:
                return 1
            if s[i]==s[j]:
                return dfs(i+1,j-1)+2
            return max(dfs(i+1,j),dfs(i,j-1))
        return dfs(0,n-1)

法二:二维数组动态规划

根据递归转化为动态规划,不过本题更新dp[i][j]时,需要用到dp[i+1][j],此时dp[i+1][j]若未更新将导致结果错误;而倒序遍历,则可以保证用到的dp[i+1][j]已经是最新计算出来的值,因此我们倒序遍历i。

class Solution:
    def longestPalindromeSubseq(self, s: str) -> int:
        n=len(s)
        dp=[[0]*(n)for _ in range(n)]
        for i in range(n-1,-1,-1):
            dp[i][i]=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]

算法练习一:LeetCode1039. 多边形三角剖分的最低得分

你有一个凸的 n 边形,其每个顶点都有一个整数值。给定一个整数数组 values ,其中 values[i] 是第 i 个顶点的值(即 顺时针顺序 )。
假设将多边形 剖分 为 n - 2 个三角形。对于每个三角形,该三角形的值是顶点标记的乘积,三角剖分的分数是进行三角剖分后所有 n - 2 个三角形的值之和。
返回 多边形进行三角剖分后可以得到的最低分 。在这里插入图片描述

法一 :递归+记忆化搜索

在这里插入图片描述在这里插入图片描述

  • 递归函数定义:dfs,接收两个参数 i 和 j 表示当前处理的顶点范围从i到j,返回值是组成最小三角剖分的分值。

  • 状态转移方程:在这里插入图片描述

  • 边界值:在递归函数中,当当前处理的顶点范围只有一个或两个点,无法组成三角形时,直接返回 0;当前顶点范围有三个点时,可以直接计算出这三个点组成的三角形的得分返回。

  • 返回值:dfs(0,n-1)

class Solution:
    def minScoreTriangulation(self, values: List[int]) -> int:
        n=len(values)
        @cache
        def dfs(i:int,j:int)->int:
            # 如果两个顶点之间没有其他点,则不能组成三角形,分值为0
            if j-i<2:
                return 0
            # 如果两个顶点之间有两个其他点,则只有一种组合方式,直接计算返回分值
            if j-i==2:
                return values[i]*values[i+1]*values[j]
            score=inf
            # 第二步:枚举第k号顶点(i+1 <= k <= j-1),将第i、j号顶点和第k号顶点连边,
            # 分成"第i号顶点到第k号顶点"和"第k号顶点到第j号顶点"两部分,递归求解
            # 然后将两部分分值相加,并加上连接上第1号、k号和j号顶点的得分 values[i]*values[k]*values[j]
            for k in range(i+1,j):
                #k等于i+1时dfs(i,k)=0,k等于j-1时有dfs(k,j)=0
                score=min(score,dfs(i,k)+values[i]*values[k]*values[j]+dfs(k,j))
            return score
        return dfs(0,n-1)

法二:动态规划

直接利用上述递归思路进行转换,不过需要注意与示例一样,dp[i][j]更新时需要dp[k][j],而k是大于i的,所以在遍历i时需要倒序遍历

func minScoreTriangulation(values []int) int {
    n:=len(values)
    dp:=make([][]int,n)
    for i :=range dp{
        dp[i]=make([]int,n)
    }
    // 倒序枚举左端点,且由于三角形至少3个点,故左端点从n-3开始
    for i:=n-3;i>=0;i--{
        // 正序枚举右端点,且由于三角形至少3个点,右端点从i+2开始枚举
        for j:=i+2;j<n;j++{
            dp[i][j]=math.MaxInt
            // 枚举中间断点k从i+1到j-1
            for k:=i+1;k<j;k++{
                dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]+values[i] * values[j] *values[k])
            }
        }
    }
    return dp[0][n-1]
}
func min(a,b int)int{if a>b{return b};return a}

算法练习二:LeetCode375. 猜数字大小 II

我们正在玩一个猜数游戏,游戏规则如下:
我从 1 到 n 之间选择一个数字。
你来猜我选了哪个数字。
如果你猜到正确的数字,就会 赢得游戏 。
如果你猜错了,那么我会告诉你,我选的数字比你的 更大或者更小 ,并且你需要继续猜数。
每当你猜了数字 x 并且猜错了的时候,你需要支付金额为 x 的现金。如果你花光了钱,就会 输掉游戏 。
给你一个特定的数字 n ,返回能够 确保你获胜 的最小现金数,不管我选择那个数字 。在这里插入图片描述

法一:递归+记忆化搜索

  • 递归函数定义:dfs(i, j) 表示选择在范围 [i, j] 中猜数时所需要的最小代价。

  • 递归方程:由于我们要找出一个代价最小的 x 来猜测结果,因此 dp[i][j] 可以通过以下方式转移而来:在 [i,j] 的所有可能猜测数中,选出一个数 k 进行猜测,根据猜测的结果将区间 [i,j] 分成两个子区间 [i,k-1] 和 [k+1,j],求出两个区间的最大值。其中,猜测数字 k 的代价可以表示为 k,由于要最小化代价,因此我们需要枚举所有可能的 k 值,然后选取其中的最小值。可能最小的最大值有点难以理解,但是最大值是为了确保能胜利,而最小值是因为我们可以根据不同的n选择每次不同的k值即选择最佳策略,能得到最小的确保胜利的金钱。递归方程如下所示
    d f s ( i , j ) = m i n ( k + m a x ( d f s ( i , k − 1 ) , d f s ( k + 1 , j ) ) ) ( i < = k < = j ) dfs(i, j) = min(k + max(dfs(i, k-1), dfs(k+1, j))) (i <= k <= j) dfs(i,j)=min(k+max(dfs(i,k1),dfs(k+1,j)))(i<=k<=j)

  • 边界值:在递归函数中,当处理区间只剩下 1 个数时,猜测次数为 0,返回 0;当处理区间只剩下 2 个数时,肯定最多只需要猜一次,猜较小的那个即可,返回小的数 i;

  • 返回值:dfs(1,n)返回 1 到 n 所需要的最小代价。

class Solution:
    def getMoneyAmount(self, n: int) -> int:
        # 缓存中间计算结果的记忆化递归函数
        @cache
        def dfs(i:int,j:int)->int:
            # 边界情况,当 i > j 时没有可猜的数,返回 0
            if i>=j:
                return 0
            # 边界情况,当只剩下两个数时,肯定最多只需要猜一次,猜较小的那个即可,返回小的数i
            if i==j+1:
                return i
            # 初始化当前区间的最小代价
            ans=inf
            # 枚举可能的猜测数字k,计算从 [i,j] 区间猜这个数字的代价
            for k in range(i,j+1):
                # 递归地计算两个子问题的最大代价,并求出当前 k 猜测的代价
                cost=k+max(dfs(i,k-1),dfs(k+1,j))
                # 取所有可能的代价中最小的那个作为当前区间的最小代价
                ans=min(ans,cost)
            return ans

        # 调用记忆化递归函数,返回从1到n所需要的最小代价
        return dfs(1,n)

法二:动态规划

直接利用上述递归思路进行转换,不过需要注意与示例一样,dp[i][j]更新时需要dp[k+1][j],而k是大于i的,所以在遍历i时需要倒序遍历

func getMoneyAmount(n int) int {
    dp:=make([][]int,n+2) // 初始化动态规划数组dp
    for i:=range dp{
        dp[i]=make([]int ,n+2)
    }
    for i:=n;i>0;i--{ // 倒序枚举i
        for j:=i+1;j<=n;j++{
            dp[i][j]=math.MaxInt // 初始化dp[i][j]为正无穷大
            for k:=i;k<=j;k++{ // 枚举[i,j]区间内的数k
                dp[i][j]=min(dp[i][j],k+max(dp[i][k-1],dp[k+1][j])
            }
        }
    } 
    return dp[1][n] 
}
func min(a,b int)int{if a>b{return b};return a} 
func max(a,b int)int{if a>b{return a};return b} 

算法练习三:LeetCode1312. 让字符串成为回文串的最少插入次数

给你一个字符串 s ,每一次操作你都可以在字符串的任意位置插入任意字符。
请你返回让 s 成为回文串的 最少操作次数 。「回文串」是正读和反读都相同的字符串。在这里插入图片描述

法一:递归+记忆化搜索

递归函数定义:dfs(i,j),其中i 和 j 表示当前递归处理的子串的左右两端的索引位置。

状态转移方程:分为两种情况。若当前子串两端字符相同,则需要插入次数与去掉两端后的子串相同。若当前子串两端字符不相同,则可以讨论在左端或右端插入一个字符,使得当前子串变成回文串。因此需要递归处理两种情况并取最小值。

边界值:当子串为空或只有一个字符时不需要插入;当子串只有两个字符时,若两端字符相等则不需要插入,否则需要插入一次。

返回值:dfs(0,n-1)

class Solution:
    def minInsertions(self, s: str) -> int:
        n=len(s)
        @cache  
        def dfs(i:int,j:int)->int:
            if i >= j:  # 当子串为空或只有一个字符时不需要插入
                return 0
            if i + 1 == j:  # 当子串只有两个字符时
                if s[i] == s[j]:  # 对称则不需要插入
                    return 0
                else:  # 不对称需要插入一次
                    return 1
            if s[i] == s[j]:  # 当子串两端字符相同,则递归处理去掉两端后的子串
                return dfs(i + 1, j - 1)
            else:  # 当子串两端不相同时,则用两种方式插入一次字符,取最小值
                return 1 + min(dfs(i, j - 1), dfs(i + 1, j))
        return dfs(0,n-1)

法二:动态规划

直接利用上述递归思路进行转换,不过需要注意与示例一样,dp[i][j]更新时需要dp[k+1][j],而k是大于i的,所以在遍历i时需要倒序遍历

func minInsertions(s string) int {
    n:=len(s)
    dp:=make([][]int,n+1)
    for i:=0;i<n;i++{
        dp[i]=make([]int,n+1)
    }
    // 自底向上计算dp数组
    for i:=n-1;i>=0;i--{
        dp[i][i]=0 // 只有一个字符时不需要插入
        for j:=i+1;j<n;j++{
            dp[i][j]=math.MaxInt // 初始化为最大值
            if s[i] == s[j]{
                dp[i][j]=dp[i+1][j-1] // 当两端字符相同时,去掉两端后的子串已经是回文串
            }else{
                dp[i][j]=1+min(dp[i][j-1],dp[i+1][j]) // 分别插入左右使得两端相同,取最小值
            }
        }
    }
    return dp[0][n-1]
func min(a,b int)int{if a>b{return b};return a}

算法进阶一:LeetCode1547. 切棍子的最小成本

给你一个整数数组 cuts ,其中 cuts[i] 表示你需要将棍子切开的位置。你可以按顺序完成切割,也可以根据需要更改切割的顺序。
每次切割的成本都是当前要切割的棍子的长度,切棍子的总成本是历次切割成本的总和。对棍子进行切割将会把一根木棍分成两根较小的木棍(这两根木棍的长度和就是切割前木棍的长度)。请参阅第一个示例以获得更直观的解释。
返回切棍子的 最小总成本 。
在这里插入图片描述

法一:递归+记忆化搜索,不排序cuts直接遍历长度n

class Solution:
    def minCost(self, n: int, cuts: List[int]) -> int:
        # 定义递归函数,i和j表示当前区间的左右端点
        @cache
        def dfs(i:int,j:int)->int:
            # 边界条件:当区间长度小于等于1时,不需要再切割,返回0
            if i+1>=j:
                return 0
            res = inf 
            # 枚举所有可能的切割点
            for cut in cuts:
                if i < cut < j:
                    # 递归计算左右两个子区间的最小代价,并更新最小值
                    res = min(res, dfs(i, cut) + dfs(cut, j) )
            # 返回当前区间的最小代价,加上当前区间的长度
            return res + j - i if res != inf else 0
        # 调用递归函数,计算整个区间[0,n]的最小代价
        return dfs(0,n)

法二:递归+记忆化搜索,排序遍历cuts

class Solution:
    def minCost(self, n: int, cuts: List[int]) -> int:
    	cuts = [0] + sorted(cuts) + [n]
        @cache
        def dfs(i, j):  # (i, j)
            if i + 1 >= j:
                return 0
            res = inf 
            for k in range(i + 1, j):
                res = min(res, dfs(i, k) + dfs(k, j) + cuts[j] - cuts[i])
            return res
        return dfs(0, len(cuts) - 1)

法三:动态规划,排序遍历cuts

不知道为什么对于法一换为动态规划后有错误,只能对于法二进行动态规划转换

func minCost(n int, cuts []int) int {
    m := len(cuts)
    sort.Ints(cuts)
    cuts = append([]int{0}, cuts...)
    cuts = append(cuts, n)
    f := make([][]int, m+2)
    //[i][j] 表示在当前待切的木棍左端点为 cuts[i-1],右端点为 cuts[j+1] 时,将木棍全部切开的最小成本
    for i := range f {
        f[i] = make([]int, m+2)
    }
    for i := m; i >= 1; i-- {
        for j := i; j <= m; j++ {
            // 初始化 f[i][j] 的值为最大整数
            f[i][j] = math.MaxInt32
            // 枚举所有可能的切割点 k
            for k := i; k <= j; k++ {
                // 更新 f[i][j] 的值为左右两个子区间的最小代价加上当前区间的代价
                f[i][j] = min(f[i][j], f[i][k-1]+f[k+1][j])
            }
            f[i][j] += cuts[j+1] - cuts[i-1]
        }
    }
    return f[1][m]
}
func min(a,b int)int{if a>b{return b};return a}

算法练习五:LeetCode1000. 合并石头的最低成本

有 N 堆石头排成一排,第 i 堆中有 stones[i] 块石头。
每次移动(move)需要将连续的 K 堆石头合并为一堆,而这个移动的成本为这 K 堆石头的总数。
找出把所有石头合并成一堆的最低成本。如果不可能,返回 -1 。

在这里插入图片描述

法一:递归+记忆化搜索

本题有一定的难度,首先对于数组nums想要求nums[i:j]的和,我们要想到使用前缀和,定义它的前缀和 s [ 0 ] = 0 , s [ i + 1 ] = ∑ j = 0 i stones [ j ] \textit{s}[0]=0,\textit{s}[i+1] = \sum\limits_{j=0}^{i}\textit{stones}[j] s[0]=0s[i+1]=j=0istones[j]

通过前缀和,我们可以把子数组的元素和转换成两个前缀和的差,即

∑ j = left right stones [ j ] = ∑ j = 0 right stones [ j ] − ∑ j = 0 left − 1 stones [ j ] = s [ right + 1 ] − s [ left ] \sum_{j=\textit{left}}^{\textit{right}}\textit{stones}[j] = \sum\limits_{j=0}^{\textit{right}}\textit{stones}[j] - \sum\limits_{j=0}^{\textit{left}-1}\textit{stones}[j] = \textit{s}[\textit{right}+1] - \textit{s}[\textit{left}] j=leftrightstones[j]=j=0rightstones[j]j=0left1stones[j]=s[right+1]s[left]

其次,也要考虑能否将数组合并成1堆,由于每次都会合并K堆为1堆,即每次减少k-1堆,原本n堆,最后剩余1堆,故可以通过判断n-1能否整除k-1判断是否能够为合并为1堆。

class Solution:
    def mergeStones(self, stones: List[int], k: int) -> int:
        n = len(stones)
        # 每次减少k-1堆,最后剩一堆,如果无法整除说明无法合并成一堆
        if (n - 1) % (k - 1):  
            return -1
        s = list(accumulate(stones, initial=0))  # 前缀和
        @cache  # 缓存装饰器,避免重复计算 dfs 的结果
        def dfs(i: int, j: int) -> int:
            if i == j:  # 只有一堆石头,无需合并
                return 0
            #m每次增加k-1,故每次dfs(i, m)与dfs(m + 1, j)都可以合并成一堆
            res = min(dfs(i, m) + dfs(m + 1, j) for m in range(i, j, k - 1))
            
            # 如果j-i是k-1的倍数,则一定可以合并成一堆
            # 说明从i到j所有元素都要移动,故加上从i到j的所有前缀和
            #如果j-i不是k-1的倍数,则说明当前不能合并为一堆,故不能将从i到j所有合并,需要留到后续添加
            if (j - i) % (k - 1) == 0:  
                res += s[j + 1] - s[i]
            return res
        return dfs(0, n - 1)

可能不太容易理解,就拿示例举例,dfs(0,4)首先m为i即m=0,故可分为dfs(0,0)+dfs(1,4),而dfs(1,4)可以分为dfs(1,1)+dfs(2,4)或者dfs(1,3)+dfs(4,4),其中最小的是dfs(1,3)+dfs(4,4)=8+0=8。此时回到dfs(0,4)中的res=dfs(0,0)+dfs(1,4)=0+8=8,之后由于 (j - i) % (k - 1) = 0,故说明能合并成一堆,从i到j所有的石头都会移动,故 res += s[5] - s[0]=8+17=25.

法二:动态规划

同理根据递归代码修改为二维数组DP,i倒序遍历

func mergeStones(stones []int, k int) int {
    n := len(stones)
    if (n-1)%(k-1) > 0 { // 无法合并成一堆
        return -1
    }
    //前缀和数组s
    s := make([]int, n+1)
    for i, x := range stones {
        s[i+1] = s[i] + x 
    }
    dp := make([][]int, n)
    for i := n - 1; i >= 0; i-- {
        dp[i] = make([]int, n)
        for j := i + 1; j < n; j++ {
            dp[i][j] = math.MaxInt
            for m := i; m < j; m += k - 1 {
                dp[i][j] = min(dp[i][j], dp[i][m]+dp[m+1][j])
            }
            if (j-i)%(k-1) == 0 { // 可以合并成一堆
                dp[i][j] += s[j+1] - s[i]
            }
        }
    }
    return dp[0][n-1]
}
func min(a, b int) int { if b < a { return b }; return a }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/564487.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

网络安全证书合集

网络安全作为2022年十大高新紧缺技能之一&#xff0c;足以证明网络安全的重要性&#xff0c;作为网络安全工程师更是责任重大&#xff0c;证书作为能力证明的一方面&#xff0c;也是尤为重要&#xff0c;本文将着重盘点一下网络安全证书合集。 国家注册渗透测试工程师&#xf…

传染病学模型 | Matlab实现SEIR传染病学模型 (SEIR Epidemic Model)

文章目录 效果一览基本描述模型介绍程序设计参考资料效果一览 基本描述 传染病学模型 | Matlab实现SEIR传染病学模型 (SEIR Epidemic Model) 模型介绍 SEIR模型是一种常见的传染病传播模型,用于描述人群感染某种传染病的过程。SEIR模型将人群划分为四个互相转化的状态: 易感者…

内容好但流量差?B站流量密码可能就在这

B站知名数码UP主老师好我叫何同学&#xff08;以下简称“何同学”。&#xff09;时隔两个月再次更新&#xff0c;这支标题为《为了找到流量密码&#xff0c;我们做了个假B站...》的视频不仅吸引了观众的围观&#xff0c;更是获得了众多B站UP主们的“声援”。 如题所见&#xf…

Netty 源码分析系列(十八)一行简单的writeAndFlush都做了哪些事?

文章目录 前言源码分析ctx.writeAndFlush 的逻辑writeAndFlush 源码ChannelOutBoundBuff 类addMessage 方法addFlush 方法AbstractNioByteChannel 类 小结 前言 对于使用netty的小伙伴来说&#xff0c;我们想通过服务端往客户端发送数据&#xff0c;通常我们会调用ctx.writeAn…

SURF算法详解

Speeded Up Robust Features&#xff08;SURF&#xff0c;加速稳健特征&#xff09; 一&#xff0e;积分图像 1.什么是积分图像 积分图像是输入的灰度图像经过一种像素间的累加运算得到种新的图像媒介。对于一幅灰度的图像&#xff0c;积分图像中的任意一点&#xff08;x,y&…

【投毒情报】PyPI中 colorara 等组件包泄漏主机截屏等敏感信息

漏洞描述 PyPI仓库中受影响版本的 colorara 和 libida组件在安装过程中会根据不同操作系统分别执行恶意逻辑&#xff0c;针对Windows执行White Snake远控木马&#xff0c;针对Linux收集系统截屏、主机名、用户名、IP等主机敏感信息发送至telegram。 漏洞名称PyPI中 colorara …

大数据可视化大屏电子沙盘合集

大数据可视化电子沙盘 使用HTML、CSS、JavaScript&#xff0c;实现的可视化大数据电子沙盘 如果觉得对你有用&#xff0c;随手点个 &#x1f31f; Star &#x1f31f; 支持下&#xff0c;这样才有持续下去的动力&#xff0c;谢谢&#xff01;&#xff5e; 体验地址&#xff0…

一文告诉你黑盒测试、白盒测试、集成测试和系统测试的区别与联系

于开发人员来说&#xff0c;往往对各种测试方法感到疑惑。特别是在整合代码的时候&#xff0c;我们就能深刻感觉受到测试的重要性。很多开发人员只注重写代码&#xff0c;轻视测试的重要性。总是代码一写完提交然后就交给测试组测试了&#xff0c;没多久测试组发回测试报告。然…

java 在线音乐网站系统Myeclipse开发mysql数据库struts2结构java编程计算机网页项目

一、源码特点 java 在线音乐网站系统 是一套完善的web设计系统&#xff0c;对理解JSP java编程开发语言有帮助struts2开发技术&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为 TOMCAT7.0,Myeclipse8.5开发&#xff0c;数据库为Mys…

二叉树初阶和堆的详解

前言&#xff1a;二叉树是一种基础数据结构&#xff0c;它由节点和边构成&#xff0c;其中每个节点最多只有两个子节点&#xff0c;称为左子节点和右子节点。二叉树具有许多应用&#xff0c;例如搜索算法和排序算法&#xff0c;还可以用于创建堆等高级数据结构。 堆是一种基于完…

一次完整的性能测试,测试人员需要做哪些工作?

今天和大家讲一下完成性能测试&#xff0c;测试人员需要做哪些工作&#xff1f;接下来一菲用四个步骤妥妥的教会你&#xff0c;啥叫完整的性能测试&#xff0c;请看好了呀&#xff01; 一.流程概述 1.规范流程的意义 规范的性能测试实施流程能够加强测试工作流程控制&#x…

Unity3D安装:从命令行安装 Unity

推荐&#xff1a;将 NSDT场景编辑器 加入你的3D工具链 3D工具集&#xff1a; NSDT简石数字孪生 从命令行安装 Unity 如果要在组织中自动部署 Unity&#xff0c;可以从命令行安装 Editor 和其他组件。这些组件是普通的安装程序可执行程序和软件包&#xff0c;可以给用来自动部署…

【MySQL新手到通关】第四章 排序与分页

文章目录 &#x1f43c;1. 排序数据&#x1fa82;&#x1fa82;1.1 排序规则&#x1fa82;&#x1fa82;1.2 单列排序&#x1fa82;&#x1fa82;1.3 多列排序 &#x1f43c;2. 分页&#x1fa82;&#x1fa82;2.1 背景&#x1fa82;&#x1fa82;2.2 实现规则&#x1fa82;&am…

实用交互设计工具大盘点

近年来&#xff0c;页面交互设计有了很好的发展&#xff0c;越来越受到人们的重视。如果你想成为一名页面交互设计师&#xff0c;除了对平面设计和产品设计有一定的了解外&#xff0c;更重要的是要知道哪个软件适合页面交互设计。本文将带您了解5款流行的页面交互设计软件。 1…

一题都看不懂,大厂面试真的变态......

最近我的一个读者朋友去了字节面试&#xff0c;来给我发信息吐槽&#xff0c;说字节的面试太困难了&#xff0c;像他这种三年经验的测试员&#xff0c;在技术面&#xff0c;居然一题都答不上来&#xff0c;这要多高的水平才能有资格去面试字节的测试岗位。 确实&#xff0c;字…

Hudi(三)集成Flink

1、环境准备 将编译好的jar包放到Flink的lib目录下。 cp hudi-flink1.13-bundle-0.12.0.jar /opt/module/flink-1.13.2/lib 2、sql-client方式 2.1、修改flink-conf.yaml配置 vim /opt/module/flink-1.13.2/conf/flink-conf.yamlstate.backend: rocksdb execution.checkpoi…

SpringCloud Gateway高级应用

目录 1 SpringCloud技术栈1.1 SpringCloud技术栈1.2 SpringCloud经典技术介绍1.3 SpringCloud项目场景 2 SpringCloud Gateway2.1 Gateway工作原理2.2 Gateway路由2.2.1 业务说明2.2.2 基于配置路由设置2.2.3 基于代码路由配置2.2.4 Gateway-Predicate2.2.5 断言源码剖析 2.3 G…

Settings apk进行系统签名覆盖安装

由于AndroidStudio中Settings编译出来的包是未签名的,不能将设备覆盖安装替换原先签名的包,故需要将AndroidStudio打包出来的apk进行签名 一.拷贝未签名的apk 注意签名过程需要在ubuntu中进行,所以需要将未签名的apk拷贝到ubuntu中,如下: 二.拷贝libconscrypt_openjd…

Sketch文件用什么软件打开

现在&#xff0c;想要在线打开 Sketch 文件只需要 2 步就能搞定了&#xff01; 第一步&#xff0c;访问Windows 也能用的「协作版 Sketch」——即时设计官网并点击免费使用&#xff0c;即可进入即时设计工作台。 第二步&#xff0c;进入即时设计工作台后&#xff0c;点击【文件…

【软件分析/静态分析】学习笔记01——Introduction

&#x1f517; 课程链接&#xff1a;李樾老师和谭天老师的&#xff1a;南京大学《软件分析》课程01&#xff08;Introduction&#xff09;_哔哩哔哩_bilibili 目录 一、静态程序分析介绍 1.1 PL and Static Analysis 程序语言和静态分析 1.2 为什么要学 Static Analysis? …