動態規劃的一些總結

首先,動態規劃適用於什麼情況呢?我個人的想法是,先考慮數據最簡單的情況,逐步增大數據,觀察是否存在遞推關係。

說一說遞歸關係。
第一種思路:由當前狀態推導以後的狀態,①可以確定下一個狀態②可以確定下以後的多個狀態③可能確定以後的多個狀態。
第二種思路:當前狀態由之前狀態推導,①由上一狀態推導出②由之前多個狀態中最優解推導。
那麼這兩種思路的效率或者說循環次數有差異嗎?
是這樣的,當出現第一種思路的②情況時,第一種循環的次數要少,因爲說不定哪一次我從中間一個狀態直接推導出結果了呢。可以看下邊的第五題。同時還要注意判別②,③兩種情況,可以看下邊的第六個題。

說一說dp數組。
給dp數組設定正確的含義,通常是部分數據範圍的解。關於空間降維,是當前狀態只與上一個狀態有關,注意循環是否要倒序,不倒序的話 當前狀態會污染當前狀態嗎?(雖然都是當前狀態,但是當前狀態至少是個一維數據,假如前邊影響後邊呢,看第一題)。還有,對於當前推導下一個狀態的,是否可以只保留兩個狀態,採用迭代的方法,從而減少不必要的內存浪費,畢竟vector可以“移動複製”;還有一些根本不需要數組保存的,直接兩個基本變量就ok了。

說一個題外話,可以打印出dp數組,更有利於瞭解遞推關係式及狀態的變化。

1.揹包問題
在n個物品中挑選若干物品裝入揹包,最多能裝多滿?假設揹包的大小爲m,每個物品的大小爲A[i]。
如果有4個物品[2, 3, 5, 7]
如果揹包的大小爲11,可以選擇[2, 3, 5]裝入揹包,最多可以裝滿10的空間。
如果揹包的大小爲12,可以選擇[2, 3, 7]裝入揹包,最多可以裝滿12的空間。
函數需要返回最多能裝滿的空間大小。
解答。首先dp[n][m]代表前n個物品中在體積爲m的情況時,所能裝的最大空間。對於某個物品,有取與不取兩種策略。根據當前狀態只與上一個狀態可以優化空間。
dp[m]代表體積爲m情況下所能裝的最大值。外層循環遍歷物品,內層循環更新當前物品取與不取的最大值。至於爲什麼從大到小循環,是因爲當前狀態只與上一層狀態有關(也就是外循環-1那層狀態),若反過來從小到大,在同一層外循環裏,內循環裏前邊會影響後邊的。
從循環更新的值來看,兩者是一樣的。數據更新都是一樣的

int backPack(int m, vector<int> A) {
    int n = A.size();
    vector<int> dp(m + 1, 0);

    for (int i = 1; i <= n; i++)
    {
        for (int j = m; j >= A[i - 1]; j--)
        {
            dp[j] = max(dp[j], A[i - 1] + dp[j - A[i - 1]]);
        }
        //for (auto x : dp)cout << x << " "; cout << endl;
    }
    return dp[m];
}
int backPack2(int m, vector<int> A) {
    int n = A.size();
    vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));

    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= m; j++)
        {
            if (j >= A[i - 1])
                dp[i][j] = max(dp[i - 1][j], A[i - 1] + dp[i - 1][j - A[i - 1]]);
            else
                dp[i][j] = dp[i - 1][j];
        }
        //for (auto x : dp[i]) cout << x << " "; cout << endl;
    }
    return dp[n][m];
}

2.揹包問題Ⅱ
給出n個物品的體積A[i]和其價值V[i],將他們裝入一個大小爲m的揹包,最多能裝入的總價值有多大?
對於物品體積[2, 3, 5, 7]和對應的價值[1, 5, 2, 4], 假設揹包大小爲10的話,最大能夠裝入的價值爲9。
解答。同第一題

int backPackII(int m, vector<int> A, vector<int> V) {
        // write your code here
        int n = A.size();
        vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));

        for(int i=1; i<=n; i++)
        {
            for(int j=1; j<=m; j++)
            {
                if(j>=A[i-1])
                    dp[i][j] = max(dp[i-1][j], V[i-1]+dp[i-1][j-A[i-1]]);
                else
                    dp[i][j] = dp[i-1][j];
            }
        }
        return dp[n][m];
    }
int backPackII(int m, vector<int> A, vector<int> V) {
        // write your code here
        int n = A.size();
        vector<int> dp(m + 1, 0);

        for(int i=1; i<=n; i++)
        {
            for(int j=m; j>=A[i-1]; j--)
            {
                dp[j] = max(dp[j], V[i-1]+dp[j-A[i-1]]);
            }
        }
        return dp[m];
    }

3.最小調整代價
給一個整數數組,調整每個數的大小,使得相鄰的兩個數的差不大於一個給定的整數target,調整每個數的代價爲調整前後的差的絕對值,求調整代價之和最小是多少。你可以假設數組中每個整數都是正整數,且小於等於100。
樣例
對於數組[1, 4, 2, 3]和target=1,最小的調整方案是調整爲[2, 3, 2, 3],調整代價之和是2。返回2。
解答。dp[i][j]表示對於前i個數,把當前i調整至j的最小代價,當前狀態與上一個狀態的一段區域最優解有關。

    int MinAdjustmentCost(vector<int> A, int target) {
        // write your code here
        int m = A.size();
        vector<vector<int> > dp(m + 1, vector<int>(101, 0));

        for(int i=1; i<=m; i++)
        {
            for(int j=1; j<=100; j++)
            {
                int temp = abs(A[i-1]-j);
                dp[i][j] = dp[i-1][j];
                for(int k=-1*target; k<=target; k++)
                    if (j + k >= 1 && j+k<=100)//可以優化一下,提前算好上下界,避免無用循環
                        dp[i][j] = min(dp[i][j], dp[i-1][j+k]);
                dp[i][j] += temp;
                cout << dp[i][j] << " ";
            }
            cout<<endl;
        }
        int ret = dp[m][1];
        for(int i=2; i<=100; i++)
        {
            ret = min(ret, dp[m][i]);
        }
        return ret;
    }

樣例的dp數組
4.骰子求和
扔 n 個骰子,向上面的數字之和爲 S。給定 Given n,請列出所有可能的 S 值及其相應的概率。
給定 n = 1,返回 [ [1, 0.17], [2, 0.17], [3, 0.17], [4, 0.17], [5, 0.17], [6, 0.17]]
解答。很典型的動態規劃,思路是由小到大,一生二,二生三。不需要保存太多狀態,一個base,一個新的,加上vector可以複製,其實是右值引用,所以性能應該還是不錯的。

    vector<pair<int, double>> dicesSum(int n) {
        // Write your code here
       vector<double> base(7, 1.0/6);
       for(int i=2; i<=n; i++)
       {
           int large = i*6;
            vector<double> dp(large+1, 0);
            for(int j=i; j<=large; j++)
            {
                for(int k=i-1; k<=6*(i-1); k++)
                {
                    int temp = j-k;
                    if(temp>=1 && temp<=6)
                    {
                        dp[j] += base[k];
                    }
                }
                dp[j] *= 1.0/6;
            }
            base = dp;
       }
       int count = 5*n+1;
       vector<pair<int, double>> ret(count);
       for(int i=6*n; i>=n; i--)
            ret[--count] = make_pair(i, base[i]);
       return ret;
    }

5.單詞切分
給出一個字符串s和一個詞典,判斷字符串s是否可以被空格切分成一個或多個出現在字典中的單詞。
樣例:給出s = “lintcode” dict = [“lint”,”code”] 返回 true 因爲”lintcode”可以被空格切分成”lint code”
解答,dp[n]代表從1到n是否能夠拆分,由小得大。第二種通過了:從當前狀態可能推導出以後的n個狀態,先找出字典中單詞的最大長度,減少不必要循環。第一種的話,加上找最大長度應該也能通過。兩種思路,第一個當前狀態由前邊的一些狀態得到,第二個當前狀態推導出接下來的一些狀態。

    bool wordBreak(string s, unordered_set<string> dict) {
        // write your code here
        int m = s.size();
        vector<bool> dp(m+1, false);
        dp[0] = true;

        for(int i=1; i<=m; i++)
        {
            for(int j=0; j<i; j++)
            {
                if(dp[j])
                {
                    string temp = s.substr(j, i-j);

                    auto iter = dict.find(temp);
                    if(iter != dict.end())
                    {
                        dp[i] = true;
                        break;
                    }
                }
            }
        }
        return dp[m];
int max_len(unordered_set<string>& dict)
     {
        int ret = 0;
        for(auto iter=dict.begin(); iter!=dict.end(); iter++)
        {
            ret = (ret>=(*iter).size())?ret:iter->size();
        }
        return ret;
     }
    bool wordBreak(string s, unordered_set<string> dict) {
        // write your code here
        int m = s.size();
        int ml = max_len(dict);
        vector<bool> dp(m+1, false);
        dp[0] = true;

        for(int i=0; i<=m; i++)
        {
            if(dp[i] == true)
            {
                for(int j=i+1; j<=i+ml && j<=m; j++)
                {
                    if(dp[j]==false)
                    {
                        string temp = s.substr(i, j-i);
                        auto iter = dict.find(temp);
                        if(iter != dict.end())
                        {
                            dp[j] = true;
                            if(j==m) return true;
                        }
                    }
                }
            }
        }
        return dp[m];
    }

6.完美平方
給一個正整數 n, 找到若干個完全平方數(比如1, 4, 9, … )使得他們的和等於 n。你需要讓平方數的個數最少。
解答:同上題,還是兩種思路,但是兩者效率來說一樣,因爲從當前推導以後的狀態並不是最優的,所以還要循環完才知道。所以兩者循環次數應該是一樣的。

    int numSquares(int n) {
        // write your code here
        vector<int> dp(n+1, INT_MAX);

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

7.交叉字符串
給出三個字符串:s1、s2、s3,判斷s3是否由s1和s2交叉構成。
樣例
比如 s1 = “aabcc” s2 = “dbbca”, 當 s3 = “aadbbcbcac”,返回 true. 當 s3 = “aadbbbaccc”, 返回 false.
解答:dp[i][j]代表s1的前i個和s2的前j個是否和s3的前i+j個匹配,

bool isInterleave(string s1, string s2, string s3) {
        // write your code here
        int m = s1.size();
        int n = s2.size();
        if(m+n != s3.size()) return false;
        vector< vector<bool> > dp(m + 1, vector<bool>(n+1, false));
        dp[0][0] = true;
        for(int i=1; i<=m; i++)
        {
            if(s3[i-1] == s1[i-1])
                dp[i][0] = true;
        }
        for(int j=1; j<=n; j++)
        {
            if(s3[j-1] == s2[j-1])
                dp[0][j] = true;

        }
        for(int i=1; i<=m; i++)
        {
            for(int j=1; j<=n; j++)
            {
                if( (s3[i+j-1]==s1[i-1] && dp[i-1][j]) || 
                    (s3[i+j-1]==s2[j-1] && dp[i][j-1]) )
                    dp[i][j] = true;
            }
        }
        return dp[m][n];
    }

樣例的dp狀態圖

8.最長上升子序列
給定一個整數序列,找到最長上升子序列(LIS),返回LIS的長度。
最長上升子序列的定義:最長上升子序列問題是在一個無序的給定序列中找到一個儘可能長的由低到高排列的子序列,這種子序列不一定是連續的或者唯一的。
樣例:給出 [5,4,1,2,3],LIS 是 [1,2,3],返回 3. 給出 [4,2,4,5,3,7],LIS 是 [2,4,5,7],返回 4
解答:dp[i]代表對於以nums[i]爲尾部的最長上升序列長度,它與之前的n個狀態有關。

int longestIncreasingSubsequence(vector<int> nums) {
        // write your code here
        int m = nums.size();
        if(m==0) return 0;
        vector<int> dp(m+1, 1);
        for(int i=2; i<=m; i++)
        {
            for(int j=1; j<i; j++)
            {
                if(nums[i-1] > nums[j-1])
                    dp[i] = max(dp[i], 1+dp[j]);
            }
        }
        int ret = dp[1];
        for(int i=1;i<=m;i++)
        {
            if(dp[i]>ret)
                ret = dp[i];
        }
        return ret;
    }

9.最長公共子序列
定義:最長公共子序列問題是在一組序列(通常2個)中找到最長公共子序列(注意:不同於子串,LCS不需要是連續的子串)。該問題是典型的計算機科學問題,是文件差異比較程序的基礎,在生物信息學中也有所應用。
解答:dp[i][j]代表A的前i個,B的前j個的最長公共子序列。狀態關係,若當前相等,則1+上左,否則取 上或者左 的較大值。想想dp圖。

    int longestCommonSubsequence(string A, string B) {
        // write your code here
        int m = A.size();
        int n = B.size();
        vector< vector<int> > dp(m + 1, vector<int>(n+1, 0));
        for(int i=1; i<=m; i++)
        {
            for(int j=1; j<=n; j++)
            {
                if(A[i-1] == B[j-1])
                {
                    dp[i][j] = 1 + dp[i-1][j-1];
                }
                else
                    dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
            }
        }
        return dp[m][n];
    }

10.k數和
dp[i][j][n] = dp[i-1][j][n] + dp[i-1][j-1][n-A[i-1]]);分別代表當前數選 不選的方案數。
空間優化之後dp[j][n] += dp[j-1][n-A[i-1]];

int kSum(vector<int> A, int k, int target) {
        // write your code here
        int m = A.size();
        vector<vector<int>> dp(k+1, vector<int>(target+1,0));
        dp[0][0] = 1;
        for(int i=1; i<=m; i++)
        {
            for(int j=min(k, i); j>=1; j--)
            {
                for(int n=target; n>=A[i-1];n--)
                {
                    dp[j][n] += dp[j-1][n-A[i-1]];
                }
            }
        }
        return dp[k][target];
    }

11.二叉樹中的最大路徑和
給出一棵二叉樹,尋找一條路徑使其路徑和最大,路徑可以在任一節點中開始和結束(路徑和爲兩個節點之間所在路徑上的節點權值之和)

int maxPathSum(TreeNode * root) {
        // write your code here
        int ret = INT_MIN;
        maxPathSum(root, ret);
        return ret;
    }
    int maxPathSum(TreeNode *root, int &ret)
    {
        if(root == NULL)
            return 0;
        int left_max = maxPathSum(root->left, ret);
        int right_max = maxPathSum(root->right, ret);

        int cur = max(0, left_max) + max(0, right_max) + root->val;
        ret = max(ret, cur);
        return root->val + max(0, max(left_max, right_max));
    }

12.最小路徑和
給定一個只含非負整數的m*n網格,找到一條從左上角到右下角的可以使數字和最小的路徑。

    int minPathSum(vector<vector<int>> grid) {
        // write your code here
        int m = grid.size();
        if(m==0) return 0;
        int n = grid[0].size();
        vector<vector<int>> dp(m, vector<int>(n, INT_MAX));
        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 j=1; j<n; j++)
            dp[0][j] = grid[0][j]+dp[0][j-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];
    }

13.待續

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章