题目
礼物的最大价值
在一个m*n的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于0).你可以从棋盘的左上角开始拿格子里的礼物,并每次向左或向下移动一格,直到到达棋盘的右下角。给定一格棋盘和上面的礼物,请计算你最多能拿到多少价值的礼物?
思路【动态规划】
1、理解题目:举个例子,下面棋盘中,如果沿着数字路线1、12、5、7、7、16、5,那么我们能拿到最大价值为53的礼物
2、这时一个典型的动态规划问题。我们先用递归的思路来分析。先定义第一个函数
f(i,j)
f
(
i
,
j
)
表示到达坐标
(i,j)
(
i
,
j
)
的格子时能拿到的礼物总和的最大值。根据题目的要求,我们有两种可能的途径到达坐标
(i,j)
(
i
,
j
)
的格子:通过格子
(i−1,j)
(
i
−
1
,
j
)
或者
(i,j−1)
(
i
,
j
−
1
)
.所以
f(i,j)=max(f(i−1,j),f(i,j−1))+gift[i,j]
f
(
i
,
j
)
=
m
a
x
(
f
(
i
−
1
,
j
)
,
f
(
i
,
j
−
1
)
)
+
g
i
f
t
[
i
,
j
]
。
gift[i,j]
g
i
f
t
[
i
,
j
]
表示坐标
(i,j)
(
i
,
j
)
的格子里礼物的价值
3、这道题如果使用递归,会有大量的重复计算,所以使用循环效率会较高一点。为了缓存中间计算结果,我们需要一个辅助的二维数组。数组中坐标
(i,j)
(
i
,
j
)
的元素表示到达坐标为
(i,j)
(
i
,
j
)
的格子时能拿到的礼物价值总和的最大值
int getMaxValue_1(const int* values, int rows, int cols)
{
if (values == nullptr || rows <= 0 || cols <= 0)
return 0;
//初始化一个int*数组
int** maxValues = new int*[rows];
for (int i = 0; i < rows; i++)
maxValues[i] = new int[cols];
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++) {
int left = 0; //左边一个
int up = 0; //上面一个
if (i > 0)
up = maxValues[i - 1][j];
if (j > 0)
left = maxValues[i][j - 1];
maxValues[i][j] = max(up, left) + values[i*cols + j];
}
}
int maxValue = maxValues[rows - 1][cols - 1];
for (int i = 0; i < rows; i++)
delete[] maxValues[i];
delete[] maxValues;
return maxValue;
}
4、接下来考虑进一步优化。前面提到,到达坐标为
(i,j)
(
i
,
j
)
的格子时能够拿到的礼物的最大值只依赖坐标为
(i−1,j)
(
i
−
1
,
j
)
和
(i,j−1)
(
i
,
j
−
1
)
的格子,因此考虑第i-2行及更上面的所有格子礼物的最大值实际上没有必要保存下来。我们可以用一个一维数组来替代前面代码中的二维矩阵maxValues。该一维数组的长度为棋盘的列数n。当我们计算到达坐标为
(i,j)
(
i
,
j
)
的格子时能够拿到的礼物的最大价值
f(i,j)
f
(
i
,
j
)
,数组中前j个数字分别是
f(i,0),f(i,1),f(i,2),……,f(i,j−1)
f
(
i
,
0
)
,
f
(
i
,
1
)
,
f
(
i
,
2
)
,
…
…
,
f
(
i
,
j
−
1
)
,数组从下标为j的数字开始到最后一个数字,分别为
f(i−1,j),f(i−1,j+1),f(i−1,j+2),……,f(i−1,n−1)
f
(
i
−
1
,
j
)
,
f
(
i
−
1
,
j
+
1
)
,
f
(
i
−
1
,
j
+
2
)
,
…
…
,
f
(
i
−
1
,
n
−
1
)
.也就是说,该数组前面j个数字分别是当前第i行前面j个格子礼物的最大价值,而之后的数字分别保存前面第i-1行n-j个格子礼物的最大价值。
5、总的一句话:0~j-1代表左,j~cols-1代表下
int getMaxValue_2(const int* values, int rows, int cols)
{
if (values == nullptr || rows == 0 || cols == 0)
return 0;
int* maxValues = new int[cols];
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++) {
int left = 0;
int up = 0;
//数组从下标为j的数字开始到最后一个数字,
//分别为f(i−1,j),f(i−1,j+1),f(i−1,j+2),……,f(i−1,n−1)
//因为每开始一个新的i就对一维数组的每一个值从头开始更新,所以j开始后面的还没有被更新到
//其实无论是第一种做法还是第二种做法,都需要遍历每一格
if (i > 0)
up = maxValues[j];
if (j > 0)
left = maxValues[j - 1];
maxValues[j] = max(left, up) + values[i*cols + j];
}
}
int maxValue = maxValues[cols - 1];
delete[] maxValues;
return maxValue;
}
最长不含重复字符的子字符串
请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。假设字符串中只包含‘a~z’的字符。例如,在字符串“arabcacfr”中,最长的不含重复字符的子字符串是“cafr”,长度是4
思路【动态规划】
1、定义函数
f(i)
f
(
i
)
表示以第i个字符为结尾的不包含重复字符的子字符串的最长长度。我们从左到右逐一扫描字符串的每个字符。当我们计算以第i个字符为结尾的不包含重复字符的子字符串的最长长度
f(i)
f
(
i
)
时,我们就已经知道
f(i−1)
f
(
i
−
1
)
了。
2、如果第i个字符之前没有出现过,那么
f(i)=f(i−1)+1
f
(
i
)
=
f
(
i
−
1
)
+
1
。例如,在字符串“arabcacfr”中,显然
f(0)
f
(
0
)
等于1.在计算
f(1)
f
(
1
)
时,下标为1的字符‘r’之前没有出现过,因此
f(1)
f
(
1
)
等于2,即
f(1)=f(0)+1
f
(
1
)
=
f
(
0
)
+
1
。到此为止,最长的不含重复字符的子字符串是“ar”
3、如果第i个字符之前已经出现过,我们就需要先计算第i个字符和它上次出现在字符串中的位置的距离,并记为d,接着分两种情形分析。第一种情形是d小于或等于
f(i−1)
f
(
i
−
1
)
,此时第i个字符上次出现在
f(i−1)
f
(
i
−
1
)
对应的最长子字符串之中。因此
f(i)=d
f
(
i
)
=
d
。同时这也意味着在第i个字符出现两次所夹的子字符串中再也没有其他重复的字符了。我们继续计算
f(2)
f
(
2
)
,即以下标为2的字符’a‘为结尾的不含重复字符的子字符串的最长长度。我们注意到字符‘a’在之前出现过。该字符上一次出现在下标为0的位置,它们之间的距离d=2,也就是字符串‘a’出现在
f(1)
f
(
1
)
对应的最长子字符串中,此时
f(2)=d=2
f
(
2
)
=
d
=
2
,对应最长不含重复字符的子字符串是“ra”
4、第二种情形是d大于
f(i−1)
f
(
i
−
1
)
,此时第i个字符上次出现
f(i−1)
f
(
i
−
1
)
对应的最大子字符串之前,因此仍然有
f(i)=f(i−1)+1
f
(
i
)
=
f
(
i
−
1
)
+
1
.
5、接下来分析以字符串“arabcacfr”最后一个字符‘r’为结尾的最长不含重复字符的子字符串的长度,即求
f(8)
f
(
8
)
。以它前一个字符‘f’为结尾的最长不含重复字符的子字符串是“acf”,因此
f(7)=3
f
(
7
)
=
3
。可以看到最后一个字符’r’之前在字符串“arabcacfr”中出现过,上一次出现在下标为1的位置,因此两次出现的距离d等于7,大于
f(7)
f
(
7
)
.这说明上一个字符‘r’不出现在
f(7)
f
(
7
)
对应的最长不含重复字符的子字符串“acf”中,此时把字符’r’拼接到“acf”中也不会出现重复字符。因此
f(8)=f(7)+1
f
(
8
)
=
f
(
7
)
+
1
,即
f(8)=4
f
(
8
)
=
4
,对应的最长不含重复字符串是“acfr”
int longestSubstringWithoutDuplication(const string& str)
{
int curLength = 0;
int maxLength = 0;
int* position = new int[26];//记录字符最近出现的位置
for (int i = 0; i < 26; i++)
position[i] = -1;
for (int i = 0; i < str.length(); i++)
{
int prevIndex = position[str[i] - 'a'];
//如果是第一次出现或者在当前子字符串之外出现
if (prevIndex<0 || i - prevIndex>curLength)
++curLength;
//如果是刚好在当前字符串内
else {
//那就要先保存当前长度了,要不然要对当前的子字符串进行操作了
if (curLength > maxLength)
maxLength = curLength;
curLength = i - prevIndex;//d
}
position[str[i] - 'a'] = i;
}
if (curLength > maxLength)
maxLength = curLength;
delete[] position;
return maxLength;
}
测试用例中要有字符全都一样的字符串以及只有一个字符的字符串
丑数
我们把只包含因子2、3和5的数称为丑数。求按从小到大的顺序的第1500个丑数。例如,6、8都是丑数,但14不是,因为它包含因子7.习惯上我们把1当成第一个丑数
思路
解法1:逐个判断每个整数是不是丑数的解法,直观但不够高效
1、所谓一个数m是另一个数n的因子,是指n能被m整除,也就是说n%m=0.根据丑数的定义,丑数只能被2、3、5整除,也就是说,如果一个数能被2整除,就连续除以2;如果能被3整除,就连续除以3;如果能被5整除就连续除以5.如果最后得到的结果是1,那么这个数就是丑数;否则不是。
bool isUgly(int number)
{
while (number % 2 == 0)
number /= 2;
while (number % 3 == 0)
number /= 3;
while (number % 5 == 0)
number /= 5;
return (number == 1);
}
int GetUglyNumber(int index)
{
if (index <= 0)
return 0;
int number = 0;
int uglyFound = 0;
while (uglyFound < index)
{
number++;
if (isUgly(number))
++uglyFound;
}
return number;
}
解法2:创建数组保存已经找到的丑数,用空间换时间的做法
2、生成丑数的做法,而不是找丑数的做法。我们可以创建一个数组,里面的数字是排好序的丑数,每个丑数都是前面的丑数乘以2、3或者5得到的。
3、这种思路的关键在于怎样确保数组里面的丑数都是排好序的。假设数组中已经有若干个排好序的丑数,并且把已有最大的丑数记为M,接下来分析如何生成下一个丑数。该丑数肯定是前面某一个丑数乘以2、3或者5的结果,所以我们首先考虑把已有的每个丑数乘以2.在乘以2的时候,能得到若干个小于或等于M的结果。由于是按照顺序生成的,小于或等于M肯定已经在数组中了,我们不需再次考虑;还会得到若干个大于M的结果,但我们只需要第一个大于M的结果,因为我们希望丑数是按从小到大的顺序生成的,其他更大的结果以后再说。我们把得到的第一个乘以2后大于M的结果记为M2.同样,把已有的每个丑数乘以3和5,能得到第一个大于M的结果M3和M5.那么下一个丑数应该是M2,M3和M5这3个数的最小者。
4、推翻前面的分析:在前面分析的时候提到把已有的每个丑数分别乘以2、3和5。事实上这不是必须的,因为已有的丑数是按顺序存放在数组中的。对于乘以2而言,肯定存在某一个丑数T2,排在它之前的每个丑数乘以2都会小于已有最大的丑数,在它之后的每个丑数乘以2得到的结果都会太大。我们只需要记下这个丑数的位置,同时每次生成新的丑数的时候去更新这个T2即可。对于乘以3和5而言,也存在同样的T3和T5
int GetUglyNumber_2(int index)
{
if (index <= 0)
return 0;
//存放所有的丑数
int* pUglyNumbers = new int[index];
pUglyNumbers[0] = 1;
int nextUglyIndex = 1;
//三个指针在一开始都指向pUglyNumber指向的空间
int* pMultiply2 = pUglyNumbers;
int* pMultiply3 = pUglyNumbers;
int* pMultiply5 = pUglyNumbers;
while (nextUglyIndex < index) {
int min = (*pMultiply2 * 2 < *pMultiply3 * 3) ? *pMultiply2 * 2 : *pMultiply3 * 3;
min = (min < *pMultiply5 * 5) ? min : *pMultiply5 * 5;
//总是从P2 P3 P5中找一个最小的放进去
pUglyNumbers[nextUglyIndex] = min;
//对P2 P3 P5三个的指向进行改变,一般都是目前谁最小就改谁的指向
//为什么是while?因为指向新的数之后可能还是小于现在最小的数,那就要继续变
//直到指向符合条件的最大值
while (*pMultiply2 * 2 <= pUglyNumbers[nextUglyIndex])
++pMultiply2;
while (*pMultiply3 * 3 <= pUglyNumbers[nextUglyIndex])
++pMultiply3;
while (*pMultiply5 * 5 <= pUglyNumbers[nextUglyIndex])
++pMultiply5;
++nextUglyIndex;
}
int ugly = pUglyNumbers[index - 1];
delete[] pUglyNumbers;
return ugly;
}
解法2的缺点是,要找到第几个丑数,就要有对应大小的数组空间来存生成的丑数,这样的话可以说是用了很多的内存空间了,明显的用空间换时间的做法。
第二种做法干看是很难理解的,所以画图,画图就很清晰了,画图主要是要注意指针的指向,有四个指针!!!