1. 程式人生 > 實用技巧 >第16周LeetCode記錄

第16周LeetCode記錄

12.29 76. 剪繩子

給你一根長度為 n 的繩子,請把繩子剪成整數長度的 m 段(m、n都是整數,n>1並且m>1),每段繩子的長度記為 k[0],k[1]...k[m-1] 。請問 k[0]k[1]...*k[m-1] 可能的最大乘積是多少?例如,當繩子的長度是8時,我們把它剪成長度分別為2、3、3的三段,此時得到的最大乘積是18。

輸入: 10
輸出: 36
解釋: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36

最優解思路

動態規劃:

對於的正整數 n,當 n≥2 時,可以拆分成至少兩個正整數的和。令 k 是拆分出的第一個正整數,則剩下的部分是 n−k,n−k 可以不繼續拆分,或者繼續拆分成至少兩個正整數的和。由於每個正整數對應的最大乘積取決於比它小的正整數對應的最大乘積,因此可以使用動態規劃求解。

dp陣列的含義: dp[i] 表示將正整數 i 拆分成至少兩個正整數的和之後,這些正整數的最大乘積。
邊界條件: 0 不是正整數,1 是最小的正整數,0 和 1 都不能拆分,因此 dp[0]=dp[1]=0。
狀態轉移方程:
當 i≥2 時,假設對正整數 i 拆分出的第一個正整數是 j(1≤j<i),則有以下兩種方案:

將 i 拆分成 j 和 i−j 的和,且 i−j 不再拆分成多個正整數,此時的乘積是 j×(i−j);
將 i 拆分成 j 和 i−j 的和,且 i−j 繼續拆分成多個正整數,此時的乘積是 j×dp[i−j]。
因此,當 j 固定時,有 dp[i]=max(j×(i−j),j×dp[i−j])。由於 j 的取值範圍是 1 到 i−1,需要遍歷所有的 j 得到 dp[i] 的最大值,因此可以得到狀態轉移方程如下:

最優解

class Solution {
    public int cuttingRope(int n) {
        int[] dp = new int[n + 1];
        for (int i = 2; i <= n; i++) {
            for (int j = 1; j < i; j++) {
                dp[i]= Math.max(dp[i], Math.max(j * (i - j), j * dp[i - j]));
            }
        }
        return dp[n];
    }
}

最優解總結

相當於求出d0,d1,d2....dn, 每次都要依賴之前的結果。

12.30 77. 組合總和

給定一個由正整陣列成且不存在重複數字的陣列,找出和為給定目標正整數的組合的個數。

nums = [1, 2, 3]
target = 4

所有可能的組合為:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)

請注意,順序不同的序列被視作不同的組合。

因此輸出為 7。

最優解思路

“動態規劃”的兩個步驟是思考“狀態”以及“狀態轉移方程”。

1、狀態

對於“狀態”,我們首先思考能不能就用問題當中問的方式定義狀態,上面遞迴樹都畫出來了。當然就用問題問的方式。

dp[i] :對於給定的由正整陣列成且不存在重複數字的陣列,和為 i 的組合的個數。

思考輸出什麼?因為狀態就是問題當中問的方式而定義的,因此輸出就是最後一個狀態 dp[n]。

2、狀態轉移方程

由上面的樹形圖,可以很容易地寫出狀態轉移方程:

dp[i] = sum{dp[i - num] for num in nums and if i >= num}

注意:在 0 這一點,我們定義 dp[0] = 1 的,它表示如果 nums 裡有一個數恰好等於 target,它單獨成為 1 種可能。

最優解

class Solution:
    def combinationSum4(self, nums, target):
        size = len(nums)
        if size == 0 or target <= 0:
            return 0

        dp = [0 for _ in range(target + 1)]
        
        # 這一步很關鍵,想想為什麼 dp[0] 是 1
        # 因為 0 表示空集,空集和它"前面"的元素湊成一種解法,所以是 1
        # 這個值被其它狀態參考,設定為 1 是合理的
        
        dp[0] = 1

        for i in range(1, target + 1):
            for j in range(size):
                if i >= nums[j]:
                    dp[i] += dp[i - nums[j]]

        return dp[-1]

最優解總結

dp[4] = 1+dp[3] 的 數量 加 2+dp[2]的數量 加3+dp[1]的數量,即dp[1]+dp[2]+dp[3],同理,分解其中的每一項所以要判斷,

for num in nums:
          if i >= num:

只能由比他小的數字構成。一共有0,1,2,到target,所以是target+1項,每一項都會有對應的組合數量。

12.31 78. 目標和

給定一個非負整數陣列,a1, a2, ..., an, 和一個目標數,S。現在你有兩個符號 + 和 -。對於陣列中的任意一個整數,你都可以從 + 或 -中選擇一個符號新增在前面。

返回可以使最終陣列和為目標數 S 的所有新增符號的方法數。

輸入:nums: [1, 1, 1, 1, 1], S: 3
輸出:5
解釋:

-1+1+1+1+1 = 3
+1-1+1+1+1 = 3
+1+1-1+1+1 = 3
+1+1+1-1+1 = 3
+1+1+1+1-1 = 3

一共有5種方法讓最終目標和為3。

最優解思路

我們用 dp[i][j] 表示用陣列中的前 i 個元素,組成和為 j 的方案數。考慮第 i 個數 nums[i],它可以被新增 + 或 -,因此狀態轉移方程如下:

dp[i][j] = dp[i - 1][j - nums[i]] + dp[i - 1][j + nums[i]]

由於陣列中所有數的和不超過 1000,那麼 j 的最小值可以達到 -1000。在很多語言中,是不允許陣列的下標為負數的,因此我們需要給 dp[i][j] 的第二維預先增加 1000,即:

dp[i][j + nums[i] + 1000] += dp[i - 1][j + 1000]
dp[i][j - nums[i] + 1000] += dp[i - 1][j + 1000]

最優解

public class Solution {
    public int findTargetSumWays(int[] nums, int S) {
        int[][] dp = new int[nums.length][2001];
        dp[0][nums[0] + 1000] = 1;
        dp[0][-nums[0] + 1000] += 1;
        for (int i = 1; i < nums.length; i++) {
            for (int sum = -1000; sum <= 1000; sum++) {
                if (dp[i - 1][sum + 1000] > 0) {
                    dp[i][sum + nums[i] + 1000] += dp[i - 1][sum + 1000];
                    dp[i][sum - nums[i] + 1000] += dp[i - 1][sum + 1000];
                }
            }
        }
        return S > 1000 ? 0 : dp[nums.length - 1][S + 1000];
    }
}

2021

1.1 79. 零錢兌換

給定不同面額的硬幣和一個總金額。寫出函式來計算可以湊成總金額的硬幣組合數。假設每一種面額的硬幣有無限個。

輸入: amount = 5, coins = [1, 2, 5]
輸出: 4
解釋: 有四種方式可以湊成總金額:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1

思路

類似於77組合總和,dp[n] 表示和為n的組合總數。但是注意重複的只算一種

最優解

class Solution:
    @classmethod
    def change(self, amount: int, coins: list) -> int:
        if amount == 0:
            return 0
        dp = [0] * (amount + 1)
        dp[0] = 1

        for coin in coins:
            for i in range(coin, amount + 1):
                dp[i] += dp[i - coin]
        return dp[amount]

1.2 80. 單詞拆分

給定一個非空字串 s 和一個包含非空單詞的列表 wordDict,判定 s 是否可以被空格拆分為一個或多個在字典中出現的單詞。

輸入: s = "leetcode", wordDict = ["leet", "code"]
輸出: true
解釋: 返回 true 因為 "leetcode" 可以被拆分成 "leet code"。

輸入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
輸出: false

最優解思路

揹包問題,動態規劃。dp[i]表示s的前i位是否可以用wordDict中的單詞表示。

遍歷字串的所有子串,開始索引i,區間為[0,n),結束索引j,區間為[i+1,n+1)

若dp[i] = True,且s[i,..j)在wordlist中:dp[j] = True。

最優解

class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool:       
        n=len(s)
        dp=[False]*(n+1)
        dp[0]=True
        for i in range(n):
            for j in range(i+1,n+1):
                if(dp[i] and (s[i:j] in wordDict)):
                    dp[j]=True
        return dp[-1]