【数据结构与算法(二十一)】

题目

礼物的最大价值

在一个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 ) 的格子:通过格子 (i1,j) ( i − 1 , j ) 或者 (i,j1) ( i , j − 1 ) .所以 f(i,j)=max(f(i1,j),f(i,j1))+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 ) 的格子时能够拿到的礼物的最大值只依赖坐标为 (i1,j) ( i − 1 , j ) (i,j1) ( 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,j1) f ( i , 0 ) , f ( i , 1 ) , f ( i , 2 ) , … … , f ( i , j − 1 ) ,数组从下标为j的数字开始到最后一个数字,分别为 f(i1,j)f(i1,j+1)f(i1,j+2)f(i1,n1) 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(i1) f ( i − 1 ) 了。
2、如果第i个字符之前没有出现过,那么 f(i)=f(i1)+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(i1) f ( i − 1 ) ,此时第i个字符上次出现在 f(i1) 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(i1) f ( i − 1 ) ,此时第i个字符上次出现 f(i1) f ( i − 1 ) 对应的最大子字符串之前,因此仍然有 f(i)=f(i1)+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的缺点是,要找到第几个丑数,就要有对应大小的数组空间来存生成的丑数,这样的话可以说是用了很多的内存空间了,明显的用空间换时间的做法。
第二种做法干看是很难理解的,所以画图,画图就很清晰了,画图主要是要注意指针的指向,有四个指针!!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值