DP(动态规划)入门(一)

目录

A.经典模版:求一个数列的最大连续子序列和

B.最大不连续子序列和(小偷)

C.小偷进阶版

D.跳跃成功与否

E.最短跳跃次数

F.最长递增子序列

G.花费最小爬楼梯

H.乘积最大子数组

I.删除并获得点数

J.环形子数组的最大和

K.最佳观光组合

L. 乘积为正数的最长子数组长度


A.经典模版:求一个数列的最大连续子序列和

http://​ .https://leetcode-cn.com/problems/maximum-subarray/ ​

(之后很多题都与此题思路一致)

用pre保留这一位的数值加上之前保留的子序列最大值的和,与这一位本身进行比较,保留最大值;若前方子序列总和都没有这一位大,那么就从这一位开始计数;若本位与前方总序列总和更大则加上本位;max将保留的已计算过的所有子序列中最大和与pre进行比较,保留最大值

int maxSubArray(int* nums, int numsSize) 
{
    int pre = 0, max = nums[0];
    for (int i = 0; i < numsSize; i++) {
        pre = fmax(pre + nums[i], nums[i]);
//将当前位与前保留的最大子序列和的和与当前位进行对比,保留最大值
        max = fmax(max, pre);//保留前所有子序列中最大的值
    }
    return max;//返回最大值
}

上式相当于:

pre = fmax(加上此位,只取此位) ==>都是取了此位的

max = fmax(不加此位,fmax(加上此位,只取此位)) ==>可以不加上此位

相当于对每一位都考虑了三种情况后求出最大值

这里设置 加上此位是a种情况,只取此位是b,不加此位是c,fmax(加上此位,只取此位)是d:

pre中存在(a,b)种情况

max中存在(c,d)种情况

举例:

给出一段数列nums[]={-2,1,-3,4,-1,2,1,-5,4}

先设定所有连续子序列最大值max=nums[0]=-2,设置刚开始i=-1,pre初始值就是nums[-1],这里设置为0

进入循环:

i=0 (nums[0]=-2):

pre=fmax(0+nums[0],nums[0])=fmax(-2,-2)=-2;此时pre保留了当前位和前方最大连续子序列和为-2 (-2前最大连续子序列就是num[-1]=pre=0)

max=fmax(nums[0],pre)=fmax(-2,-2)=-2;此时max保留所有遍历过的子序列中和最大的值为-2

i=1 (nums[1]=1):

pre=fmax(-2+1,1)=fmax(-1,1)=1;此时属于情况b,只取了此位,因为这一位就比前面的总和都要大

max=fmax(-2,1)=1;此时在max中属于情况d,取了加上此位和只取此位中的最大值,也就是pre(pre=fmax(加上此位,只取此位));

i=2 (nums[2]=-3):

pre=fmax(1+-3,-3)=(-2,-3)=-2;情况a,虽然是负数,但因为前子数列最大和是正数,而此位是负数,加上此位比只取此位要大

max=fmax(1,-2)=1;情况c,没有加上此位,因为加上此位比之前保留的最大总和要小

i=3 (nums[3]=4)

pre=fmax(-2+4,4)=4;情况b,只取了此位

max=fmax(1,4);情况d,取了此位中并且只取此位的情况(相当于从i=3作为最大连续子序列的头再向后开始寻找)

........

到最后max保留的是从-4到1的和6,因为后续-5+4明显是负数若取上最大和变小,相当于1后面max取的都是情况c,不加此位

而pre到最后一位都一直在更新,因为它一直都是加上每一位后再进行的最大取值,少了一种不加此位的比较,而max刚好可以进行不加此位的判断,所以pre相当于在为max提供一个参数以便于考虑到所有情况,以防对最大值的误判

最后返回的max就是在对每一位都考虑过三种情况后产生的最大值

B.最大不连续子序列和(小偷)

https://leetcode-cn.com/problems/house-robber/

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

int rob(int* nums, int numsSize)
{
    if(numsSize==0)//没有房屋
        return 0;    
    if(numsSize==1)//一间房屋
        return nums[0];
    if(numsSize==2)//两间房屋只能偷一间
         return fmax(nums[0],nums[1]);

    /*下方用数组下标代替每一间房屋的位置*/
    
    int dp[numsSize+1];//引入dp数组 来在每一位上存放(此位前的最大不连续子序列和)和(此位)的最大和
    dp[0]=nums[0];//第一位前没有子序列所以其最大子序列和就是本位
    dp[1]=fmax(nums[0],nums[1]);/*求出前两位数的最大和放在第二位,
    方便前三位数最大和(dp[2]) 与 第四位(nums[3])加上第二位最大和(dp[1])的比较*/
    
    //进入循环比较
    for(int i=2;i<numsSize;i++){
        dp[i]=fmax(dp[i-2]+nums[i],dp[i-1]);//求出前i位数的最大和放在i位,方便前i+1位数最大和与第i+2加上前i位和的比较
    }
    //循环一直将本位前面所有数的最大和放在本位上
    return dp[numsSize-1];//返回最后一位数(就是所有数最大和)
}

不连续子序列和连续的不同就在于若取了此位,前一位就不能取。

所以这里的核心思想就是:

在遍历每一位时,把前几位的数列最大和放在此位上,后对每一间房屋进行判断,

比较此位加上前第两位(i-2)的偷窃最大值 和 前第一位(i-1)位的偷窃最大值,保留其中的最大值(因为不能连续偷窃)

C.小偷进阶版

https://leetcode-cn.com/problems/house-robber-ii/

房屋是一个环围在一起的,不管从哪开始偷,都要满足相隔房屋不能偷的条件

把环化成直线就成为了和前一道题一样的解法,那么怎么化成直线呢?

环有两种情况,一种是偷第一家,一种是不偷第一家,

偷第一家的话最后一家就不能偷,因为第一家和最后一家是连在一起的,

不偷第一家可能可以偷最后一家

此时环就变成了两条路线:

一条不偷第一家(start=1),一条偷第一家(start=0)

分别对两条线路取最大值,再在最后比较两条线路的最大值,取得总最大值

int robRange(int* nums, int start, int end) /*对每一条线路取最大值
                                        (用start的不同代表第一间房屋的偷窃情况)*/
{
    int *dp=(int*)malloc(sizeof(int)*1001);//还是用dp数组来保留前几位不连续子序列最大和在本位上
    dp[start] = nums[start], dp[start+1] = fmax(nums[start], nums[start + 1]);//初始化dp前两位

    //下方是和无环型最大不连续子序列一样的求值法
    for (int i = start + 2; i < end; i++) 
        dp[i] = fmax(dp[i-2] + nums[i], dp[i-1]);
    return dp[end-1];//返回最后一位上保留的所有子序列最大值
}
int rob(int* nums, int numsSize) 
{
    if(numsSize==0) 
        return 0;
    if (numsSize == 1) 
        return nums[0];
    else if (numsSize == 2) 
        return fmax(nums[0], nums[1]);
    return fmax(robRange(nums,0,numsSize-1),robRange(nums,1,numsSize));//返回两条线路最大值中的最大值
}

上面的robRange函数还可以这样写,不用引入数组,节省空间,思路相同:

int robRange(int* nums, int start, int end) 
{
    int first = nums[start], second = fmax(nums[start], nums[start + 1]);
    for (int i = start + 2; i < end; i++) {
        int temp = second;//用temp暂存当前最大值
        second = fmax(first + nums[i], temp);//更新最大值
        first = temp;//将之前最大值赋值给first
    }
    return second;//返回更新后的最大值
}

D.跳跃成功与否

https://leetcode-cn.com/problems/jump-game/

给定一个非负整数数组 nums ,你最初位于数组的第一个下标 。

数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个下标。

还是dp思想,只注重于当前一步应该怎么走,保留这一步产生的结果用于下一次的计算,每一步都保留住最好的结果最后一步自然会返回最好的结果。

在这一题中的体现就是不停用k保留前方所有跳跃结果的最大值,直到最后一位判断是否保留的最大值大过了或者刚好等于终点值以此来判断是否能到达终点

bool canJump(int* nums, int numsSize){
    int i = 0;
    for (int k = 0; i < numsSize && i <= k; i++) 
        k = fmax(k, i + nums[i]);
/*将这一点的位置(下标i)与从这一点能跳出的最大长度(距离nums[i])相加,
得到从这一点跳到下一点的最大位置,与从前一点能跳到的最大位置相比,保留最大值。

一旦出现一点的前方能跳到的最远位置(前面保留的k)小于这一点的位置,
说明无论如何前面的点都跳不到当前这一点,那么后面的位置也更到不了,返回false
如果循环结束了(i>=numsSize),也没有找到到不了的点,那就返回true*/
    return i >= numsSize;
}

E.最短跳跃次数

https://leetcode-cn.com/problems/jump-game-ii/

给你一个非负整数数组 nums ,你最初位于数组的第一个位置。

数组中的每个元素代表你在该位置可以跳跃的最大长度。

你的目标是使用最少的跳跃次数到达数组的最后一个位置。

假设你总是可以到达数组的最后一个位置。

基本思路都相同:

int jump(int* nums, int numsSize)
{
    int cnt = 0, k = 0, max = 0;//设置最大跳跃距离max,跳跃次数cnt,暂存变量k
    for (int i = 0; i < numsSize - 1; i++) {//位置一位位增加
        max = fmax(max ,nums[i] + i);
/*与上题一致,比较从每一点跳到下一点的最大位置与从前一点能跳到的最大位置,保留最大值*/
        if (i == k) {/*当位置加到与保留的最大跳跃距离k一致时
                    将k更新为下次的最大跳跃距离max并且跳跃次数加一*/
            k = max;
            cnt++;
        }
    }
    return cnt;//到达结尾输出跳跃次数
}

F.最长递增子序列

https://leetcode-cn.com/problems/longest-increasing-subsequence/

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。

例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列

int lengthOfLIS(int* nums, int numsSize) 
{
    int dp[numsSize+1];
    int max = 0;//初始化max=0当numsSize为0时返回0
    for (int i = 0; i < numsSize; i++) {//一层循环遍历数组的每一项
        dp[i] = 1;//每次不满足递增时返回后 再初始化递增子序列长度是1
        for (int j = 0; j < i; j++) {//判断本位之前所有位上的数字与本位的大小关系

        //每次判断时j都从0开始是因为题目要求不是连续递增而是在整个数列中递增即可
            if (nums[i] > nums[j]) //当第j位小于本位时记录更新最大长度
                dp[i] = fmax(dp[i], dp[j] + 1);//dp保留前i个数递增的数列最大长度
        }
        max = fmax(max, dp[i]);//保留已遍历过的子序列中的最大递增长度
        //和第一题思路一致,可以选择只保留这一位 或者加上这一位 或者不保留这一位,三种情况
    }
    return max;
}

G.花费最小爬楼梯

https://leetcode-cn.com/problems/min-cost-climbing-stairs/

数组的每个下标作为一个阶梯,第 i 个阶梯对应着一个非负数的体力花费值 cost[i](下标从 0 开始)。

每当你爬上一个阶梯你都要花费对应的体力值,一旦支付了相应的体力值,你就可以选择向上爬一个阶梯或者爬两个阶梯。

请你找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯。

#define MAX 1001
int minCostClimbingStairs(int* cost, int costSize)
{
    int dp[MAX];//此时dp数组用来存放 跳到 每一位的最小体力耗费
    dp[0] = 0,dp[1] = 0;//因为这两个位置都可以直达(作为初始阶梯),所以耗费体力为0
    for (int i = 2; i <= costSize; i++) 
        dp[i] = fmin(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);//第 i 个位置可以从第 i-2 位置跳过来,也可以从 i-1 位置跳过来
        /*取从前面跳到i-1位置耗费的体力数(dp[i - 1])加上从i-1位置跳到当前位置耗费的体力数(cost[i - 1])
        和从前面跳到i-2位置耗费的体力数(dp[i - 2])加上从i-2位置跳到当前位置耗费的体力数(cost[i - 2])中的最小耗费体力值*/
    return dp[costSize];//返回储存在最后一位上的跳完所有阶梯所花费的最小体力值
}

返回的不是[costSize-1],是因为dp[costSize-1]保存的是 跳到 最后一个台阶所花费的体力值

而还需要加上从最后一个台阶跳出去到终点的体力值储存在dp[costSize]中

H.乘积最大子数组

https://leetcode-cn.com/problems/maximum-product-subarray/

给你一个整数数组 nums ,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。

先给代码(与第一题类似只不过多保留了一个min):

int maxProduct(int* nums, int numsSize){
    long i,realmax = nums[0], max=nums[0], min=nums[0], temp;
    for (i = 1; i < numsSize; i++){
        if(nums[i] < 0) {//遇到负数交换最大值和最小值
            temp = max;
            max = min;
            min = temp;//此时(max为负数/max绝对值较大)在下方与nums[i](负数)可以乘出更大的值
        }
        max = fmax(max*nums[i], nums[i]);
        min = fmin(min*nums[i], nums[i]);//同时保存max和min值(防止出现负负得正的情况)
        realmax = fmax(realmax, max);
    }
    return realmax;
}

由于存在负数,那么会导致最大的变最小的,最小的变最大的。

因此还需要维护当前最小值min,当负数出现时则max与min进行交换再进行下一步计算

也就是说因为存在负负得正的情况,所以遍历过程同时维护最小负值和最大的正值,最后再取max

举个例子(-2,3,-4):当nums[2]=-4时max=-2和min=-6,若不进行交换会得到max=8,min=24,

而交换后因为max=-6,min=-2所以得到最终结果为max=24,min=8符合题意

I.删除并获得点数

https://leetcode-cn.com/problems/delete-and-earn/

给你一个整数数组 nums ,你可以对它进行一些操作。每次操作中,选择任意一个 nums[i] ,删除它并获得 nums[i] 的点数。

之后,你必须删除 所有 等于 nums[i] - 1 和 nums[i] + 1 的元素。开始你拥有 0 个点数。返回你能通过这些操作获得的最大点数。

int rob(int *nums, int numsSize);
int deleteAndEarn(int *nums, int numsSize) 
{
    int max = 0;
    for (int i = 0; i < numsSize; i++) 
        max = fmax(max, nums[i]);//循环求数组中最大值
    int *sum=(int*)calloc(sizeof(int),(max + 1));//所有数字初始化为0相当于一开始都不存在
    for (int i = 0; i < numsSize; i++) 
        sum[nums[i]] += nums[i];//从小到大寻找存在的数就相当于在新数组中进行排序(从小到大)
    return rob(sum, max + 1);//+1防溢出
}
int rob(int *nums, int numsSize) 
{
    /*因为已经排好序,要满足题目删除左右-1/+1的数,也就相当于隔一位取一位,
    此时就变成了经典的bp问题,解题思路和之前一样*/
    int first = nums[0], second = fmax(nums[0], nums[1]);//省内存的写法(不用申请一个数组)
    for (int i = 2; i < numsSize; i++) {
        int temp = second;
        second = fmax(first + nums[i], second);
        first = temp;
    }//和c题进阶写法相同
    return second;
}

J.环形子数组的最大和

https://leetcode-cn.com/problems/maximum-sum-circular-subarray/

给定一个由整数数组 A 表示的环形数组 C,求 C 的非空子数组的最大可能和。

在此处,环形数组意味着数组的末端将会与开头相连呈环状。(形式上,当0 <= i < A.length 时 C[i] = A[i],且当 i >= 0 时 C[i+A.length] = C[i])

此外,子数组最多只能包含固定缓冲区 A 中的每个元素一次。(形式上,对于子数组 C[i], C[i+1], ..., C[j],不存在 i <= k1, k2 <= j 其中 k1 % A.length = k2 % A.length)

分别求出数列最大最小子序列值(和a题相同):

int maxSubArray(int* nums, int numsSize) 
{
    int pre = 0, max = nums[0];
    for (int i = 0; i < numsSize; i++) {
        pre = fmax(pre + nums[i], nums[i]);
        max = fmax(max, pre);
    }
    return max;//返回最大值
}
int minSubArray(int* nums, int numsSize) 
{
    int pre = 0, min = nums[0];
    for (int i = 1; i < numsSize-1; i++) {
        pre = fmin(pre + nums[i], nums[i]);
        min = fmin(min, pre);
    }
    return min;//返回最小值
}
int maxSubarraySumCircular(int* nums, int numsSize)
{
    if(numsSize==1) return nums[0];
    int max,min,sum=0;
    max=maxSubArray(nums,numsSize);//最小子序列和
    min=minSubArray(nums,numsSize);//最大子序列和
    for(int i=0;i<numsSize;i++)//求出整个数组的和
        sum+=nums[i];
    return fmax(sum-min,max);//比较无环和有环的情况求出最大和
}

环形子数组的最大和具有两种可能,一种是不使用环的情况,另一种是使用环的情况

(1)不使用环的情况时,直接通过a题的思路,逐步求出整个数组中的最大子序和即可

(2)使用到了环,则必定包含 A[n-1]和 A[0]两个元素且说明从A[1]到A[n-2]这个子数组中必定包含负数,否则只通过一趟最大子序和就可以得出结果

因此只需要把A[1]-A[n-2]间这些负数的最小和求出来,用整个数组的和减掉这个负数最小和即可以得到有环子序列的最大和,最后再比较 无环子序列和有环子序列的大小 求出整个数组的最大值

K.最佳观光组合

https://leetcode-cn.com/problems/best-sightseeing-pair/

给你一个正整数数组 values,其中 values[i] 表示第 i 个观光景点的评分,并且两个景点 i 和 j 之间的 距离 为 j - i。

一对景点(i < j)组成的观光组合的得分为 values[i] + values[j] + i - j ,也就是景点的评分之和 减去 它们两者之间的距离。

返回一对观光景点能取得的最高分。

也和a题思路一样:

int maxScoreSightseeingPair(int* values, int valuesSize){
    int max=0;
    int pre=0;
    for(int i=1;i<valuesSize;i++){
        /*保留当前最优解*/
        pre=fmax(pre+values[i]-values[i-1]-1,values[i]+values[i-1]-1);//计算每个点的最大得分
    /*pre+values[i]-values[i-1]-1,pre是前一个结点(i-1)和前面保留下来的得分最大值的结点的的和,
    加上此景点的分数(values[i]),减去前一个结点(i-1),再减去一的长度(i比i-1多了一长度),
    相当于是用此结点替换前一个结点去和之前的保留最大值计算出得分,并把结果
    和此结点(i)与前一个被替换的结点(i-1)计算(values[i]+values[i+1]-1)出的得分进行比较,保留最大值*/
    
        max=fmax(max,pre);//记录已遍历过的子序列中的最大值
    }
    return max;
}

L. 乘积为正数的最长子数组长度

https://leetcode-cn.com/problems/maximum-length-of-subarray-with-positive-product/

给你一个整数数组 nums ,请你求出乘积为正数的最长子数组的长度。

一个数组的子数组是由原数组中零个或者更多个连续数字组成的数组。

请你返回乘积为正数的最长子数组长度。

两种情况:负数是偶数个,负数是奇数个,分别判断即可:

int getMaxLen(int* nums, int numsSize){
    // neg负、pos正数个数 第一个负数出现的位置first
    int neg = 0, pos = 0, fisrt = -1, max = 0;
    for (int i = 0; i < numsSize; i++) 
    {
        if (nums[i] == 0) {
            // 遇到0所有标记初始化
            pos = 0;
            neg = 0;
            fisrt = -1;
        } 
        else if (nums[i] > 0) //计正数加一
            pos++;
        else {
            // 记录该段第一个负数出现的位置
            if (fisrt == -1) 
                fisrt = i;
            neg++;//负数加一
        }
        if (neg % 2 == 0) //若负数是偶数个,正数总长度为负数加正数总数
            // 该段所有负数乘积为正
            max = fmax(max, pos + neg);//乘积是正数的总长度为负数加正数总数
        else //负数为单数(不能转换为正数)
            // 从第一个负数出现的位置后面 到当前位置的乘积为正(舍去第一个负数)
            max = fmax(max, i - fisrt);//max保留已遍历过的子序列中乘积是正数的最大子序列长度
    }
    return max;//返回最大长度
}

以上全部例题均来自leetcode

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TGRD

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值