暑假也是闲来无事,就想做做动态规划的题。想想在过去不久的第十届蓝桥杯国赛经历,几乎全是动态规划的题,虽然有个别题可以用深搜广搜解决,但是能过的样例不超过30%,都会超时的。因此我想总结一下。希望不会让我掉头发,这太烧脑了…
1.矩形覆盖
我们可以用2 * 1的小矩形横着或者竖着去覆盖更大的矩形。请问用n个2 * 1的小矩形无重叠地覆盖一个2 * n的大矩形,总共有多少种方法?
思路
这个题我之前也做过,深搜就可以解决,但是数据量一大必然超时。当然,在这里我们要用dp解决。
2 * n的大矩形,和n个2 * 1的小矩形
其中n * 2为大矩阵的大小
有以下几种情形:
n <= 0 大矩形为0 * 1,直接return 0;
n = 1大矩形为2 * 1,只有一种摆放方法,return1;
n = 2 大矩形为2 * 2,有两种摆放方法,return2;
n = t 分为两步考虑:
第一次摆放一块 2 * 1 的小矩阵,则摆放方法总共为f(t - 1)
第一次摆放一块1 * 2的小矩阵,则摆放方法总共为f(t-2)
因为,摆放了一块1 * 2的小矩阵(用√√表示),对应下方的1 * 2(用××表示)摆放方法就确定了,所以为f(t-2)
class Solution {
public:
int rectCover(int number) {
if (number <= 0)
return 0;
if (number <= 2)
return number;
int f1 = 1;
int f2 = 2;
int tmp;
for (int i = 3; i <= number; i++)
{
tmp = f1 + f2;
f1 = f2;
f2 = tmp;
}
return tmp;
}
};
2.连续子数组的最大和
HZ偶尔会拿些专业问题来忽悠那些非计算机专业的同学。今天测试组开完会后,他又发话了:在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢?例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。给一个数组,返回它的最大连续子序列的和,你会不会被他忽悠住?(子向量的长度至少是1)
class Solution {
public:
int FindGreatestSumOfSubArray(vector<int> array) {
int sum = array[0];
int max = array[0];
for (size_t i = 1; i < array.size(); i++)
{
sum = sum <= 0 ? array[i] : sum + array[i];
max = max < sum ? sum : max;
}
return max;
}
};
array | 6 | -3 | -2 | 7 | -15 | 1 | 2 | 2 |
---|---|---|---|---|---|---|---|---|
sum | 6 | 3 | 1 | 8 | -7 | 1 | 3 | 5 |
思考
之前我做这个题的时候,一直在想,array[i]是正数时怎么加,是负数时怎么加,这样很容易出错。动态规划的题应该紧紧抓住前面已经求解了的结果,紧紧抓住sum。
当sum是负数时,sum应该等于array[i],因为负数对sum没有贡献,是正数的时候sum+=array[i]。
再用max记录sum变化过程中的最大值就行了。
3.路径总数
一个机器人在m×n大小的地图的左上角(起点,下图中的标记“start"的位置)。
机器人每次向下或向右移动。机器人要到达地图的右下角。(终点,下图中的标记“Finish"的位置)。
可以有多少种不同的路径从起点走到终点?
上图是3×7大小的地图,有多少不同的路径?
备注:m和n小于等于100
class Solution {
public:
int uniquePaths(int m, int n) {
int *arr = new int[n];
for (int i = 0; i < n; i++)
arr[i] = 1;
for (int i = 1; i < m; i++)
{
for (int j = 1; j < n; j++)
arr[j] += arr[j - 1];
}
return arr[n - 1];
}
};
思考
简单的dp问题,对于位置arr【i】【j】,他的路径数是arr【i-1】【j】加上arr【i】【j-1】的路径数,
而在第一行和第一列的位置,他们的路径数都为1。
4.背包问题
我之前写过背包问题的详细分析,这里直接附上链接吧背包问题
5.回文串分割
给出一个字符串s,分割s使得分割出的每一个子串都是回文串
计算将字符串s分割成回文分割结果的最小切割数
例如:给定字符串s=“aab”,
返回1,因为回文分割结果[“aa”,“b”]是切割一次生成的。
class Solution {
public:
int minCut(string s)
{
if (s.empty())
return 0;
vector<int> maze;
int len = s.size();
for (int i = 0; i <= len; i++)
maze.push_back(i - 1);
for (int i = 1; i <= len; i++)
{
for (int j = 0; j < i; j++)
{
if (isPalindrome(s, j, i - 1))
{
maze[i] = maze[i] < maze[j] + 1 ? maze[i] : maze[j] + 1;
}
}
}
return maze[len];
}
bool isPalindrome(string s, int start, int end)
{
while (start < end)
{
if (s[start] != s[end])
return false;
start++;
end--;
}
return true;
}
};
字符串 | a | l | e | e | l | a | t | t | l | e | |
---|---|---|---|---|---|---|---|---|---|---|---|
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
初始化 | -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
结果 | 0 | 1 | 2 | 2 | 1 | 0 | 1 | 1 | 2 | 3 |
初始化的内容是当没有回文字符串时,maze[i]代表前i个字符需要进行多少次分割。
当下标j到i-1所对应的字符串是回文串时,maze[i]的值便有可能更新,maze[i] = maze[i] < maze[j] + 1 ? maze[i] : maze[j] + 1;具体通过表格看例子能很快理解。最后maze[len]便是答案。
优化
上述代码在判断是否回文串时时间复杂度较高,可以动态规划一下判断回文串。
class Solution2 {
public:
int minCut(string s)
{
if (s.empty())
return 0;
int len = s.size();
vector<int> cut;
// F(i)初始化
// F(0)= -1,必要项,如果没有这一项,对于重叠字符串“aaaaa”会产生错误的结果
for (int i = 0; i < 1 + len; ++i)
{
cut.push_back(i - 1);
}
vector<vector<bool> > mat = getMat(s);
for (int i = 1; i < 1 + len; ++i)
{
for (int j = 0; j < i; ++j)
{
// F(i) = min{F(i), 1 + F(j)}, where j<i && j+1到i是回文串
// 从长串判断,如果从第j+1到i为回文字符串
// 则再加一次分割,从1到j,j+1到i的字符就全部分成了回文字符串
if (mat[j][i - 1]) {
cut[i] = cut[i] < 1 + cut[j] ? cut[i] : 1 + cut[j];
}
}
}
return cut[len];
}
vector<vector<bool> > getMat(string s)
{
int len = s.size();
vector<vector<bool> > mat = vector<vector<bool> >(len, vector<bool>(len, false));
for (int i = len - 1; i >= 0; --i)
{
for (int j = i; j < len; ++j)
{
if (j == i) {
// 单字符为回文字符串
mat[i][j] = true;
}
else if (j == i + 1) {
// 相邻字符如果相同,则为回文字符串
mat[i][j] = (s[i] == s[j]);
}
else {
mat[i][j] = ((s[i] == s[j]) && mat[i + 1][j - 1]);
}
}
}
return mat;
}
};