动态规划的一些总结

来源:互联网 发布:unity3d 工业仿真 编辑:程序博客网 时间:2024/06/12 01:37

首先,动态规划适用于什么情况呢?我个人的想法是,先考虑数据最简单的情况,逐步增大数据,观察是否存在递推关系。

说一说递归关系。
第一种思路:由当前状态推导以后的状态,①可以确定下一个状态②可以确定下以后的多个状态③可能确定以后的多个状态。
第二种思路:当前状态由之前状态推导,①由上一状态推导出②由之前多个状态中最优解推导。
那么这两种思路的效率或者说循环次数有差异吗?
是这样的,当出现第一种思路的②情况时,第一种循环的次数要少,因为说不定哪一次我从中间一个状态直接推导出结果了呢。可以看下边的第五题。同时还要注意判别②,③两种情况,可以看下边的第六个题。

说一说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.待续

原创粉丝点击