1. 程式人生 > >leetcode:貪心、動態規劃、記憶化搜尋

leetcode:貪心、動態規劃、記憶化搜尋

貪心的基本概念

所謂貪心演算法,是指在對問題求解時,總是做出在當前看來是最好的選擇。也就是說,不從整體最優上加以考慮,他所做出的僅是在某種意義上的區域性最優解。
貪心演算法沒有固定的演算法框架,演算法設計的關鍵是貪心策略的選擇。必須注意的是,貪心演算法不是對所有問題都能得到整體最優解,選擇的貪心策略必須具備無後效性,即某個狀態以後的過程不會影響以前的狀態,只與當前狀態有關。
所以對所採用的貪心策略一定要仔細分析其是否滿足無後效性。

貪心策略適用的前提是:區域性最優策略能導致產生全域性最優解。
實際上,貪心演算法適用的情況很少。一般,對一個問題分析是否適用於貪心演算法,可以先選擇該問題下的幾個實際資料進行分析,就可做出判斷。

基於貪心演算法的幾類區間覆蓋問題

區間完全覆蓋問題

問題描述:
給定一個長度為m的區間,再給出n條線段的起點和終點(注意這裡是閉區間),求最少使用多少條線段可以將整個區間完全覆蓋。
樣例:
區間長度8,可選的覆蓋線段[2,6],[1,4],[3,6],[3,7],[6,8],[2,4],[3,5]
解題過程:
1、將每一個區間按照左端點遞增順序排列,排完序後為[1,4],[2,4],[2,6],[3,5],[3,6],[3,7],[6,8];
2、設定一個變量表示已經覆蓋到的區域。在剩下的線段中找出所有左端點小於等於當前已經覆蓋到的區域的右端點的線段中,右端點最大的線段

,加入,直到已經覆蓋全部的區域。
過程:
假設第一步加入[1,4],那麼下一步能夠選擇的有[2,6],[3,5],[3,6],[3,7],由於7最大,所以下一步選擇[3,7],最後一步只能選擇[6,8],這個時候剛好達到了8,於是退出,所選區間為3。

最大不相交覆蓋

問題描述:
給定一個長度為m的區間,再給出n條線段的起點和終點(開區間和閉區間處理的方法不同,這裡以開區間為例),問題是從中選取儘量多的線段,使得每個線段都是獨立不相交的。
樣例:
區間長度8,可選的覆蓋線段[2,6],[1,4],[3,6],[3,7],[6,8],[2,4],[3,5]
解題過程:
1、對線段的右端點進行升序排序


2、從右端點第二大的線段開始,選擇左端點最大的那一條,如果加入以後不會跟之前的線段產生公共部分,那麼就加入,否則就繼續判斷後面的線段。
過程:

  1. 排序:將每一個區間按右端點進行遞增順序排列,排完序後為[1,4],[2,4],[3,5],[2,6],[3,6],[3,7],[6,8];
  2. 第一步選取[2,4],發現後面只能加入[6,8],所以區間的個數為2。
const int N=100000+10;
int n,s[N]={0},t[N]={0};

int main() {
    vector<P> v;

    while(cin>>n)) {
        for(int i=0;i<n;i++)
        {
            cin>>s[i]>>t[i];
            v.push_back(P(s[i],t[i]));
        }
        //按照右端點的升序排序
        sort(v.begin(),v.end(),myCmp());
        int ans=0;
        int t=0;
        for(int i=0;i<n;i++)
        {
            if(t<v[i].first) 
            {
                ans++;
                t=v[i].second;//前一個線段的右端點
            }
        }
        cout<<ans<<endl;
    }
}

區間選點問題

問題描述:
數軸上有n個閉區間[Ai,Bi],取儘量少的點,使得每個區間都至少有一個點。
樣例
輸入:n=5, [1,5], [8,9], [4,7], [2,6], [3,5]
輸出:2 (選擇點5,9即可)
貪心策略:把所有區間按照B從小到大排序,如果B相同,按照A從大到小排序,每次都取第一個區間中的最後一個點。

const int N=10000+10;
struct Node {
    int L,R;
    bool operator<(const Node& rhs) const 
    {
        return R<rhs.R || (R==rhs.R && L>rhs.L);
    }
}a[N];

int main()
{
    int n;
    while(cin>>n) {
        me(a);
        for(int i=0;i<n;i++)
            scanf("%d%d",&a[i].L,&a[i].R);
        sort(a,a+n);
        int ans=0,p=0;
        for(int i=0;i<n;i++) {
            if(p<a[i].L) {
                p=a[i].R;
                ans++;
            }
        }
        cout<<ans<<endl;
    }
}

動態規劃的基本概念

動態規劃是利用儲存歷史資訊使得未來需要歷史資訊時不需要重新計算, 從而達到降低時間複雜度, 用空間複雜度換取時間複雜度的方法。可以把動態規劃分為以下幾步:

  1. 確定遞推量。 這一步需要確定遞推過程中要保留的歷史資訊數量和具體含義, 同時也會定下動態規劃的維度;
  2. 推導遞推式。 根據確定的遞推量, 得到如何利用儲存的歷史資訊在有效時間(通常是常量或者線性時間)內得到當前的資訊結果;
  3. 計算初始條件。 有了遞推式之後, 我們只需要計算初始條件, 就可以根據遞推式得到我們想要的結果了。 通常初始條件都是比較簡單的情況, 一般來說直接賦值即可;

動態規劃的時間複雜度是O((維度)×(每步獲取當前值所用的時間複雜度))。 基本上按照上面的思路, 動態規劃的題目都可以解決, 不過最難的一般是在確定遞推量, 一個好的遞推量可以使得動態規劃的時間複雜度儘量低。

記憶化搜尋的基本概念

記憶化搜尋=搜尋的形式+動態規劃的思想。

記憶化搜尋的思想是,在搜尋過程中,會有很多重複計算,如果我們能記錄一些狀態的答案,就可以減少重複搜尋量。最典型的記憶化搜尋的應用就是滑雪問題,見下題329。

DP是從下向上求解的,而記憶化搜尋是從上向下的,因為它用到了遞迴。

329. 矩陣中最長的上升路徑(滑雪問題)

思路:
可看作滑雪問題,因為求最長的上升路徑也可以理解成求最長的下降路徑。

這道題給我們一個二維陣列,讓我們求矩陣中最長的遞增路徑,規定我們只能上下左右行走,不能走斜線或者是超過了邊界。那麼這道題的解法要用記憶化搜尋來做,可以避免重複計算。
我們需要維護一個二維動態陣列dp,其中dp[i][j]表示陣列中以(i,j)為終點的最長遞增路徑的長度(不包括自己),初始將dp陣列都賦為0,當我們用遞迴呼叫時,遇到某個位置(x, y), 如果dp[x][y]不為0的話,我們直接返回dp[x][y]即可,不需要重複計算。
我們需要以陣列中每個位置都為終點呼叫遞迴來做,比較找出最大值。在以一個位置為起點用DFS搜尋時,對其四個相鄰位置進行判斷,如果相鄰位置的值小於該位置,則對相鄰位置繼續呼叫遞迴,求該相鄰位置的dp。並更新當前該位置的dp,搜素完成後返回即可。
注意最後是返回res+1,因為實際上最長遞增路徑的終點也是要算進去的。

class Solution {
public:
    int longestIncreasingPath(vector<vector<int> >& matrix) {
        if (matrix.empty() || matrix[0].empty()) 
            return 0;
        int res = 0;
        int m = matrix.size();
        int n = matrix[0].size();

        dp.resize(m, vector<int>(n, 0));

        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                res = max(res, dfs(matrix, i, j));
            }
        }
        return res+1;
    }

    int dx[4]={0,0,-1,1};  
    int dy[4]={1,-1,0,0};
    vector<vector<int> > dp;

    int dfs(vector<vector<int> > &matrix, int i, int j) 
    {
        if (dp[i][j]) return dp[i][j];

        int m = matrix.size();
        int n = matrix[0].size();
        int x,y;

        for(int k=0;k<4;k++)
        {
            x = i + dx[k];
            y = j + dy[k];
            if (x >= 0 && x < m && y >= 0 && y < n && matrix[i][j] > matrix[x][y] )
                dp[i][j] = max(dp[i][j], 1 + dfs(matrix, x, y));
        }
        return dp[i][j];
    }
};

用DP和記憶化搜尋解最長公共子序列(LCS)

DP

int dp[MAXN][MAXN];  
string str1, str2;  

int main(void) 
{  
    cin >> str1 >> str2;  

    for(int i=1; i<=str1.size(); ++i) 
    {  
        for(int j=1; j<=str2.size(); ++j) 
        {
            if(str1[i] == str2[j])  
                dp[i][j] = dp[i-1][j-1]+1;  
            else dp[i][j] = max(dp[i-1][j], dp[i][j-1]);  
        }  
    }  
    cout << dp[str1.size()][str2.size()] << endl;  
    return 0;  
}  

記憶化搜尋

int dp[MAXN][MAXN];  
string str1, str2;  

int LookUp(int i, int j) { 
    if(dp[i][j]) 
        return dp[i][j]; 
    if(i==0 || j==0) 
        return 0; 
    if(str1[i-1] == str2[j-1]) { 
        dp[i][j] = LookUp(i-1, j-1)+1; 
    } 
    else dp[i][j] = max(LookUp(i-1, j), LookUp(i, j-1)); 
    return dp[i][j]; 

}

int main(void) 
{ 
    cin >> str1 >> str2;  
    LookUp(str1.size(), str2.size());  
    cout << dp[str1.size()][str2.size()] << endl;  
    return 0;  
}  

11. 盛最多的水

給定n個非負整數a1,a2,…,an,其中每個代表一個點座標(i,ai)。n個垂直線段,線段的兩個端點在(i,ai)和(i,0)。在x座標上找到兩個線段,與x軸形成一個容器,使其包含最多的水。
思路
這是一個貪心策略,每次取兩邊圍欄最矮的一個推進,希望獲取更多的水。
容器的寬是兩個點的橫座標之差,高是兩個點中較短的那個。初始狀態是left=0,right=n,然後逐漸朝裡靠攏。對於某一次的狀態,假設

height[left]<height[right]

那麼就不應該是right–,而應該是left++,因為right–不可能使容積變大,此時的短板在left那邊。所以每次移動時,都是移動較短的那一邊。最後,當left+1=right時結束,返回整個過程中容積的最大值。

class Solution {
public:
    int maxArea(vector<int>& height) {
        int AreaMax=0;
        int temp=0;
        int left=0;
        int right=height.size()-1;
        AreaMax = (right-left)*min(height[left],height[right]);

        while(left<right)
        {
            height[left] < height[right] ? left++ : right--;
            temp = (right-left)*min(height[left],height[right]);
            if(AreaMax<temp)    
                AreaMax=temp;
        }
        return AreaMax;
    }
};

343. 拆分整數,使乘積最大

已知n(n>=2),求相加等於n且乘積最大的一組整數的積。
思路
可以用數學求導解決,也可以用DP思想。

記dp[n]為n對應的最大乘積。
那麼遞推方程就是:dp[n]=max(i*dp[n-i],i*(n-i))(其中i從1到n-1)。
邊界:dp[2]=1;

class Solution {
public:
    int integerBreak(int n) {
        vector<int> dp(n+5,0);
        dp[1]=1;
        dp[2]=1;
        for(int i=3;i<=n;i++)
        {
            for(int j=1;j<i;j++)
            {
                dp[i]=max(dp[i],max(j*dp[i-j],j*(i-j)));
            }
        }
        return dp[n];
    }
};

96. 結點數為n的二叉搜尋樹的數目

已知整數n,返回結點數為n的結構不同的二叉搜尋樹的數目。

思路
對於一顆二叉樹,簡單來說可以分為:

     根結點
    /     \
 左子樹   右子樹

假如整個樹有 n 個結點,根結點為 1 個結點,兩個子樹平分剩下的 n-1 個結點。
假設我們已經知道結點數量為 x 的二叉樹有dp[x]種不同的形態。
則一顆二叉樹左結點數量為 k 時,其形態數為dp[k] * dp[n - 1 - k]
而對於一顆 n 個結點的二叉樹,其兩個子樹分配結點的方案有 n-1 種:

(0, n-1), (1, n-2), ..., (n-1, 0)

因此我們可以得到對於 n 個結點的二叉樹,其形態有:

//求和
Sigma(dp[i] * dp[n-1-i]) | i = 0 .. n-1

邊界條件為dp[0] = 1。

class Solution {
public:
    int numTrees(int n) {
        int dp[n+1] = {0};
        if(n == 0) return 1;
        if(n == 1) return 1;
        if(n == 2) return 2;

        dp[0] = 1;
        dp[1] = 1;
        dp[2] = 2;
        for(int i = 3;i <= n;i++)
        {
            for(int j=0;j<i;j++)
                dp[i] += dp[j]*dp[i-1-j];
        }
        return dp[n];
    }
};

70. 上臺階

上n層臺階,一次可以上1層,也可以上2層。問上n層臺階總共有多少種不同的方式。

思路
經典的dp問題。其實是一個斐波那契數列。

class Solution {
public:
    int climbStairs(int n) {
        int i;
        int a = 0, b = 1, c;
        if (n <= 1)
            return n;

        for (i = 2; i <= n; i++) 
        {
            c = a + b;
            a = b;
            b = c;
        }
        return c;
    }
};

進階版:
如果題目變成一次可以上1層,也可以上2層……也可以上n層。問上n層臺階總共有多少種方式。
思路
找規律,可以發現f(n)=f(n-1)+f(n-2)+……f(0)=2*f(n-1)
所以:

class Solution {
public:
    int climbStairs(int n) {
        if(number<=0)
           return 0;
        if(number==1)
            return 1;
         int sum=1<<(number-1);//位運算代替*2  1<<n 意思為2的n次方
        return sum;
    }
};

322. 組成某個數的最少紙幣數(Java)

給定不同面額的紙幣,每種面額可以有無限張,給定一個數額,要求組成該數額的最少的紙幣數。
思路:
用dp[i]儲存組成金額i所需要的紙幣數量,當滿足:

  1. i>=coins[j]
  2. dp[i-coins[j]] != INT_MAX,即可以用紙幣組成金額i-coins[j]

時,遞推公式為:
dp[i] = min(dp[i], dp[i - coins[j]] + 1)

public class Solution {
    public int coinChange(int[] coins, int amount) {  
        int[] dp = new int[amount + 1];

        for (int i = 1; i <= amount; i++) 
        {  
            dp[i] = Integer.MAX_VALUE;  
                //i大於等於紙幣面額,而且i-coins[j]是可以被組成的  
                if (i >= coins[j] && dp[i - coins[j]] != Integer.MAX_VALUE)
                    dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);  
        }  
     return dp[amount] == Integer.MAX_VALUE ? -1 : dp[amount];  
    }  
}

53. 和最大的子串

Find the contiguous subarray within an array (containing at least one number) which has the largest sum.
For example, given the array [−2,1,−3,4,−1,2,1,−5,4],
the contiguous subarray [4,−1,2,1] has the largest sum = 6.

思路
可以用動態規劃的思路解決。
記r[i]表示以nums[i]結尾的子串中最大的那個和,r[i]應該怎麼求呢?r[i]可以表示成

r[i]=max(r[i-1]+nums[i],nums[i]);

於是問題就變成求r中的最大值。

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        vector<int> r(nums.size());
        int result=nums[0];
        r[0]=nums[0];
        //從第2個數開始,求以nums[i]結尾的子串的最大值
        for(int i=1;i<nums.size();i++) 
        {
            r[i]=max(r[i-1]+nums[i],nums[i]);
            result=max(result,r[i]);          
        }
        return result;
    }

};

309. 買賣股票的最佳策略(帶cooldown)

Say you have an array for which the ith element is the price of a given stock on day i.

Design an algorithm to find the maximum profit. You may complete as many transactions as you like (ie, buy one and sell one share of the stock multiple times) with the following restrictions:

You may not engage in multiple transactions at the same time (ie, you must sell the stock before you buy again).
After you sell your stock, you cannot buy stock on next day. (ie, cooldown 1 day)
Example:

prices = [1, 2, 3, 0, 2]
maxProfit = 3
transactions = [buy, sell, cooldown, buy, sell]

思路
狀態的跳轉是依據時間的跳轉,即第i天的收益情況依賴於第i-1天的收益情況。不過現在需要三個狀態,即buy,sell,cooldown(滿倉、空倉、空倉並且已冷卻過)。
我們記錄第i-1天的這三個狀態的收益情況是last_ buy,last_sell,last_cooldown。那麼第i天的這三個收益情況的依賴關係是:

buy=max(last_buy, last_cooldown - price[i]);
sell = max(last_sell, last_buy + price[i]); 
cooldown = max(last_cooldown, last_sell); 

程式碼如下:

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int last_sell = 0, last_buy = INT_MIN, last_cooldown = 0, sell = 0, buy = 0, cooldown = 0;
        for(auto price:prices) {
            buy = max(last_buy, last_cooldown - price);
            sell = max(last_sell, last_buy + price);
            cooldown = max(last_cooldown, last_sell);
            last_buy = buy;
            last_sell = sell;
            last_cooldown = cooldown;
        }
        return sell;
    }
};

121. 買賣股票的最佳策略(只能買賣一次)

思路
假設最佳策略是在第i天做出的,那麼該天的最大收益是“在第1到i-1天中價格最低的時候買入,當天賣出”得到的。
記lowest為到目前為止的最低價;記m為到目前為止能取得的最大收益。
從頭到尾遍歷prices,最後得到的m即為所求。

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        if(prices.size()<=1) return 0;
        int lowest=prices[0];
        int m=0;

        for(int i=1;i<prices.size();i++){
            m=max(m,prices[i]-lowest);
            lowest=min(lowest,prices[i]);
        }
        return m;
    }
};

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.

思路
記dp[l][r]表示扎破(l, r)範圍內(不含邊界l和r)所有氣球獲得的最大硬幣數。
假設第k氣球破了,num[k-1]和num[k+1]會變成相鄰的,如果此時踩num[k-1]或者num[k+1],則都會受到另一個子整體的影響,這樣的話,兩個子問題就不獨立,也就不能用分治了。
可以發現:

N1和N2相互獨立 <=> k點是整體N中最後一個被踩破的氣球。

也就是k點被踩破之前,N1和N2的氣球都不會相互影響。於是我們就成功構造了子問題。因此分治加dp就可以對問題進行求解了。

寫一下狀態傳遞方程:

dp[left][right] = max{dp[left][right] , nums[left] * nums[i] * nums[right]  +  nums[left] * nums[i]  +  nums[i] * nums[right]};

其中 left<i<right , dp[left][right]即為當前子問題:第left和第right之間位置的氣球的maxcoin。

l與r的跨度k是從2開始逐漸增大的。

當k = 2時,l = 0, r = 2; l = 1, r = 3; ……
當k = 3時,l = 0, r = 3; l = 1, r = 4;…..
…..
當k = n-1時,l=0,r=n-1。

此時的dp[0][n-1]即為所求。

如果用(n-1)*(n-1)矩陣形象解釋的話,就是考察主對角線及以上部分。從dp[0][2]、dp[1][3]…那條斜線開始,不斷給對角線賦值。最右上角的dp[0][n-1]即為所求。

class Solution {  
public:  
    int maxCoins(vector<int>& nums) {  
        int arr[nums.size()+2];  
        //重新建立一個大小為n+2的空間
        for(int i=1;i<nums.size()+1;++i) arr[i] = nums[i-1];  
        arr[0] = arr[nums.size()+1] = 1;  

        int dp[nums.size()+2][nums.size()+2]={};  
        int n = nums.size()+2;  
        //跨度k從2到n-1
        for(int k=2;k<n;++k) {  
            //left從0到n-k
            for(int left = 0;left<n-k;++left) { 
                //right是left+k
                int right = left + k; 
                //對於dp[left][right],要根據最後破的氣球的位置i,確定當前的maxCoins
                for(int i=left+1;i< right; ++i) {  
                    dp[left][right] = max(dp[left][right],arr[left]*arr[i]*arr[right] + dp[left][i] + dp[i][right]);  
                }  
            }      
        }  
        return dp[0][n-1];  
    }  
};  

64. 最小路徑和

給定一個只含非負整數的m*n網格,找到一條從左上角到右下角的可以使數字和最小的路徑。
每次只能向下或者向右移動一步。
思路
構建一個m*n的矩陣,每個點表示從左上角到該位置的最小路徑和。然後,遞推式是
sum[i][j]=min(sum[i-1][j],sum[i][j-1])+grid[i][j];
值得注意的是,當i=0或j=0時,要考慮下邊界情況。

PS:
如果是要求最大路徑和,只要把遞推式改成:

sum[i][j]=max(sum[i-1][j],sum[i][j-1])+grid[i][j];
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>> sum(m,vector<int>(n,0));

        for(int i=0;i<m;i++){
            for(int j=0;j<n;j++){
                if(i==0 && j==0)
                {
                    sum[i][j]=grid[i][j];
                    continue;
                }
                if(i==0)
                    sum[i][j]=sum[i][j-1]+grid[i][j];
                else if(j==0) 
                    sum[i][j]=sum[i-1][j]+grid[i][j];
                else 
                    sum[i][j]=min(sum[i-1][j],sum[i][j-1])+grid[i][j];
            }
        }
        return sum[m-1][n-1];     
    }
};

PSS:
這裡用到了m*n的輔助陣列sum,實際上如果允許改變原陣列grid的話,只需要原地更新grid[i][j]就可以了,而不需要用到sum。
又或者,如果要用輔助空間,也不用開m*n,而是定義一個一維的大小為n的陣列pre,加上一個cur變數,用來儲存s[i][j-1]中的元素。

class Solution {
public:
    int minPathSum(vector<vector<int>>& grid)  
{  
    if (grid.empty())  
        return 0;  
    int rows = grid.size();  
    int cols = grid[0].size();
    vector<int> pre(cols, grid[0][0]);  
    //儲存s[i][j-1]中的元素
    int cur = grid[0][0];  
    //根據第0行初始化pre  
    for (int i = 1; i < cols; ++i)  
    {  
        pre[i] = pre[i - 1] + grid[0][i];  
    }  
    //獲得s[i][j]的最小值  
    for (int i = 1; i < rows; ++i)  
    {  
        cur = grid[i][0] + pre[0];  
        pre[0] = cur;  
        for (int j = 1; j < cols; ++j)  
        {  
            cur = min(cur, pre[j]) + grid[i][j];  
            pre[j] = cur;  
        }  
    }  
    return pre[cols - 1];  
}  
};

300. 最長上升子序列(Longest Increasing Subsequence)

求一個未排序序列中的最長上升子序列(不要求連續)
思路
建立一個數組lol[n],初始化為1,用於儲存到當前元素為止,以當前元素結尾的子序列的長度。當陣列全部計算完畢後,找出其中的最大值,即為所求。
狀態轉移方程為:
lol[i]=max(lol[i],lol[j]+1);

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int n=nums.size(),i,j;
        if(n<=1) return n;
        int lol[n],maxx=0;
        //給lol的所有元素賦初值1
        for(i=0;i<n;i++) lol[i]=1;
        for(i=1;i<n;i++) {
            for(j=0;j<i;j++) {
                if(nums[i]>nums[j])
                    lol[i]=max(lol[i],lol[j]+1);
            }
            maxx = max(maxx,lol[i]);
        }

        return maxx;
    }

};

279. 和為n的最少的完全平方數

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.

思路
動態規劃。
如果一個數x可以表示為一個任意數a加上一個平方數b∗b,也就是x=a+b∗b,那麼能組成這個數x最少的平方數個數,就是能組成a最少的平方數個數再加上1(因為b∗b已經是平方數了)。

class Solution {  
public:  
    int numSquares(int n) {  
        vector<int> dp(n+1,0); 
        //最壞情況,n全由1組成
        for(int i=0;i<n+1;i++) dp[i]=i;

        for(int a=0;a<=n;a++)  
            for(int b=1;a+b*b<=n;b++)  
                dp[a+b*b]=min(dp[a+b*b],dp[a]+1);//要麼本身,要麼加一個平方數   
        return dp[n];  
    }  
};

357. 各位數字都不同的數

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

思路
題目要找出 0≤ x < 10^n中各位數字都不相同的數的個數。要解這道題只需要理解兩點:

  • 設f(n)表示n位數字中各位都不相同的個數,則有
countNumbersWithUniqueDigits(n) = f(n)+……+f(2)+f(1)
 = f(n)+countNumbersWithUniqueDigits(n-1);
  • 對於f(n),由於首位不能為0,之後n-1位可以選不重複的任意數字,所以總的可能性為9*9*8*……(n超過10則這樣的數不存在);

    理解了以上兩點,這道題就很好解出。

class Solution {
public:
    int countNumbersWithUniqueDigits(int n) {
        return f(n);
    }

    int f(int n){
        if(n==0) return 1;
        int num_n=0;
        int temp=n;
        //位數不能超過10位,否則沒有滿足條件的數
        if(n>0 && n<10){
            int c=9;
            num_n=9;
            n--;
            while((n--)>0){
                num_n *= (c--);
            }
        }
       return num_n+f(temp-1);
    }

};

392. 判斷是否是子串(Java)

Given a string s and a string t, check if s is subsequence of t.
思路
從頭至尾考察字串t。如果s是t的子串,那麼在遍歷t的過程中,一定能按順序找到所有s的字元。

public class Solution {  
    public boolean isSubsequence(String s, String t) {  
        if (s.length()==0) {  
            return true;  
        }  
        int is = 0,it = 0;  
        while(it<t.length()){  
            if (s.charAt(is)==t.charAt(it)) 
            {  
                is++;  
                if (is==s.length()) 
                {
                    return true;  
                }  
            }  
            it++;  
        }  
        return false;  
    }  
}  

416. 平分數列

Given a non-empty array containing only positive integers, find if the array can be partitioned into two subsets such that the sum of elements in both subsets is equal.
思路
用揹包問題的思路來思考。首先:

  • 陣列的和必須要是偶數,否則無法劃分。共計n個數,這裡value和weight都設為等於nums[i]
  • 將問題轉化為揹包問題,即取前i個數(物品),在揹包體積為j的前提下,dp[i][j]的最大值dp[i][j]=max{ dp[i-1][j], dp[i-1][j-weight[i]]+value[i] }
  • 最後,如果dp[n][sum/2] 等於sum/2,就證明在使用了這n個數下,正好能加出一個sum/2,符合題意

在下面的具體實現中,沒有采用二維陣列dp[][],而是使用了簡化了的一維陣列dp[],效果是一樣的:

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int length=nums.size();
        if(length==0) return true;
        if(length==1) return false;
        int value[length];
        int weight[length];
        int sum=0;
        for(int i=0;i<length;i++)
        {
            value[i]=nums[i];
            weight[i]=nums[i];
            sum += nums[i];
        }
        if(sum%2) return false;//如果sum是奇數,那麼就不可能平分
        int dp[sum/2+1]={0};
        for(int i=0;i<length;i++)
            for(int j=sum/2;j>=0;j--)
                if(weight[i]<=j)
                    dp[j] = max(dp[j],value[i]+dp[j-weight[i]]);
        return dp[sum/2]==(sum/2);
    }
};

377. 組合的和IV(Java)

Given an integer array with all positive numbers and no duplicates, find the number of possible combinations that add up to a positive integer target.

Example:

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

The possible combination ways are:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)

Note that different sequences are counted as different combinations.

Therefore the output is 7.
思路
記dp[i]表示和為i的組合的個數,則有dp[i]=dp[i-j1]+dp[i-j2]+....+dp[i-jn]+k,其中j1,j2…jn是nums陣列中比target小的數,而k則可以表示成:
如果存在j等於target,則k=1;否則k=0。
舉個例子:

nums=[1,2,3],target=6,
dp[1]=1,
dp[2]=dp[1]+1,
dp[3]=dp[2]+dp[1]+1,
dp[4]=dp[3]+dp[2]+dp[1],
dp[5]=dp[4]+dp[3]+dp[2],
dp[6]=dp[5]+dp[4]+dp[3]

上面k的取值可以用設定dp[0]來實現,設dp[0]為1,然後狀態轉移方程可以寫成:

        ans[i]+=ans[i-nums[j]]; (nums[j]<=i)

完整程式碼如下:

public class Solution {  
    public int combinationSum4(int[] nums, int target) {  
        Arrays.sort(nums);  
        int[] ans=new int[target+1];  
        ans[0]=1;
        for (int i = 1; i < ans.length; i++)  
            for (int j = 0; j < nums.length; j++) 
                if (nums[j]<=i)
                    ans[i]+=ans[i-nums[j]];

        return ans[target];  
    }  
} 

375. 猜數字 II(Java)

猜數遊戲,不過題目要求你猜錯了就得付與你所猜的數目一致的錢,最後應求出保證你能贏的錢數(即求出保證你獲勝的最少的錢數)。
思路:
在1-n個數裡面,我們任意猜一個數(設為i),保證獲勝所花的錢應該為 i + max(w(1 ,i-1), w(i+1 ,n)),這裡w(x,y))表示猜範圍在(x,y)的數保證能贏應花的錢,則我們依次遍歷 1-n作為猜的數,求出其中的最小值即為答案,即最小的最大值問題。

public class Solution {  
    public int getMoneyAmount(int n) {  
        int[][] table = new int[n+1][n+1];  
        return DP(table, 1, n);  
    }  

   public int DP(int[][] t, int s, int e){  
        if(s >= e) return 0;  
        if(t[s][e] != 0) return t[s][e];  
        int res = Integer.MAX_VALUE;  
        for(int x=s; x<=e; x++){  
            int tmp = x + Math.max(DP(t, s, x-1), DP(t, x+1, e));  
            res = Math.min(res, tmp);  
        }  
        t[s][e] = res;  
        return res;  
    }  
} 

小偷問題 II

After robbing those houses on that street, the thief has found himself a new place for his thievery so that he will not get too much attention. This time, all houses at this place are arranged in a circle. That means the first house is the neighbor of the last one. Meanwhile, the security system for these houses remain the same as for those in the previous street.
Given a list of non-negative integers representing the amount of money of each house, determine the maximum amount of money you can rob tonight without alerting the police.
思路:
House Robber I的升級版. 因為第一個element 和最後一個element不能同時出現. 則分兩次call House Robber I.
case 1: 不包括最後一個element.
case 2: 不包括第一個element.
兩者的最大值即為全域性最大值。
完整程式碼:

public class Solution {  
    public int rob(int[] nums) {  
        if(nums==null || nums.length==0) return 0;  
        if(nums.length==1) return nums[0];  
        if(nums.length==2) return Math.max(nums[0], nums[1]); 
        return Math.max(robsub(nums, 0, nums.length-2), robsub(nums, 1, nums.length-1));  
    }  

    private int robsub(int[] nums, int s, int e) {  
        int n = e - s + 1;  
        int[] d =new int[n];  
        d[0] = nums[s];  
        d[1] = Math.max(nums[s], nums[s+1]);  

        for(int i=2; i<n; i++) {  
            d[i] = Math.max(d[i-2]+nums[s+i], d[i-1]);  
        }  
        return d[n-1];  
    }  
}  

120. 三角數列的最小和(Java)

思路:
DFS方法的時間複雜度有點高,這裡使用動態規劃。從底向上,對於某行上的某元素,它所在的最小路徑,肯定包含它和它下一行的兩個相鄰元素中的較小者。因此,可以從下往上更新一個數組,直到最上。此時,array[0]即為所求。
之所以要選擇從下往上,是因為這樣更新比較簡單,而且只需要O(n)的空間複雜度。

public class Solution {
    public int minimumTotal(List<List<Integer>> triangle) {
        int Size=