1. 程式人生 > >【Leetcode 動態規劃】 不知如何分類 就都放這裡了

【Leetcode 動態規劃】 不知如何分類 就都放這裡了

312. Burst Balloons

Given n balloons, indexed from 0 to n-1. Each balloon is painted with a number on it represented by array nums. You are asked to burst all the balloons. If the you burst balloon i you will get nums[left] * nums[i] * nums[right] coins. Here left and right are adjacent indices of i

. After the burst, the left and right then becomes adjacent.

Find the maximum coins you can collect by bursting the balloons wisely.

Note: 
(1) You may imagine nums[-1] = nums[n] = 1. They are not real therefore you can not burst them.
(2) 0 ≤ n ≤ 500, 0 ≤ nums[i] ≤ 100

題意:
給定n個氣球。每次你可以打破一個,打破第i個,那麼你會獲得nums[left] * nums[i] * nums[right]個積分。 如果旁邊沒有氣球了,則按1算,以此類推,求能得到的最多金幣數

思路:

我們維護一個二維動態陣列dp,其中dp[i][j]表示打爆區間[i,j]中的所有氣球能得到的最多金幣。題目中說明了邊界情況,當氣球周圍沒有氣球的時候,旁邊的數字按1算,這樣我們可以在原陣列兩邊各填充一個1,這樣方便於計算。【即新建的vector的大小是n+2維! 如果還是n維 就會超時】

遞推式為:dp[i][j] = max(dp[i][j],  dp[i][k - 1] + nums[i - 1]*nums[k]*nums[j + 1] + dp[k + 1][j])                 ( i ≤ k ≤ j )

含義:打破i~j間氣球所得金幣 = 之前的or打破i~k-1間氣球所得金幣 + 打破k+1~j間氣球所得金幣

nums[i - 1]*nums[k]*nums[j + 1] 這次打破k的:因為i~k-1破了,所以他的左邊相鄰是nums[i-1],同理,右邊相鄰是nums[j+1]

程式碼:

注意點:1.新建的dp陣列必須是n+2維;2.left的範圍是1~(n-打破長度+1),而不是1~n【會超時】;3.return的是dp[1][n]【本應是dp[0][n-1],但因為前面加了邊界的1,所以~

class Solution {
public:
    int maxCoins(vector<int>& nums) {
        int n = nums.size();
        if(n == 0) return 0;
        nums.insert(nums.begin(), 1);
        nums.insert(nums.end(), 1);
        vector<vector<int> > dp (n + 2, vector<int>(n + 2, 0));
        
        for(int len = 1; len <= n; ++len) {
            for(int left = 1; left <= n - len + 1; ++left) {
                int right = left + len - 1;
                for(int k = left; k <= right; ++k) {
                    dp[left][right] = max(dp[left][right],  dp[left][k-1] + nums[left-1]*nums[k]*nums[right+1] + dp[k + 1][right]);
                }
            }
        }
        return dp[1][n];
    }
};

322. Coin Change

You are given coins of different denominations and a total amount of money amount. Write a function to compute the fewest number of coins that you need to make up that amount. If that amount of money cannot be made up by any combination of the coins, return -1.

Example 1:
coins = [1, 2, 5], amount = 11
return 3 (11 = 5 + 5 + 1)

Example 2:
coins = [2], amount = 3
return -1.

Note:
You may assume that you have an infinite number of each kind of coin.

題意:給一組nums代表所擁有的硬幣面額,amount代表和。求組成amount所需的硬幣最小數目,若不能組成amount則返回-1。

思路:

dp[i][v], which is the minimum number of coins used to sum up to v, and i is the number of different denominations used for this sum (use the first i denominations in coins).

f[i][v] = min {case1, case2}

Case1 is f[i-1][v], where coins[i] isn't used;

Case2 is f[i][v-coins[i-1]]+1, where coins[i] is used (and can be used multiple times)

So the weight-lifting work is now finished, and all we have to do is to iterate i from 1 to coins.size() (set count[0] to 0, obviously), while for each i we update coins[v] from v=cost[i-1] to v=amount (no need to start from v=0, because count[v-coins[i-1]] is meaningful only when v-coins[i-1]>0

After finishing all this, return count[amount] as the final result (if count[amouint] is still INT_MAX, which means the search fails, then return -1)
class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        vector<int> dp(amount + 1, amount + 1);//初值amount+1,表達INT_MAX的功能,若用後者會有潛在風險,因為後文會用到該值+1,則變成0x80000000,最小值了
        dp[0] = 0;
        
        for(int i = 0; i < coins.size(); ++i) {
            for(int v = coins[i]; v <= amount; ++v)
                dp[v] = min(dp[v], dp[v - coins[i]] + 1);
        }
        return (dp[amount] == amount + 1)? -1: dp[amount];
    }
};


338. Counting Bits

Given a non negative integer number num. For every numbers i in the range 0 ≤ i ≤ num calculate the number of 1's in their binary representation and return them as an array.

Example:
For num = 5 you should return [0,1,1,2,1,2].

意思是給你一個非負整數num,對於 0~num 這(num+1)個整數,求出每個數用二進位制表示時1的個數。
最簡單的思路:對每個數,利用移位和按位與(i & 1)運算,計算1的個數。這樣時間複雜度為O(n*sizeof(integer)),如果int用32位表示,那麼時間複雜度就是O(32n)。
考慮優化成O(n):

對於11這個數,我們暫時用一個位元組來表示

11:           0000 1011
11/2 = 5: 0000 0101

容易發現,除了11最右邊那個位和5的最高位,其他位對應一樣。也就是說i用二進位制表示時1出現的次數等於i/2中1出現的次數加1(如果i用二進位制表示時最右邊一位為1,否則不加1)。這樣我們在計算i時可以利用前面已計算出的i/2:ret[i] = ret[i/2] + (i % 2 == 0 ? 0 : 1)  即 ret[i] = ret[i/2] + i % 2。
vector<int> countBits(int num) {  
    vector<int> ret(num + 1, 0);  
    for(int i = 1; i <= num; ++i)  
        ret[i] = ret[i>>1] + i % 2;  
    return ret;   
}


343. Integer Break

Given a positive integer n, break it into the sum of at least two positive integers and maximize the product of those integers. Return the maximum product you can get.

For example, given n = 2, return 1 (2 = 1 + 1); given n = 10, return 36 (10 = 3 + 3 + 4).

Note: You may assume that n is not less than 2 and not larger than 58.

Hint:

  1. There is a simple O(n) solution to this problem.
  2. You may check the breaking results of n ranging from 7 to 10 to discover the regularities.
這道題給了我們一個正整數n,讓我們拆分成至少兩個正整數之和,使其乘積最大,題目提示中讓我們用O(n)來解題,而且告訴我們找7到10之間的規律,那麼我們一點一點的來分析:

從1開始,但是1不能拆分成兩個正整數之和,所以不能當輸出。
2只能拆成1+1,所以乘積也為1。
數字3可以拆分成2+1或1+1+1,顯然第一種拆分方法乘積大為2。
數字4拆成2+2,乘積最大,為4。
數字5拆成3+2,乘積最大,為6。
數字6拆成3+3,乘積最大,為9。
數字7拆為3+4,乘積最大,為12。
數字8拆為3+3+2,乘積最大,為18。
數字9拆為3+3+3,乘積最大,為27。
數字10拆為3+3+4,乘積最大,為36。
....

那麼通過觀察上面的規律,我們可以看出從5開始,數字都需要先拆出所有的3,一直拆到剩下一個數為2或者4,因為剩4就不用再拆了,拆成兩個2和不拆沒有意義,而且4不能拆出一個3剩一個1,這樣會比拆成2+2的乘積小。那麼這樣我們就可以寫程式碼了,先預處理n為2和3的情況,然後先將結果res初始化為1,然後當n大於4開始迴圈,我們結果自乘3,n自減3,根據之前的分析,當跳出迴圈時,n只能是2或者4,再乘以res返回即可:
int integerBreak(int n) {
    if (n == 2 || n == 3)
		return n - 1;
    int res = 1;
    while (n > 4) {
        res *= 3;
        n -= 3;
    }
    return res * n;
}

357. Count Numbers with Unique Digits

Given a non-negative integer n, count all numbers with unique digits, x, where 0 ≤ x < 10n.

Example:
Given n = 2, return 91. (The answer should be the total numbers in the range of 0 ≤ x < 100, excluding [11,22,33,44,55,66,77,88,99])

題意:求n位數中,各個位的數字都不相同的數一共有多少個。

思路:

令 f(n) 為所求結果。
f(1) = 10. (0, 1, 2, 3, ...., 9)
f(2) = 9 * 9. Because for each number i from 1, ..., 9, we can pick j to form a 2-digit number ij and there are 9 numbers that are different from i for j to choose from.
f(3) = f(2) * 8 = 9 * 9 * 8. Because for each number with unique digits of length 2, say ij, we can pick k to form a 3 digit number ijk and there are 8 numbers that are different from i and j for k to choose from.

Similarly f(4) = f(3) * 7 = 9 * 9 * 8 * 7....
...
f(10) = 9 * 9 * 8 * 7 * 6 * ... * 1


f(11) = 0 = f(12) = f(13)....

Hence return f(1) + f(2) + .. + f(n)

class Solution {
public:
    int countNumbersWithUniqueDigits(int n) {
        if(n == 0) return 1;
        
        int result = 10;
        int uniqueNumber = 9;
        int availbleNumber = 9;//即f()
        for(int i = 1; i < n; ++i) {
            availbleNumber *= uniqueNumber;
            result += availbleNumber;
            uniqueNumber--;
            if(uniqueNumber <= 0)
                break;
        }
        return result;
    }
};

376. Wiggle Subsequence

A sequence of numbers is called a wiggle sequence if the differences between successive numbers strictly alternate between positive and negative. The first difference (if one exists) may be either positive or negative. A sequence with fewer than two elements is trivially a wiggle sequence.

For example, [1,7,4,9,2,5] is a wiggle sequence because the differences (6,-3,5,-7,3) are alternately positive and negative. In contrast,[1,4,7,2,5] and [1,7,4,5,5] are not wiggle sequences, the first because its first two differences are positive and the second because its last difference is zero.

Given a sequence of integers, return the length of the longest subsequence that is a wiggle sequence. A subsequence is obtained by deleting some number of elements (eventually, also zero) from the original sequence, leaving the remaining elements in their original order.

Examples:

Input: [1,7,4,9,2,5]
Output: 6
The entire sequence is a wiggle sequence.

Input: [1,17,5,10,13,15,10,5,16,8]
Output: 7
There are several subsequences that achieve this length. One is [1,17,10,13,10,16,8].

Input: [1,2,3,4,5,6,7,8,9]
Output: 2
題意:找出這樣一個序列,其相鄰元素的差值 交替正負(0不算),該序列不要求連續。 

程式碼:(貪婪思想)

class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        int n = nums.size();
        if(n < 2) return n;
        
        int flag = 0;
        int count = 1;
        for(int i = 1; i < n; ++i) {
            if(nums[i] < nums[i -1] && (flag == 0 || flag == 1)) {
                flag = -1;
                ++count;
            }
            if(nums[i] > nums[i - 1] && (flag == 0 || flag == -1)) {
                flag = 1;
                ++count;
            }
        }
        return count;
    }
};


96. Unique Binary Search Trees

Given n, how many structurally unique BST's (binary search trees) that store values 1...n?

For example,
Given n = 3, there are a total of 5 unique BST's.

   1         3     3      2      1
    \       /     /      / \      \
     3     2     1      1   3      2
    /     /       \                 \
   2     1         2                 3
本題使用一維線性規劃解決。

如果n == 0時,結果為0;
如果n == 1時,只有一個節點,結果為1;
如果n == 2時,根節點有兩種選擇,結果為2;
如果n >= 3時,
n個點中每個點都可以作為root,當 i 作為root時,小於 i  的點都只能放在其左子樹中,大於 i 的點只能放在右子樹中,此時只需求出左、右子樹各有多少種,二者相乘即為以 i 作為root時BST的總數。

class Solution {
public:
    int numTrees(int n) {
        if(n <= 2)
            return n;
        vector<int> dp(n + 1, 0);
        dp[0] = 1;
        dp[1] = 1;
        dp[2] = 2;
        
        for(int i = 3; i <= n; ++i) {
            int tmp = 0;
            for(int j = 0; j < i; ++j) {
                tmp += dp[j] * dp[ i - j - 1];
            }
            dp[i] = tmp;
        }
        return dp[n];
    }
};
62. Unique Paths

A robot is located at the top-left corner of a m x n grid (marked 'Start' in the diagram below).

The robot can only move either down or right at any point in time. The robot is trying to reach the bottom-right corner of the grid (marked 'Finish' in the diagram below).

How many possible unique paths are there?


一、題目描述:

給定一個m*n的矩陣,讓機器人從左上方走到右下方,只能往下和往右走,一共多少種走法。

二、解題方法:
//動態規劃:
//設狀態為f[i][j],表示從起點(1;1)到達(i; j)的路線條數,則狀態轉移方程為:
//f[i][j] = f[i-1][j] + f[i][j-1]
class Solution {
public:
	int uniquePaths(int m, int n) {
		vector<vector<int> > f(m, vector<int>(n, 1));
		for (int i = 1; i < m; i++)
			for (int j = 1; j < n; j++)
				f[i][j] = f[i - 1][j] + f[i][j - 1];
		return f[m - 1][n - 1];
	}
};
上面方法的空間複雜度較大為O(m*n),然而通過觀察可以發現,我們每次更新f[i][j]只需要f[i-1][j](同一列)和f[i][j-1](左一列),所以只要儲存當前列和左一列就行,而不是整個m*n矩陣,下面的程式碼可以將空間複雜度優化到O(min(m,n))
class Solution {
	int uniquePaths(int m, int n) {
		if (m > n) return uniquePaths(n, m);
		vector<int> pre(m, 1);
		vector<int> cur(m, 1);
		for (int j = 1; j < n; j++) {
			for (int i = 1; i < m; i++)
				cur[i] = cur[i - 1] + pre[i];
			swap(pre, cur);
		}
		return pre[m - 1];
	}
};
通過進一步的觀察,我們還可以發現,上面程式中的pre[i]就是更新前的cur[i],所以可以進一步優化為:
class Solution {
    int uniquePaths(int m, int n) {
        if (m > n) return uniquePaths(n, m);
        vector<int> cur(m, 1);
        for (int j = 1; j < n; j++)
            for (int i = 1; i < m; i++)
                cur[i] += cur[i - 1]; 
        return cur[m - 1];
    }
}; 
最終優化空間程式為:(其實先遍歷m還是先遍歷n都無所謂的。關鍵是要注意 vector的長度 與 內層for迴圈的長度 是一樣的~!)
class Solution{
public:
	int uniquePaths(int m, int n) {
		if (m == 0 && n == 0)
			return 0;

		vector<int> dp(n, 1);
		for (int i = 1; i < m; i++)
			for (int j = 1; j < n; j++)
				dp[j] = dp[j - 1] + dp[j];

		return dp[n - 1];
	}
};
63. Unique Paths II

Follow up for "Unique Paths":

Now consider if some obstacles are added to the grids. How many unique paths would there be?

An obstacle and empty space is marked as 1 and 0 respectively in the grid.

For example,

There is one obstacle in the middle of a 3x3 grid as illustrated below.

[
  [0,0,0],
  [0,1,0],
  [0,0,0]
]

The total number of unique paths is 2.

Note: m and n will be at most 100.

這道題跟 Unique Paths 差不多,只是這道題給機器人加了障礙,不是每次都有兩個選擇(向右,向下)了。
因為有了這個條件,所以 Unique Paths 中最後一個直接求組合的方法就不適用了,這裡最好的解法就是用動態規劃了。
遞推式還是跟 Unique Paths 一樣,只是每次我們要判斷一下是不是障礙,如果是障礙,則dp[i][j] = 0;
否則還是dp[i][j] = dp[i - 1][j] + dp[i][j - 1]。

class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        int m = obstacleGrid.size();
        int n = obstacleGrid[0].size();
        if(m == 0 || n ==0)
            return 0;
        
        vector<int> dp(n);
        dp[0] = 1;
        for(int i = 0; i < m; ++i) {
            for(int j = 0; j < n; ++j) {
                if(obstacleGrid[i][j] == 1)
                    dp[j] = 0;
                else if(j > 0)
                    dp[j] += dp[j - 1];
            }
        }
        return dp[n - 1];
    }
};
64. Minimum Path Sum

Given a m x n grid filled with non-negative numbers, find a path from top left to bottom right which minimizes the sum of all numbers along its path.

Note: You can only move either down or right at any point in time.

這是動態規劃的問題,由於每次只能向下或者向右移動,因此[i, j]位置時的最小路徑的和等於[i, j-1] 與 [i-1, j]中較小的加上[i, j]位置的數值。

因此遞推公式是grid[i][j] += min(grid[i][j-1],  grid[i-1][j])。

時間複雜度:O(mn)
空間複雜度:O(mn)

class Solution {
public:
    int minPathSum(vector<vector<int>>& grid) {
        int m = grid.size();
        int n = grid[0].size();
        if(m == 0 && n == 0)
            return 0;
        
        vector<vector<int> > dp (m, vector<int>(n, 0));
        dp[0][0] = grid[0][0];
        
        for(int i = 1; i < m; ++i)
            dp[i][0] += grid[i][0] + dp[i - 1][0];
        for(int i = 1; i < n; ++i)
            dp[0][i] += grid[0][i] + dp[0][i - 1];
            
        for(int i = 1; i < m; ++i) {
            for(int j = 1; j < n; ++j) {
                dp[i][j] += grid[i][j] + min(dp[i - 1][j], dp[i][j - 1]);
            }
        }
        return dp[m - 1][n - 1];
    }
};
279. Perfect Squares

Given a positive integer n, find the least number of perfect square numbers (for example, 1, 4, 9, 16, ...) which sum to n.

For example, given n = 12, return 3 because 12 = 4 + 4 + 4; given n = 13, return 2 because 13 = 4 + 9.

這道題說是給我們一個正整數,求它最少能由幾個完全平方陣列成。

用動態規劃Dynamic Programming來做,我們建立一個長度為n+1的一維dp陣列,將第一個值初始化為0,其餘值都初始化為INT_MAX.i從0迴圈到n,j從1迴圈到i+j*j <= n的位置,然後每次更新dp[i+j*j]的值,動態更新dp陣列,其中dp[i]表示正整數i能少能由多個完全平方陣列成,那麼我們求n,就是返回dp[n]即可,也就是dp陣列的最後一個數字,參見程式碼如下:

class Solution {
public:
    int numSquares(int n) {

        vector<int> dp(n + 1, 0x7fffffff);
        for(int i = 0; i * i <= n; ++i)
            dp[i * i] = 1;
        
        for(int i = 1; i <= n; ++i) {
            for(int j = 1; i + j * j <= n; ++j) {
                dp[i + j * j] = min(dp[i] + 1, dp[i + j * j]);
            }
        }
        return dp[n];
    }
};

120. Triangle

Given a triangle, find the minimum path sum from top to bottom. Each step you may move to adjacent numbers on the row below.

For example, given the following triangle

[
     [2],
    [3,4],
   [6,5,7],
  [4,1,8,3]
]

The minimum path sum from top to bottom is 11 (i.e., 2 + 3 + 5 + 1 = 11).

Note:
Bonus point if you are able to do this using only O(n) extra space, where n is the total number of rows in the triangle.

分析:

求一個三角形二維陣列從頂到低端的最小路徑和。每次只能挪一個位置。
我們從低端向頂端計算。設狀態為 S[i][j]表示從從位置 ( i, j ) 出發,到最低端路徑的最小和
狀態轉移方程:S[i][j] = min(S[i+1][j] + S[i+1][j+1]) +S[i][j]
S[0][0]就是要求解的答案。
時間複雜度 O(n^2) ,空間複雜度 O(1)

class Solution {
public:
    int minimumTotal(vector<vector<int> > &triangle) {
        int size = triangle.size();
        // down-to-top
        // 第i層
        for(int i = size - 2;i >= 0;--i){
            // 第i層的第j個元素
            for(int j = 0;j <= i;++j){
                triangle[i][j] += min(triangle[i+1][j], triangle[i+1][j+1]);
            }
        }
        return triangle[0][0];
    }
};

從上面思路的狀態轉移方程中看出:S[i][j] = min(S[i+1][j] + S[i+1][j+1]) +S[i][j]
S[i][j]只與下一行的第j個元素和第j+1個元素相關,i的關係是固定的,因此我們可以省去這一維。
開闢O(N)的陣列,然後規劃的時候使用S[j] = min(S[j+1], S[j) +Triangle[i][j]就可以了。

class Solution {
    public:
    int minimumTotal(vector<vector<int> > &triangle) {
        int n = triangle.size();
        vector<int> dp(triangle.back());//dp初值設為triangle的最後一行
        // down-to-top
        // 第i層
        for(int i = n - 2;i >= 0;--i){
            // 第i層的第j個元素
            for(int j = 0;j <= i;++j){
                dp[j] = min(dp[j], dp[j+1]) + triangle[i][j];
            }
        }
        return dp[0];
    }
};

91. Decode Ways

A message containing letters from A-Z is being encoded to numbers using the following mapping:

'A' -> 1
'B' -> 2
...
'Z' -> 26

Given an encoded message containing digits, determine the total number of ways to decode it.

For example,
Given encoded message "12", it could be decoded as "AB" (1 2) or "L" (12).

The number of ways decoding "12" is 2.

題意:1-26對應於A-Z,這樣一個數字字串可以解碼成只包含A-Z的字串。實現numDecodings(string s)接受數字字串,返回可以解碼的方式數。例如,12可以解碼成AB,也可以解碼成L。這樣12就有兩種解碼方式。但要注意的是像10,就只能解成J了,因為單個的0無法解碼,因此只有一種解碼方式。

思路:每次對於當前的字元判斷是否屬於1-9(0肯定不行,因為0不在1-26中),如果屬於,那麼當前的字元可以被decode,並且和f[n-1]組合,f[n] += f[n-1]
然後對於當前字元和前一個字元組成的字串判斷是否屬於10-26,如果屬於,那麼這兩個字元可以被decode,並且和f[n-2]組合,f[n] += f[n-2]

而result[1]初始化時不要出錯了,它 = (check(s[0]) & check(s[1])) + check(s[0], s[1]); 

class Solution {
public:
    int checkOne(char a){
        return (a == '0') ? 0: 1;
    }
    int checkTwo(char a, char b){
        return (a == '1' || a == '2' && b>= '0' && b <= '6')? 1: 0;
    }
    int numDecodings(string s) {
        int length = s.size();
        if(length == 0) return 0;
        if(length == 1) return checkOne(s[0]);
        
        vector<int> result(length + 1, 0);
        result[0] = checkOne(s[0]);
        result[1] = checkTwo(s[0], s[1]) + (checkOne(s[0]) & checkOne(s[1]));

        for(int i = 2; i < length; ++i) {
            if(checkOne(s[i]))
                result[i] += result[i - 1];
            if(checkTwo(s[i - 1], s[i]))
                result[i] += result[i - 2];
        }
        return result[length - 1];
    }
};

至此可見,有點像斐波那契數列,只需記錄下“上一個”和“上上個”的結果即可,無需O(n)空間。不再贅附程式碼。


72. Edit Distance 程式設計之美里的“字串相似度”

Given two words word1 and word2, find the minimum number of steps required to convert word1 to word2. (each operation is counted as 1 step.)

You have the following 3 operations permitted on a word:

a) Insert a character
b) Delete a character
c) Replace a character

思路:

dp[i][j]指把word1[0..i - 1]轉換為word2[0..j - 1] 的最小運算元。

邊界條件:

dp[i][0] = i; 從長度為 i 的字串轉為空串 要刪除 i 次
dp[0][j] = j. 從空串轉為長度為 j 的字串 要新增 j 次

一般情況:

如果word[i - 1] == word2[j - 1],則dp[i][j] = dp[i - 1][j - 1],因為不需要進行操作,即運算元為0.

如果word[i - 1] != word2[j - 1],則需考慮三種情況,取最小值:

Replace word1[i - 1] by word2[j - 1]: (dp[i][j] = dp[i - 1][j - 1] + 1 (for replacement));
Delete word1[i - 1]:                             (dp[i][j] = dp[i - 1][j] + 1 (for deletion));
Insert word2[j - 1] to word1[0..i - 1]:   (dp[i][j] = dp[i][j - 1] + 1 (for insertion)).

class Solution {
public:
    int minDistance(string word1, string word2) {
        int m = word1.size();
        int n = word2.size();
        vector<vector<int> > dp(m + 1, vector<int>(n + 1, 0));
        
        //邊界條件
        for(int i = 1; i <= m; ++i) //從長度為i的字串轉為空串 要刪除i次~
            dp[i][0] = i;
        for(int j = 1; j <= n; ++j)//從空串轉為長度為j的字串 要新增j次~
            dp[0][j] = j;
        
        for(int i = 1; i <= m; ++i) {
            for(int j = 1; j <= n; ++j) {
                if(word1[i - 1] == word2[j - 1])
                    dp[i][j] = dp[i - 1][j - 1];
                else
                    dp[i][j] = min(dp[i - 1][j - 1] + 1, min(dp[i - 1][j] + 1, dp[i][j - 1] + 1));
            }
        }
        return dp[m][n];
    }
};
可以發現,當我們更新dp[i][j]時,我們只需要dp[i - 1][j - 1], dp[i][j - 1], dp[i - 1][j]。所以,我們不必記錄整個m*n矩陣。事實上,我們只維護一列就夠了。空間複雜度可以被優化為O(m) 【維護一列】或 O(n)【維護一行】。
優化為程式碼為:
class Solution { 
public:
    int minDistance(string word1, string word2) {
        int m = word1.length(), n = word2.length();
        vector<int> cur(m + 1, 0);
        for (int i = 1; i <= m; i++)
            cur[i] = i;
        for (int j = 1; j <= n; j++) {
            int pre = cur[0];
            cur[0] = j;
            for (int i = 1; i <= m; i++) {
                int temp = cur[i];
                if (word1[i - 1] == word2[j - 1])
                    cur[i] = pre;
                else cur[i] = min(pre + 1, min(cur[i] + 1, cur[i - 1] + 1));
                pre = temp;
            }
        }
        return cur[m]; 
    }
}; 

f[i][j] only depends on f[i-1][j-1], f[i-1][j] and f[i][j-1], we can reduce the space to O(n) by using only the (i-1)th array and previous updated element(f [i] [j - 1]).【上法是用一列,所以for迴圈先n後m。此處用一行,for迴圈就先m後n,即內層個數 = vector長度】

int minDistance(string word1, string word2) {
    int m = word1.size();
    int n = word2.size();
    
    vector<int> dp(n+1, 0);
    for (int j = 1; j <= n; ++j)
        dp[j] = j;
    
    for (int i = 1; i <= m; ++i) {
        int prev = i;
        for (int j = 1; j <= n; ++j) {
            int cur;
			
            if (word1[i-1] == word2[j-1])
                cur = dp[j-1];
            else 
                cur = min(min(dp[j-1], prev), dp[j]) + 1;
    
            dp[j-1] = prev;
            prev = cur;
        }
        dp[n] = prev;
    }
    return dp[n];
}  


115. Distinct Subsequences

Given a string S and a string T, count the number of distinct subsequences of T in S.

A subsequence of a string is a new string which is formed from the original string by deleting some (can be none) of the characters without disturbing the relative positions of the remaining characters. (ie, "ACE" is a subsequence of "ABCDE" while "AEC" is not).

Here is an example:
S = "rabbbit"T = "rabbit"

Return 3.

思路:

我們維護dp[i][j],對應的值是S的前i個字元和T的前j個字元有多少個可行的序列(注意這道題是序列,不是子串,也就是隻要字元按照順序出現即可,不需連續出現)。下面來看看遞推式,假設我們現在擁有之前的歷史資訊,我們怎麼在常量操作時間內得到dp[i][j]。

假設S的第i個字元和T的第j個字元不相同,那麼就意味著dp[i][j]的值跟dp[i-1][j]是一樣的,前面該是多少還是多少,而第i個字元的加入也不會多出來任何可行結果。如果S的第i個字元和T的第j個字元相同,那麼所有dp[i-1][j-1]中滿足的結果都會成為新的滿足的序列,當然dp[i-1][j]的也仍是可行結果,所以dp[i][j] = [i-1][j-1] + dp[i-1][j]。

例子:

不同時:S = ABCDE, T = F。此時S[6] != T[1], dp[5][1] = dp[4][1]【即S變成ABCD,少了一個】

相同時:S = ABCDBB, T = AB。 此時S[6] == T[2]。

dp[6][2]【意為S的前6個裡面找T的前2個的次數】 =  dp[5][1] 【T中的B是S中的最後一個B,即就是這個二者相等的B。那麼問題就變成了S = ABCDB, T = A,即dp[5][1]】 +  dp[5][2]【T中的B不是S中的最後一個B,問題就變成了S = ABCDB, T = AB,即dp[5][2]】

所以綜合上面兩種情況,遞推式應該是dp[i][j]=(S[i]==T[j]?dp[i-1][j-1]:0)+dp[i][j]。演算法進行兩層迴圈,時間複雜度是O(m*n)

class Solution {
public:
    int numDistinct(string s, string t) {
        int m = s.size(), n = t.size();
        if(m == 0 || m < n)
            return 0;
            
        vector<vector<int> > dp(m + 1, vector<int>(n + 1, 0));
        for(int i = 0; i < m; ++i)//從任意長度的字串轉化為空串,只有全部刪除這一種方法
            dp[i][0] = 1;

        for(int i = 1; i <= m; ++i) {
            for(int j = 1; j <= n; ++j) {
                if(s[i - 1] == t[j - 1])
                    dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
                else
                    dp[i][j] = dp[i - 1][j];
            }
        }
        return dp[m][n];
    }
};
發現每次更新只跟[i-1]行有關,所以可以優化空間複雜度。
class Solution {
public:
    int numDistinct(string s, string t) {
        int m = s.size(), n = t.size();
        if(m == 0 || m < n)
            return 0;
            
        vector<int> dp(n + 1, 0);
        dp[0] = 1;
        
        for(int i = 1; i <= m; ++i) {
            int pre = 1;
            for(int j = 1; j <= n; ++j) {
                int tmp = dp[j];
                dp[j] = dp[j] + (s[i-1] == t[j-1] ? pre: 0);//+比 三目運算子優先級別要高!!!! 所以要把整個的三目運算子用括號括起來
                pre = tmp;
            }
        }
        return dp[n];
    }
};

131. Palindrome Partitioning

Given a string s, partition s such that every substring of the partition is a palindrome.

Return all possible palindrome partitioning of s.

For example, given s = "aab",
Return

[
  ["aa","b"],
  ["a","a","b"]
]
題意:給出一個string s,把它分割成一些子串,要求每個子串都是迴文的。求所有可能。

思路:DFS,去找所有可能,有點類似劍指offer 28 字串的全排列,只是多了一個判斷條件:是否為迴文。

步驟:從左到右遍歷字串,當傳入起點為index時,判斷s(index,i)作為一個子串是否迴文。若是迴文,將其儲存至tmp中,接著呼叫dfs函式繼續處理後面的部分s(i+1, ...)。當遍歷至結尾,將tmp儲存到result中,作為一種情況。

class Solution {
public:
    bool IsPalindrome(const string s, int start, int end) {
        while(start < end) {
            if(s[start] != s[end])
                return false;
            start++, end--;
        }
        return true;
    }
    
    void dfs(int index, string s, vector<string>& tmp, vector<vector<string> >& result) {
        if(index == s.size()) {
            result.push_back(tmp);
            return;
        }
        for(int i = index; i < s.size(); ++i) {
            if(IsPalindrome(s, index, i)) {
                tmp.push_back(s.substr(index, i - index + 1));//substr的兩個引數:起點,長度
                dfs(i + 1, s, tmp, result);
                tmp.pop_back();
            }
        }
    }
    
    vector<vector<string>> partition(string s) {
        vector<vector<string> > result;
        if(!s.empty()) {
            vector<string> tmp;
            dfs(0, s, tmp, result);
        }
        return result;
    }
};


132. Palindrome Partitioning II

Given a string s, partition s such that every substring of the partition is a palindrome.

Return the minimum cuts needed for a palindrome partitioning of s.

For example, given s = "aab",
Return 1 since the palindrome partitioning ["aa","b"] could be produced using 1 cut.

題意