《剑指Offer》动态规划(基于递归的循环)

本文探讨了动态规划的概念,通过斐波那契数列、剪绳子、求连续子数组最大和等例子,展示了如何将递归思想转化为循环,避免重复计算,提高效率。同时,介绍了动态规划在解决TSP问题和寻找最长不含重复字符子字符串等问题中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

0 前言:

递归本质是把复杂问题分解成多个简单小问题。但是如果小问题中有相互重叠部分,时间效率会很差。所以应该是以递归的思想用循环的方法实现(从上往下分析问题,从下往上求解问题)。

动态规划:求解最优解时,分解大问题为小问题,然后求每个小问题的最优解,把子问题最优解存储下来(一维或二维数组)。

动态规划方法仔细安排求解顺序,对每个子问题只求解一次,并将结果保留下来.如果随后再次需要此子问题的解,只需要查找保存的结果,而不必重新计算.因此,动态规划是付出额外的内存空间来节省计算时间,是典型的时空权衡.


10、斐波那契数列

现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)。1,1,2,3,5,8......除了第一项和第二项为1外,对于第N项,有F(N) = F(N - 1) + F(N - 2)。

我们先看一下暴力求解,其时间复杂度为O(N^2):递归问题,进入一个方法,先写出一个终止条件,然后根据题目,找出递推关系,进行递归。

public static int f1(int n) {
        if(n < 1){
            return 0;
        }
        if(n == 1 || n == 2){
            return 1;
        }
        return f1(n - 1) + f1(n - 2);
}

当然我们可以优化成时间复杂度为O(N)

class Solution {
public:
    int Fibonacci(int n) {
        if(n<2)
            return n;
        //基于递归的循环、从下向上。
        int a=0,b=1,c=0;
        for(int i=2;i<=n;i++){
            c=a+b;
            a=b;//第一个更新
            b=c;//第二个更新
        }
        return c;
    }
};

14、剪绳子

题目:给定一根长度为n的绳子,请把绳子剪成m段(m、n都是整数,n>1并且m>1),每段绳子的长度记为k[0],k[1],…,k[m]。请问k[0]* k[1] * … *k[m]可能的最大乘积是多少?例如,当绳子 的长度为8时,我们把它剪成长度分别为2,3,3的三段,得到的长度最大乘积是 18.

为了避免重复计算子问题,我们采用自下而上、从小到大的方式来求解,把先求到的子问题的解储存起来,之后要用到的时候直接进行查表。  

  • 动态规划(O(n^2)时间+O(n)空间)
#include <stdio.h>
#include <vector>
#include <iostream>
using namespace std;

int cutRope(int length){
    if(length<2)return 0;
    if(length==2)return 1;
    if(length==3)return 2;
    //创建vector,保留每个长度下最大乘积,从下向上,
    vector<int> result{0,1,2,3};//0123基本量
    int max=0;
    for(int i=4;i<=length;i++)
    {
        for(int j=1;j<=(i/2);j++)
        {
            int temp=result[j]*result[i-j];//这里的f(j)与f(i-j)都已算出过。
            if(temp>max){
                max=temp;//更新max
            }
        }
        result.push_back(max);//更新result,相当于建立一维的f(i)最优值
    }
    return result[length];
}

int main()
{
    cout<<"Please enter the rope length"<<endl;
    int n;
    while(cin>>n){
        cout<<cutRope(n)<<endl;
    }

    return 0;
}
  • 贪心算法(O(1))

  根据数学计算,当n>=5时,2(n-2)>n,3(n-3)>n,这就是说,将绳子剪成2和(n-2)或者剪成3和(n-3)时,乘积大于不剪的乘积,因此需要把绳子剪成2或者3。并且3(n-3)>=2(n-2),也就是说,当n>=5时,应该剪尽量多的3,可以使最后的乘积最大。对于长度是n的绳子,我们可以剪出n/3个3,剩余长度是1或者2,如果余数是1,就可以把1和最后一个3合并成4,那么4剪出两个2得到的乘积是4,比1*3大,因此这种情况下,需要将3的个数减少1,变成两个2;如果余数是2,那么无需做修改。
    可以得到最大的乘积是:3^timesOf3 * 2^timesOf2
 

int maxProductAfterCutting_solution2(int length)
{
    if(length < 2)
        return 0;
    if(length == 2)
        return 1;
    if(length == 3)
        return 2;
 
    // 尽可能多地减去长度为3的绳子段
    int timesOf3 = length / 3;
 
    // 当绳子最后剩下的长度为4的时候,不能再剪去长度为3的绳子段。
    // 此时更好的方法是把绳子剪成长度为2的两段,因为2*2 > 3*1。
    if(length - timesOf3 * 3 == 1)
        timesOf3 -= 1;
 
    int timesOf2 = (length - timesOf3 * 3) / 2;
 
    return (int) (pow(3, timesOf3)) * (int) (pow(2, timesOf2));
}

42、求连续子数组的最大和

题目:输入一个整型数组,数组里有正数也有负数。数组中一个或连续的多个整数组成一个子数组。求所有子数组的和的最大值。要求时间复杂度为O(n)。例如输入的数组为{1,-2,3,10,-4,7,2,-5},和最大的子数组为{3,10,-4,7,2},因此输出为该子数组的和18。、

  • 动态规划

1、状态方程 : max( dp[ i ] ) = getMax( max( dp[ i -1 ] ) + arr[ i ] ,arr[ i ] )

2、上面式子的意义是:我们从头开始遍历数组,遍历到数组元素 arr[ i ] 时,连续的最大的和 可能为 max( dp[ i -1 ] ) + arr[ i ] ,也可能为 arr[ i ] ,做比较即可得出哪个更大,取最大值。时间复杂度为 n。其中,dp[i]表示以第i个数字结尾的子数组的最大和
 

class Solution {
public:
    int FindGreatestSumOfSubArray(vector<int> array) {
        if(array.empty())return 0;
        
        int maxSum=-10000,currSum=0;//currSum是函数f(i),表示以i结尾的子数组的最大和。
        for(int i=0;i<array.size();i++){
            //更新函数f(i)
            if(currSum<=0){
                currSum=array[i];
            }
            else{
                currSum+=array[i];
            }
            //比对记录并保存最大值。
            if(currSum>maxSum)maxSum=currSum;
        }
        return maxSum;
    }
};

47、礼物的最大价值

在一个m * n的棋盘的每一个格都放有一个礼物,每个礼物都有一定价值(大于0)。从左上角开始拿礼物,每次向右或向下移动一格,直到右下角结束。给定一个棋盘,求拿到礼物的最大价值。例如,对于如下棋盘

1    10   3    8
12   2    9    6
5    7    4    11
3    7    16   5

礼物的最大价值为1+12+5+7+7+16+5=53。

  • 动态规划(O(n^2)时间+O(n)空间)

定义一个函数f(i,j)f(i,j)表示达到坐标(i,j)(i,j)的格子时能够拿到礼物总和的最大值 

#include <vector>   
#include <iostream> 
#include <string>   
#include <stdlib.h> 
#include <algorithm>
#include <string.h> 

using namespace std;

int maxGift(const int *val,int rows,int cols)
{                   
    if(val==nullptr||rows<=0||cols<=0)return 0;
    //创建一维数组保存礼物最大值,该数组会一个一个更新
    int* values=new int[cols];
    for(int i=0;i<=rows;i++){
        for(int j=0;j<cols;j++){
            int up=0,left=0;//全局变量
            if(i!=0)up=values[j];//当前(即为上面的)
            if(j!=0)left=values[j-1];//前一个(左一个)
            values[j]=max(left,up)+val[i*cols+rows];
        }
    }
    int x=values[cols-1];
    delete []values;
    return x;
    
    
}                   

int main()          
{                   
    int *val = new int[16];
    val[0] = 1;val[1] = 10;
    val[2] = 3;val[3] = 100;
    val[4] = 12;val[5] = 2;
    val[6] = 9;val[7] = 6;
    val[8] = 5;val[9] = 7;
    val[10] = 4;val[11] = 11;
    val[12] = 3;val[13] = 7;
    val[14] = 16;val[15] = 5;

    cout << maxGift(val,4,4) << endl;

    return 0;       
}

48:最长不含重复字符的子字符串

请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。假设字符串中只包含′a′′a′~′z′′z′的字符。例如,在字符串"arabcacfr""arabcacfr"中,最长的不含重复字符的子字符串是"rabc""rabc"或者"acfr""acfr",长度为4.

  • 动态规划()

 定义函数f(i)表示第i个字符为结尾的不包含重复字符的子字符串的最大长度。 

1)若当前字符第一次出现,则最长非重复子字符串长度f(i) = f(i-1)+1。 
2)若当前字符不是第一次出现,则首先计算当前字符与它上次出现位置之间的距离d。若d大于f(i-1),即说明前一个非重复子字符串中没有包含当前字符,则可以添加当前字符到前一个非重复子字符串中,所以,f(i) = f(i-1)+1。若d小于或等于f(i-1),即说明前一个非重复子字符串中已经包含当前字符,则不可以添加当前字符,所以,f(i) = d。

#include <stdio.h>
#include <vector>           
#include <string>
#include <iostream>
using namespace std;

int func(const string &str)
{
    if(str.empty())return 0;
    int* position=new int[26];//每个字母在字符串的位置
    for(int j=0;j<26;j++)position[j]=-1;
    int maxlength=0,currlength=0;
    
    for(int i=0;i<str.size();i++){
        int preIndex=position[str[i]-'a'];//上一次出现位置
        if(preIndex<0||i-preIndex>currlength){//没出现过或者d>f(i-1),f(i)=f(i-1)+1
            currlength++;
        }
        else{//出现过且d<f(i-1),f(i)=d
            currlength=i-preIndex;
        }
        position[str[i]-'a']=i;//更新该字母在字符串中位置
        //每一个字符都要进行比对,更新最大长度。
        if(maxlength<currlength)maxlength=currlength;
    }
    delete[] position;
    return maxlength;
}

int main()
{
    cout<<"Please enter a string"<<endl;
    string str;
    while(cin>>str&&str!="q"){
        cout<<func(str)<<endl;
        cout<<"Please enter q to quit"<<endl;
    }
    return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值