Algorithm Routine Eighteen - Interval DP

Algorithm Routine Eighteen - Interval DP

  • Linear DP: problems with a prefix/suffix structure where each stage only depends on the state of the previous stage
  • Interval DP: Problems that require identifying all possible states within a given interval, and moving from smaller to larger intervals.

Interval DP introduction: https://oi-wiki.org/dp/interval/

Algorithm Example: LeetCode516. Longest Palindromic Subsequence

Given a string s, find the longest palindromic subsequence in it and return the length of the sequence.
A subsequence is defined as a sequence formed by deleting some characters or not deleting any characters without changing the order of the remaining characters.
insert image description here

Method 1: Recursive + memory search

Recursive definition : The recursive function dfs(i,j) returns the length of the longest palindrome subsequence of the substring s[i:j+1].
Recursive process :

  • If s[i] and s[j] are equal, it means that these two characters can be a pair of palindrome subsequences, so we can continue to consider the longest palindrome substring corresponding to substring s[i+1:j-1] sequence, and then increase the length by two.
  • If s[i] and s[j] are not equal, then a longer palindromic subsequence must be selected from the two cases s[i+1:j] and s[i:j-1] as s[i :j+1] is a palindromic subsequence of a substring.

Boundary conditions : - If it has crossed the boundary (ie i>j), then return; - If there is only one character (ie i==j), then return 1 Return
value : Return the length of the longest palindrome subsequence of the entire input string s, That is, call 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)

Method 2: Dynamic programming of two-dimensional array

Converted to dynamic programming according to recursion, but本题更新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]

Algorithm Exercise 1: LeetCode1039. Minimum Score for Polygon Triangulation

You have a convex n-gon with each vertex having an integer value. Given an integer array values, where values[i] is the value of the ith vertex (ie in clockwise order).
Suppose the polygon is subdivided into n - 2 triangles. For each triangle, the value of that triangle is the product of the vertex labels, and the score of the triangulation is the sum of the values ​​of all n - 2 triangles after triangulation.
Returns the lowest possible score for a polygon after triangulation.
insert image description here

Method 1: Recursive + memory search

insert image description hereinsert image description here

  • Recursive function definition: dfs, receiving two parameters i and j indicates that the currently processed vertices range from i to j, and the return value is the score of the minimum triangulation.

  • State transition equation:insert image description here

  • Boundary value: In the recursive function, when the currently processed vertex range has only one or two points and cannot form a triangle, return 0 directly; when the current vertex range has three points, you can directly calculate the value of the triangle formed by these three points The score is returned.

  • Return value: 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)

Method 2: Dynamic Programming

Directly use the above recursive ideas for conversion, but it should be noted that, as in the example, dp[k][j] is required when dp[i][j] is updated, and k is greater than i, so it is necessary to traverse in reverse order when traversing 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}

Algorithm exercise 2: LeetCode375. Guess the size of the number II

We are playing a guessing game with the following rules:
I choose a number from 1 to n.
You can guess which number I chose.
If you guess the correct number, you win the game.
If you guess wrong, then I will tell you that the number I picked is larger or smaller than yours, and you need to keep guessing.
Every time you guess the number x and get it wrong, you pay x amount of cash. If you run out of money, you lose the game.
Given a specific number n, return the smallest amount of cash that guarantees you a win, no matter which number I choose.
insert image description here

Method 1: Recursive + memory search

  • Recursive function definition: dfs(i, j)indicates the minimum cost required to choose a guess in the range [i, j].

  • Recursive equation: Since we want to find an x ​​with the least cost to guess the result, dp[i][j] can be transferred in the following way: among all possible guesses of [i,j], select a number k to guess, divide the interval [i,j] into two sub-intervals [i,k-1] and [k+1,j] according to the result of guessing, and find the maximum value of the two intervals. Among them, the cost of guessing the number k can be expressed as k, since we want to minimize the cost, we need to enumerate all possible values ​​of k, and then select the minimum value among them. It may be a bit difficult to understand the smallest maximum value, but the maximum value is to ensure victory, and the minimum value is because we can choose a different k value each time according to different n, that is, choose the best strategy, and get the smallest guaranteed victory. . The recurrence equation looks like
    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, k-1), dfs(k+1, j))) (i <= k <= j)dfs(i,j)=my ( k+max(dfs(i,k1),dfs(k+1,j)))(i<=k<=j)

  • Boundary value: In the recursive function, when there is only 1 number left in the processing interval, the number of guesses is 0, and 0 is returned; when there are only 2 numbers left in the processing interval, it must be guessed once at most, and the smaller one is guessed That is, return the small number i;

  • Return value: dfs(1,n) returns the minimum cost required from 1 to 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)

Method 2: Dynamic Programming

Directly use the above recursive ideas for conversion, but it should be noted that, as in the example, dp[k+1][j] is required when dp[i][j] is updated, and k is greater than i, so it is necessary to traverse in reverse order when traversing 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} 

Algorithm Exercise 3: LeetCode1312. The minimum number of insertions to make a string a palindrome

Given a string s, you can insert any character at any position in the string for each operation.
Please return the minimum number of operations that make s a palindrome. A "palindrome" is a character string that reads the same both forward and reverse.insert image description here

Method 1: Recursive + memory search

Recursive function definition: dfs(i,j), where i and j represent the index positions of the left and right ends of the current recursively processed substring.

State transition equation: divided into two cases. If the characters at both ends of the current substring are the same, the number of insertions required is the same as that of the substring after removing both ends. If the characters at both ends of the current substring are different, you can discuss inserting a character at the left or right end to make the current substring a palindrome. So need to handle both cases recursively and take the minimum.

Boundary value: When the substring is empty or has only one character, no insertion is required; when the substring has only two characters, no insertion is required if the characters at both ends are equal, otherwise, it needs to be inserted once.

Return value: 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)

Method 2: Dynamic Programming

Directly use the above recursive ideas for conversion, but it should be noted that, as in the example, dp[k+1][j] is required when dp[i][j] is updated, and k is greater than i, so it is necessary to traverse in reverse order when traversing 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}

Advanced Algorithm 1: LeetCode1547. The minimum cost of cutting sticks

You are given an integer array cuts, where cuts[i] represents where you need to cut the stick. You can complete the cuts sequentially, or change the order of the cuts if desired.
The cost of each cut is the length of the current stick to be cut, and the total cost of cutting the stick is the sum of the previous cutting costs. Cutting a stick will split a stick into two smaller sticks (the sum of the lengths of the two sticks is the length of the stick before cutting). See the first example for a more intuitive explanation.
Returns the minimum total cost of cutting a stick.
insert image description here

Method 1: recursive + memorized search, directly traverse the length n without sorting cuts

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)

Method 2: recursive + memory search, sorting and traversing 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)

Method 3: Dynamic programming, sorting and traversing cuts

I don’t know why there is an error after changing method 1 to dynamic programming, but only dynamic programming can be converted to method 2

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}

Algorithm Exercise 5: LeetCode1000. Minimum Cost of Merging Stones

There are N piles of stones arranged in a row, and there are stones[i] stones in the i-th pile.
Each move (move) needs to merge consecutive K piles of stones into one pile, and the cost of this move is the total number of K piles of stones.
Find the minimum cost of combining all stones into one pile. Returns -1 if not possible.
insert image description here

Method 1: Recursive + memory search

This question has a certain degree of difficulty. First of all, if we want to ask for the sum of nums[i:j] for the array nums, we must think of using the prefix sum and define its prefix sum 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]

Through the prefix sum, we can convert the element sum of the subarray into the difference of two prefix sums, namely

∑ 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]

Secondly, it is also necessary to consider whether the array can be merged into 1 pile. Since K piles will be merged into 1 pile each time, that is, k-1 piles will be reduced each time. Originally, n piles, and finally 1 pile is left, so you can judge n-1 Whether it can be divisible by k-1 judges whether it can be merged into 1 pile.

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)

It may not be easy to understand, take an example, dfs (0, 4) first m is i that is m = 0, so it can be divided into dfs (0,0) + dfs (1,4), and dfs (1,4 ) can be divided into dfs(1,1)+dfs(2,4) or dfs(1,3)+dfs(4,4), the smallest of which is dfs(1,3)+dfs(4,4)= 8+0=8. At this time, return to res=dfs(0,0)+dfs(1,4)=0+8=8 in dfs(0,4), and then because (j - i) % (k - 1) = 0, Therefore, it shows that they can be merged into a pile, and all stones from i to j will move, so res += s[5] - s[0]=8+17=25.

Method 2: Dynamic Programming

In the same way, according to the recursive code, it is modified to a two-dimensional array DP, and i traverses in reverse order

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 }

Guess you like

Origin blog.csdn.net/qq_45808700/article/details/130528108