LintCode解题记录 动态规划专题 Part1 17.11.4

来源:互联网 发布:sql select语句 例题 编辑:程序博客网 时间:2024/06/08 09:22

写在前面

最近这一周多吧,都在做动态规划的题目。把一些经典的动态规划题目基础夯实(比如各大教材中关于动态规划章节所讲的例子),然后对每一道动态规划的题目,都要仔细思考 这个递归关系是怎么得来的,这样的话动态规划的功力才会得到进步。

Minimum Path Sum

题目描述

给定一个mxn的非负整数矩阵,问你求得一条从左上到右下的路径,使得这条路径上的数字和最小,每次只能往下走一格或者往右走一格。返回这个最小值。

思路

这道题的动态规划思路还是很明显的。我们定义一个dp[i][j]表示到达点(i, j)的最短路径。显然,该状态可由两个子状态推导得到,(i-1, j)往右走一格或者(i, j-1)往下走一格,所以递归关系:

dp[i][j] = min{dp[i-1][j], dp[i][j-1]} + grid[i][j];边界条件dp[0][0] = grid[0][0];dp[0][j] = dp[0][j-1] + grid[0][j], 0 < j < n;dp[i][0] = dp[i-1][0] + grid[i][0], 0 < i < m;

代码

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

Paint Fence

题目描述

假设有n个点,每一个点都有k种颜色可以涂。你现在要给所有的点涂色,但是要求不能有超过两个的邻近的点拥有相同的颜色。请问这样的涂色方式一共有多少种。

思路

这道题的英语真是捉急了,后面的那句”such that no more than two adjacent fence posts have the same color”愣是没怎么读懂。直到看了别人的答案才明白原来是这个要求。就是说,连续相同的颜色不能超过两个。即”000”就不符合题意,但是”0011”就符合题意。
我们发现对于第i个点来说,如果要求其颜色和前面一个点不一样,那么就有k-1种途法,如果和前面的点一样,那么就首先得要求前面的点不能和前前一个点颜色相同,否则就不符合题意。因此,对第i个点,我们用dp[i][0]表示第i个点和前面一点颜色不同的涂法,而用dp[i][1]表示第i个点和前面一点颜色相同的途法。这样,得到如下递归关系:

dp[i][0] = (dp[i-1][0]+dp[i-1][1]) * (k-1);dp[i][1] = dp[i-1][0];//机智的小伙伴肯定发现了可以用两个变量来代替dp,即diff = dp[i-1][0], same = diff[i-1][1],那么上述递推关系就变为:int tmp = diff;diff = (same + diff)*(k-1);same = tmp;边界条件:第一个点same = 0, diff = k.

代码

    int numWays(int n, int k) {        // write your code here        if (n == 1) return k;        int same = 0, diff = k;        for (int i = 2; i <= n; i++) {            int t = diff;            diff = (same+diff)*(k-1);            same = t;        }        return same+diff;    }

总结

有的时候先用复杂的递归公式将问题刻画出来,然后再考虑如何优化空间复杂度。

Cutting a Rod

题目描述

有一根长为n的绳子,然后同时给定了长度为i的绳子可以卖j元。那么问题如何割这根绳子,能卖出最大的价钱。

思路

《算法导论》上的原题。其实思路也很好想,定义dp[i]为长度为i时所能卖的最大价钱。那么接下来考虑如何由之前的状态推到当前的状态。比如某一状态dp[j](j < i),那么dp[i] = dp[j] + price[i-j]。即长度为i时所能卖得的最大价钱就是长度为j时所能卖得的最大价钱加上i-j这一段绳子能卖得的价钱。递推公式如下:

dp[i] = max{dp[j] + price[i-j], 0 <= j < i};边界条件dp[0] = 0.

代码

    int cutting(vector<int>& prices, int n) {        // Write your code here        vector<int> dp(n+1, 0);        for (int i = 1; i <= n; i++) {            for (int j = i; j > 0; j--) {                dp[i] = max(dp[i], dp[i-j] + prices[j-1]);            }        }        return dp[n];    }

Guess Number Game II

题目描述

给定一个数n,然后从[1, n]中随机抽取一个数。然后你来猜测我选了多少,每次你猜一个数,如果猜错了,那么就需要支付你猜的那个数的钱,然后我会告诉你这个数是大了还是小了。如果你猜对了,那么你就赢了。现在问你确保你获胜的话,需要支付的最小金额数。

思路

我看网上都给了这道题一个相当牛逼哄哄的名字:极大极小算法。当然其实这道题刚开始我题意没有读懂。既然没理解透,那么我们就从最简单的情况开始一个一个举例思考。
n = 1,就一个数那么我肯定直接猜此数就能获胜。
n = 2,那么我肯定猜1,然后根据对手的反应来决定是不是猜错了,猜错那下一次肯定就猜2呗。这样的话,猜1的情况下,如果答案是1那么直接获胜,最惨的情况就是答案是2,那么我只需要支付1元。如果我猜了2,那么最惨的情况下我就得支付2元。显然第一种是我想要的答案。
n = 3,首先考虑我,我第一个数要猜几??如果猜1,那么相应最惨的情况是什么?就是我猜完1,不对;猜2,还不对,猜3,游戏结束。这种情况下我需要花费3元。如果我猜2,那么最惨的情况就是答案不是2,我还需要再猜一次,但是再猜一次是百分百猜对的,所以猜2的情况下最惨就得支付2元。第一个数猜3的话同第一种情况。综合考虑来说,能保证我获胜的所需支付的最下金额就是2元。
想到这里,是不是有一点眉目了呢?也就是说,对于[1, n],同一个猜测策略在答案不同的情况下所需要支付的金额不同,而我要关注的是那个最惨情况下的所需要支付的金额;而不同猜测策略在最惨情况下所需要支付的金额的最小值就是我想要的答案。是不是有一点之前说的极大极小的感觉呢?那么如何从程序的角度去考虑该问题呢?
如果用一维状态去刻画,想了半天也没有想出来的情况下,仔细思考一下能不能用二维状态去刻画该问题。
本问题中我们定义状态dp[i][j]表示区间[i, j]中能让我们获胜的所需要支付的最小金额。那么我们在[i, j]中随机猜一个k,就把该区间划分为了两个子区间[i, k-1], [k+1, j]。对于该策略,最惨的情况下所需要花费的金额就是k + max(dp[i][k-1], dp[k+1][j]), k∈[i, j]。那么对于不同的k相当于不同的策略,得到如下递推公式:

dp[i][j] = min{k + max(dp[i][k-1], dp[k+1][j]), k∈[i, j]}边界条件i == j, dp[i][j] = 0;i+1 == j, dp[i][j] = i;

注意

虽然上述递推公式是那么写的,但是实际代码的过程中但却不能这么写。比如说你的循环是先i递增,然后j递增,那么dp[i, j]用到的子状态dp[k+1, j]实际上是还没有计算过!因此对于这种区间滑动问题,需要先在外围循环定义区间的上界,然后在用内层循环递增区间的下届去做。这样的话才能保证你在求取dp[i, j]时,dp[无论什么, j]都已经被计算过。

代码

    int getMoneyAmount(int n) {        // write your code here        vector<vector<int>> dp(n+1, vector<int>(n+1, 0));        for (int j = 2; j <= n; j++) { //先确定区间的上界            for (int i = j-1; i > 0; i--) { //再确定区间的下届                int global_min = INT_MAX;                for (int k = i; k < j; k++) {                    int local_max = k + max(dp[i][k-1], dp[k+1][j]);                    global_min = min(global_min, local_max);                }                dp[i][j] = i+1 == j ? i : global_min;            }        }        return dp[1][n] ;    }

总结

很值得学习的一道题,不仅动态规划的思路很巧妙,而且代码实现的过程中也有注意点。虽然是10.31做的题目,但是当我今天11.4来总结这道题目的时候也已经忘记了状态是如何定义的。说明对此题仍然很陌生,需要多加注意。

Interleaving String

题目描述

给定三个字符串s1,s2,s3,问你s3是不是s1和s2的Interleaing String,就是说从头先取s1某一部分,再去s2某一部分,再接着取s1某一部分,再取s2…按照顺序拼接出来的字符串,就是s1和s2的Interleaing String。

思路

LintCode中关于这种两个字符串之间xjb搞的动态规划的题目还是挺多的。这题还是按照常规思路,定义dp[i, j]表示s1的前i个字符和s2的前j字符,能不能Interleaing成s3的前i+j个字符。假设子状态已知,那么显然有如下递推:

dp[i, j] = True if and only if:1.dp[i-1, j] = True and s3[i+j-1] == s1[i-1]OR2.dp[i, j-1] = True and s3[i+j-1] == s2[j-1]边界条件dp[0][0] = True.

代码

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

Largest Divisible Subset

题目描述

给定一个没有重复元素的集合,找出一个最大的子集,该子集的任意两个元素Si,Sj满足Si % Sj == 0 OR Sj % Si == 0。返回这个最大集合。

思路

首先目光就注意到了返回这个最大集合。显然根据算法导论里的思想我应该是重构这个问题的解。
由于这道题目不一定在最后一个元素取到最优解,所以这一定是一个“局部最优与全局最优”的问题。那么我定义dp[i]为包含了第i个元素的最大集合的长度。那么已知子状态,如何推断dp[i]呢?如果第i个数和之前某一集合里面所有的元素都满足题示的条件,那么dp[i] = dp[j]+1,即显然得到如下递推:

dp[i] = max{dp[j]+1 | nums[i-1]对于dp[j]所代表的集合满足题目要求的条件}

那么如何由dp[j]反映出其所代表的集合呢?由[1, 2, 4, 8]我得到了一丝灵感,如果这个集合是按照从小到大排列的,那么只要nums[i-1]能够整数其中的某一个数x,那么其也一定能整数x前面的所有数。所以首先我将该集合从小到大排序,然后上述递推式就变成了:

dp[i] = max{dp[j]+1 | nums[i-1] % nums[j-1] == 0}用一个res代表全局最优,那么res = max{res, dp[i]}.根据重构解的思路,我用s[i] = j,代表最大集合中数i的前一个数是j。于是乎我只需要记录下最大集合的最后一个数就能够得到整个最大子集了。

代码

    vector<int> largestDivisibleSubset(vector<int> &nums) {        sort(nums.begin(), nums.end());        vector<int> s(nums.size(), -1);        vector<int> res;        vector<int> dp(nums.size()+1, 1);        int maxLen = 0, k = -1;        for (int i = 2; i <= nums.size(); i++) {            for (int j = i-1; j > 0; j--) {                if (nums[i-1] % nums[j-1] == 0) {                    if (dp[j]+1 > dp[i]) {                        dp[i] = dp[j]+1;                        s[i-1] = j-1;                    }                }            }            if (dp[i] > maxLen) {                maxLen = dp[i];                k = i-1;            }        }        while (k != -1) {            res.push_back(nums[k]);            k = s[k];        }        return res;    }

Longest Common Subsequence

题目描述

最长公共子串,属于各种教材书中动态规划章节中都会拿来吹一通的经典题目。

思路

dp[i, j]表示str1的前i个字符和str2的前j个字符所能得到的LCS的长度。于是乎dp[i, j] = dp[i-1][j-1]+1 if str1[i-1] == str2[j-1], else max(dp[i-1][j], dp[i][j-1])

代码

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

总结

我感觉对于这个递推公式我还是有那么一点点的晦涩,总觉得奇怪。看一下算法导论相关的章节解解惑吧。

Longest Increasing Subsequence

题目描述

最长递增子序列

思路

定义dp[i]表示前i个字符的LIS的长度(包含第i个字符)
有如下递推关系

dp[i] = max{dp[j]+1 | nums[i-1] > nums[j-1]};res = max{res, dp[i]};

代码

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

挑战

时间复杂度为O(nlogn),挖一个坑以后再来想好了。:D

Maximal Square

题目描述

给定一个2维二进制矩阵,找出这样的一个矩形,其所有的元素都是1,返回满足条件的矩形的最大面积。

思路

定义dp[i, j]表示前i行与前j列组成的矩阵中包含matrix[i-1, j-1]的最大正方形边长。(因为正方形面积等于边长的平方)。已知子状态,如何得到当前状态呢?刚开始的时候,我的想法是当前矩形一定由dp[i-1, j], dp[i, j-1]这两个矩形扩充得到。所以就遍历检查新矩形的四条边,如果全是1就说明满足题意。但这种做法不仅代码量较大而且超时了。后来看了网上的解答,才发现原来是这样的:就是说dp[i, j-1]能保证一定范围内的正方形全是1,同理dp[i-1, j], dp[i-1, j-1]。由于只要有一个0出现那么就不满足题意,那么我一定是取上述正方形的交集。也就是如下递推:

dp[i, j] = min{dp[i-1, j-1], min{dp[i-1, j], dp[i, j-1]}}+1;

emmmm..然后修改了一下代码就通过了。

    int maxSquare(vector<vector<int>>& matrix) {        // write your code here        if (matrix.size() == 0 || matrix[0].size() == 0) return 0;        int m = matrix.size(), n = matrix[0].size();        int res = 0;        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 (matrix[i-1][j-1] == 0) continue;                dp[i][j] = 1;                dp[i][j] = min(dp[i-1][j], min(dp[i-1][j-1], dp[i][j-1])) + 1;                res = max(res, dp[i][j] * dp[i][j]);            }        }        return res;    }

Maximum Product Subarray

题目描述

给定一个正数数组,找出其中乘积最大的子数组(连续的)。

思路

显然对于第i个数,由于可正可负,因此我不仅需要知道前i-1个数中乘积最大是多少,还需要知道乘积最小是多少。所以我需要用一个dp[i, 0.1]来满足我自己的需求。于是如下递推公式:

//dp[i][0] represents the max product, dp[i][1] represent the min productdp[i][0] = max{nums[i-1], dp[i-1][0]*nums[i-1], dp[i-1][1]*nums[i-1]};dp[i][1] = min{nums[i-1], dp[i-1][0]*nums[i-1], dp[i-1][1]*nums[i-1]};res = max{res, dp[i][0]};

代码

望着这样的递推公式,显然空间复杂度可以被优化为O(1).

    int maxProduct(vector<int> &nums) {        // write your code here        int n = nums.size();        int res = INT_MIN;        int posi = 1, nega = 1;        for (int i = 1; i <= n; i++) {            int tmp_posi = max(nums[i-1], max(posi * nums[i-1], nega * nums[i-1]));            nega = min(nums[i-1], min(posi * nums[i-1], nega * nums[i-1]));            posi = tmp_posi;            res = max(res, posi);        }        return res;

Minimum Adjustment Cost

题目描述

给定一个数组和一个target,修改这个数组,使其满足每两个相邻的数之间的差值的绝对值都不超过target,求这个最小的改动。

思路

刚开始做的时候全把目光集中到了一维状态的定义,想来想去既没有发现一点动态规划的影子,也没有发现啥思路。其实这道题的状态是二维的!!
我们定义dp[i, j]位前i个数其中第i个数调整到j时的最小花费。已知子状态,那么有

//由于题目中说了这个数组中的数最大不会超过100,当然也可以直接先求得数组的最大值,然后再开最大值+1的空间。dp[i][j] = min{dp[i-1][k], k∈[max(j-target, 0), min(j+target, 100)]} + |nums[i-1]-j|;

代码

看了别人定义的状态,这题就蹭蹭蹭的写出来了。还是功力不足,稍微隐藏得深一点的Dp问题都足够够我喝一壶的。

    int MinAdjustmentCost(vector<int> &nums, int target) {        // write your code here        int n = nums.size();        int res = 0;        vector<vector<int>> dp(n, vector<int> (101, 0));        for (int j = 1; j <= 100; j++)            dp[0][j] = abs(nums[0] - j);        for (int i = 1; i < n; i++) {            res = INT_MAX;            for (int j = 1; j <= 100; j++) {                int tmp = INT_MAX;                for (int k = max(j-target, 1); k <= min(j+target, 100); k++) {                    tmp = min(tmp, dp[i-1][k]);                }                dp[i][j] = tmp + abs(nums[i] - j);                res = min(res, dp[i][j]);            }        }        return res;    }

Minimum Partition

题目描述

给定一个集合,将其划分为两个子集,返回这两个子集内元素和的差值的绝对值的最小值。

思路

emmmm..神题,看到别人题解的时候我真是惊了个呆。其实这一道题稍微变一下形可以这样理解,只用考虑一个子集,因为一个子集确定了,那么另外一个子集的元素和就等于所有元素和减去该子集的元素和。如何让这两个子集的差值最小呢?就要让该子集的元素和尽可能的接近sum/2。
那么也就是说,对于n个物品,考虑放不放进该子集,让其元素和接近sum/2。是不是似曾相识??是的,背包问题!!!于是该题就变形为:给定一个容量为sum/2的背包,和n个物品,以及每一个物品对应的容量,问你该背包能装下的最大容量是多少??这样的话只要你对该类背包问题熟悉的话,便很容易的能解决这类问题了。

代码

毕竟是Google,如果你用O(n^2)来解决该背包问题,怕是会得到一个内存溢出。所以只能用O(n)的空间复杂度来求解。

    int findMin(vector<int> &nums) {        // write your code here        int n = nums.size();        int sum = 0;        for (auto n : nums) {            sum += n;        }        vector<int> dp(sum/2+1, 0);        for (int i = 1; i <= n; i++) {            for (int j = sum/2; j >= nums[i-1]; j--) {                dp[j] = max(dp[j], dp[j-nums[i-1]] + nums[i-1]);             }        }        int res = abs(sum - 2 * dp[sum/2]);        return res;    }

总结

这种题目就属于那种动态规划不是那么明显的题目了,藏得太深了,如果不是在动态规划的Tag下遇到这道题我几乎不会想到动态规划的思路。。。不得不说,Google出的题,是厉害,服了。

原创粉丝点击