[leetcode] 動態規劃(Ⅰ)

sinkinben發表於2020-06-03

?這次按通過率從高到低刷題。

本文完成的題目:{338, 1025, 303, 121, 53, 392, 70, 746, 198} ,帶有「面試」Tag 的題目:Interview - {1617, 42, 1716, 0801}

大部分題目都是 Simple 難度的水題,可以作為動態規劃的入門練習題。

位元位計數

題目[338]:一道位運算和動態規劃結合的 ?題目

解題思路

狀態定義:dp[i] 表示第 i 個自然數二進位制中 1 的個數。

狀態轉移方程:dp[i] = dp[i >> 1] + (i & 1)

程式碼實現

class Solution {
public:
    vector<int> countBits(int num) {
        vector<int> v(num + 1);
        if (num == 0)  return v;
        v[1] = 1;
        for (int i = 2; i <= num; i++)
            v[i] = v[i >> 1] + (i & 0x1);
        return v;
    }
};

除數博弈

題目[1025]:點選 ?此處 檢視題目。

解法1:數學方法

規則是 Alice 先手,顯然 2 到誰手上誰就是贏家。

  • 若 N 是奇數

不管 Alice 選擇什麼,x 的值必然是奇數(包括 1 在內)。那麼交給 Bob 的 N - x 是一個偶數,Bob 只要一直取 x = 2 ,把一個奇數交給 Alice,那麼最後 2 必然會落到 Bob 的手中。所以 N 為奇數,Alice 必輸。

也可以得到一個結論:奇數先手必輸。

  • 若 N 是偶數

x 的值可奇可偶。在這時 Alice 只需要取 x = 1 把奇數 N - x 交給 Bob,此時對於 Bob 來說是「奇數先手」情況,Bob 必輸。因此 N 為偶數,Alice 必贏。

bool divisorGame(int N) { return N % 2 == 0; }

解法2:動態規劃

狀態方程:dp[i] = true 表示 Alice 贏,否則 Bob 贏。

顯然,對於 dp[i] ,只要出現 dp[i-x]false 的情況 ( 0 < x < i ),dp[i] 就為 true。因為一旦出現這種情況,Alice選擇該 x 就能勝出

class Solution {
public:
    bool divisorGame(int N) {
        // return N % 2 == 0;
        return dpSolution(N);
    }
    bool dpSolution(int N)
    {
        if (N == 1 || N == 3)
            return false;
        if (N == 2)
            return true;
        vector<bool> v(N+1);
        v[1] = v[3] = false;
        v[2] = true;
        for (int i = 4; i <= N; i++)
            for (int j = 1; j < i; j++)
                if (i % j == 0 && !v[i - j])
                {
                    v[i] = true;
                    break;
                }
        return v[N];
    }
};

區域和檢索 - 陣列不可變

題目[303]:Click the ?Link to see the question.

解題思路

字首和(這裡是一維形式)。

狀態定義:

dp[i] = 0                       if i == 0
      = sum(nums[0, ..., i-1])  if i >= 1

那麼:sumRange(i,j) = sum(nums[0, ..., j]) - sum(nums[0, ..., i-1]) = dp[j+1] - dp[i] .

程式碼實現

class NumArray
{
public:
    vector<int> dp;
    NumArray(vector<int> &nums)
    {
        int n = nums.size();
        dp.resize(n + 1, 0);
        dp[0] = 0;
        for (int i = 1; i <= n; i++)
            dp[i] = nums[i - 1] + dp[i - 1];
    }
    int sumRange(int i, int j) { return dp[j + 1] - dp[i]; }
};

最大子序列和

題目[Interview-1617]:點選 ?此處 檢視題目。

本題與題目 Interview-42題目53 相同。

解題思路

狀態定義:定義 dp[i] 表示以 a[i] 結尾的最大連續子序列。

轉移方程:dp[i] = max(a[i], dp[i-1] + a[i])

程式碼實現

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int n = nums.size();
        vector<int> dp(nums);
        int maxval = dp[0];
        for (int i = 1; i < n; i++)
            dp[i] = max(nums[i], dp[i-1] + nums[i]), maxval = max(maxval, dp[i]);
        return maxval;
    }
};

空間優化後:

int spaceOptimize(vector<int> &nums)
{
    int dp = nums.front();
    int n = nums.size();
    int maxval = dp;
    for (int i = 1; i < n; i++)
        dp = max(dp + nums[i], nums[i]), maxval = max(maxval, dp);
    return maxval;
}

買賣股票的最佳時機

題目[121]:點選 ?連結 檢視題目。

解題思路

只掃描一遍,一邊記錄當前已找到的「最小的價格」,一邊記錄目前為止「最大利潤」。

程式碼實現

#define max(a,b) ((a) > (b) ? (a) : (b))
#define min(a,b) ((a) < (b) ? (a) : (b))
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int minval = 0x3f3f3f3f;
        int maxval = 0;
        for (auto x: prices)
        {
            minval = min(x, minval);
            maxval = max(x - minval, maxval);
        }
        return maxval;
    }
};

按摩師

題目[Interview-1716]:這個 按摩師 的名字有點意思?。

本題與下面的 198. 打家劫舍 一模一樣。

解題思路

狀態定義:dp0[i] 表示在不接受 num[i] 情況下的最大預約時間;dp1[i] 表示接受 num[i] 情況下的最大預約時間。

轉移方程:

  • dp0[i] = max(dp0[i-1], dp1[i-1]) :在不接受第 i 個請求的情況下,第 i-1 個請求可以選擇接受或者不接受。
  • dp1[i] = nums[i] + dp0[i-1] :在接受第 i 個請求的情況下,第 i-1 個請求必然不能接受。

程式碼實現

包括空間優化解法。

class Solution
{
public:
    int massage(vector<int> &nums)
    {
        return spaceOptimize(nums);
    }
    int commonDP(vector<int> &nums)
    {
        int n = nums.size();
        if (n == 0)
            return 0;
        vector<vector<int>> dp(n, vector<int>(2, 0));
        dp[0][0] = 0, dp[0][1] = nums[0];
        int maxval = nums[0];
        for (int i = 1; i < n; i++)
        {
            dp[i][0] = max(dp[i - 1][0], dp[i - 1][1]);
            dp[i][1] = dp[i - 1][0] + nums[i];
            maxval = max(maxval, max(dp[i][0], dp[i][1]));
        }
        return maxval;
    }

    int spaceOptimize(vector<int> &nums)
    {
        int n = nums.size();
        if (n == 0)
            return 0;
        int dp0 = 0, dp1 = nums[0];
        int maxval = dp1;
        int t;
        for (int i = 1; i < n; i++)
        {
            t = dp1;
            dp1 = dp0 + nums[i];
            dp0 = max(dp0, t);
            maxval = max(maxval, max(dp0, dp1));
        }
        return maxval;
    }
};

判斷子序列

題目[392]:點選 ?連結 檢視題目。

解法1:動態規劃

最長公共子序列 (LCS) 的變種題。

找出 st 的最長公共子序列長度 maxval ,判斷 maxval == s.length

關於 LCS 的具體解法,看 這裡 的第二小節。

class Solution
{
public:
    bool isSubsequence(string s, string t)
    {
        int slen = s.length(), tlen = t.length();
        vector<vector<int>> dp(slen + 1, vector<int>(tlen + 1, 0));
        int maxval = 0;
        for (int i = 1; i <= slen; i++)
        {
            for (int j = 1; j <= tlen; j++)
            {
                if (s[i - 1] == t[j - 1])
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                else
                    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
                maxval = max(maxval, dp[i][j]);
            }
        }
        return maxval == slen;
    }
};

解法2:迴圈比較

bool loopSolution(string &s, string &t)
{
    int i = 0, j = 0;
    int slen = s.length(), tlen = t.length();
    while (i < slen && j < tlen)
    {
        if (s[i] == t[j])  i++;
        j++;
    }
    return i == slen;
}

爬樓梯

題目[72]:經典題目

解法1:遞迴

超時。

int recursion(int n)
{
    if (n == 1 || n == 2)  return n;
    return recursion(n-1) + recursion(n-2);
}

解法2:動態規劃

類似於斐波那契數列。

int dp(int n)
{
    if (n == 1 || n == 2)  return n;
    int f1 = 1, f2 = 2, f3 = 3;
    for (int i = 3; i <= n; i++)
        f3 = f1 + f2, f1 = f2, f2 = f3;
    return f3;
}

使用最小花費爬樓梯

題目[746]:爬樓梯的加強版

解題思路

狀態定義:dp[i] 是到達第 i 個階梯的最小花費(但不包括第 i 個的花費 cost[i] ),因此需要預處理 cost.push_back(0)

轉移方程:

dp[i] = cost[i]                          if i==0 or i==1
      = min(dp[i-1], dp[i-2]) + cost[i]  if i>=2

解析:第 i 個階梯總是可以通過第 i-1 或 i-2 個直接抵達。

程式碼實現

int minCostClimbingStairs(vector<int> &cost)
{
    cost.push_back(0);
    int n = cost.size();
    vector<int> dp(n, 0);
    dp[0] = cost[0], dp[1] = cost[1];
    for (int i = 2; i < n; i++)
        dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];
    return dp[n - 1];
}

空間優化:

int minCostClimbingStairs(vector<int> &cost)
{
    cost.push_back(0);
    int n = cost.size();
    int f0 = cost[0], f1 = cost[1], f2;
    for (int i = 2; i < n; i++)
        f2 = cost[i] + min(f0, f1), f0 = f1, f1 = f2;
    return f2;
}

打家劫舍

題目[198]:點選檢視 題目

解法1:與上面的「按摩師」一模一樣

包括普通形式與空間優化形式。

class Solution
{
public:
    int rob(vector<int> &nums)
    {
        return spaceOptimize(nums);
    }
    int commonDP(vector<int> &nums)
    {
        int n = nums.size();
        if (n == 0)  return 0;
        if (n == 1)  return nums[0];
        vector<int> dp0(n, 0), dp1(n, 0);
        dp0[0] = 0, dp1[0] = nums[0];
        int maxval = max(nums[0], nums[1]);
        for (int i = 1; i < n; i++)
        {
            dp0[i] = max(dp0[i - 1], dp1[i - 1]);
            dp1[i] = dp0[i - 1] + nums[i];
            maxval = max(maxval, max(dp0[i], dp1[i]));
        }
        return maxval;
    }
    int spaceOptimize(vector<int> &v)
    {
        int n = v.size();
        if (n == 0)  return 0;
        if (n == 1)  return v[0];
        int dp0 = 0, dp1 = v[0];
        int maxval = v[0];
        int t;
        for (int i = 1; i < n; i++)
        {
            t = dp0;
            dp0 = max(dp0, dp1);
            dp1 = v[i] + t;
            maxval = max(dp0, dp1);
        }
        return maxval;
    }
};

解法2:官方題解

狀態定義:dp[i] 表示在 [0, ..., i] 中偷竊的最大收益。

轉移方程:dp[i] = max(dp[i-1], dp[i-2] + nums[i]) 。如果選擇偷竊 nums[i] 那麼就只能在 [0,...,i-2] 的條件下進行;如果選擇不偷竊 nums[i] 那麼可以在 [0,...,i-1] 的範圍內選擇。

int officialSolution(vector<int> &v)
{
    int n = v.size();
    if (n == 0)  return 0;
    if (n == 1)  return v[0];
    int f0 = v[0], f1 = max(v[0], v[1]), f2 = max(f0, f1);
    for (int i = 2; i < n; i++)
    {
        f2 = max(f1, f0 + v[i]);
        f0 = f1, f1 = f2;
    }
    return f2;
}

三步問題

題目[Interview-0801]:點選檢視 題目

解題思路

與上面的「爬樓梯」一模一樣,是斐波那契數列的變種。需要注意的是:資料溢位,需要使用 uint64_t 作為資料型別。

程式碼實現

#define MODNUM (1000000007)
class Solution
{
public:
    int waysToStep(int n)
    {
        if (n <= 1)  return 1;
        if (n == 2)  return 2;
        uint64_t f0 = 1, f1 = 1, f2 = 2, f3 = 4;
        for (int i = 3; i <= n; i++)
        {
            f3 = (f0 + f1 + f2) % MODNUM;
            f0 = f1, f1 = f2, f2 = f3;
        }
        return f3;
    }
};

相關文章