[week10]拿数问题2 —— 动态规划(线性型/坐标型)

题意

YJQ 上完第10周的程序设计思维与实践后,想到一个绝妙的主意,他对拿数问题做了一点小修改,使得这道题变成了 拿数问题 II。

给一个序列,里边有 n 个数,每一步能拿走一个数,比如拿第 i 个数, Ai = x,得到相应的分数 x,但拿掉这个 Ai 后,x+1 和 x-1 (如果有 Aj = x+1 或 Aj = x-1 存在) 就会变得不可拿(但是有 Aj = x 的话可以继续拿这个 x)。求最大分数。

Input

第一行包含一个整数 n (1 ≤ n ≤ 105),表示数字里的元素的个数

第二行包含n个整数a1, a2, …, an (1 ≤ ai ≤ 105)

Output

输出一个整数:n你能得到最大分值。

输入样例

Input
2
1 2

Input
3
1 2 3

Input
9
1 2 1 3 2 2 2 2 3

输出样例

Output
2

Output
4

Output
10

提示

对于第三个样例:先选任何一个值为2的元素,最后数组内剩下4个2。然后4次选择2,最终得到10分。


分析

这道题中也是利用动态规划解决的经典题目。

动态规划👉[week10]LIS&LCS——动态规划


  • 题目分析

1. 拿数问题原型解题

这道题的原型是在拿数过程中,若选择了一个数字ai,就不能选择其位置相邻的两个数字ai-1和ai+1。

显然这个问题比本题更加直观。所有选择的数字以及其被限制的数字在整个序列中的位置是相邻的。

用一个数组来记录走到当前数字所获得的最大分数。在依次遍历的过程中,当前数字对应的答案可以直接通过前面一个数的答案获得。即,

走到ai时的最大分数就为不取当前数字的答案和取当前数字的答案中较大的一个。

  • 不取当前数字的答案就等于前一个数的答案
  • 取当前数字的答案就是前前个数字对应的答案加上当前数字

2. 类比

那么这个问题改变的就是限制的条件,不能取的数从位置相邻变成了大小相邻。这就使得如果按原题做法,那么在遍历过程中,你无法保证当前数字存储的答案是合法的且是最大的。

我最开始的思路是仍然采取遍历原序列,但是用两个数组来存储答案,一个为取当前数,一个为不取当前数。确认当前数字之前是否存在不合法数。根据它之前的序列情况进行计算。

可是在设计数据调试的过程中我发现了很多问题,其中一个让我放弃的瓶颈是:

若当前数字之前的子序列中即存在不合法数,也存在合法数。而这些合法数之间又存在着彼此无法共存的数。那么该如何计算?

其实后来我也发现,最初的这个想法中运用的两个数组本身也是冗余的,有点像之前做两种线路选择的最短路径问题时的方法。但是这与动态规划算法本身的解题思路还是有一点不符。

其实在最开始分析时我就发现,这些数字在序列中的位置并不影响一个序列的最终答案,仅仅受那些不合法数影响。

所以,将所有数字都只保存一次,并记录它们在序列中的出现次数。将保存的所有数字进行排序,类比拿数问题,用一个数组来记录取到当前数的最大分数。

而对每个数字,其答案就为取当前数的答案和不取当前数答案中的最大值。同样的,这两个答案的计算方法就和原型中相同,因为此时相邻的数只和大小有关。

当然,在遍历过程中还需要判断,因为序列并不是一定包含连续整数的序列,因此在排序后相邻的数不一定是大小相邻的数。如果相邻数大小并不相邻,就说明取到当前数时,其对应答案为一定可取,即为前一个数的答案+它本身。


  • 问题

需要初始化排序后头两个数字对应的答案。在初始化第二个数字时,也要判断该数字与第一个数字的大小关系。


总结

  1. 举一反三的重要性!最开始我还是有点懵的,脑子有时候卡着就是转不动😢

代码

//
//  main.cpp
//  lab3
//
//

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

vector<long long> a,num(100010,0);

int main()
{
    ios::sync_with_stdio(false);
    
    int n = 0;
    cin>>n;
    
    long long x;
    
    for( int i = 0 ; i < n ; i++ )
    {
        cin>>x;
        
        if( num[x] == 0 )           //每个数字只记录一次
            a.push_back(x);
        
        num[x]++;           //记录每个数字出现个数
    }
    
    sort(a.begin(), a.end());     //将所有数字排序
    
    long long ans[a.size()];       //记录以当前a[i]为最大值的取数答案
    
    //最终取得的最大值与数字排序无关
    //排序后更方便
    
    //初始化头两个
    ans[0] = a[0] * num[a[0]];
    
    if( a[1] != a[0] + 1 )      //若第二个与第一个数不相邻,直接相加
        ans[1] = ans[0] + a[1] * num[a[1]];
    else                        //否则取前两个里更大的
        ans[1] = max(ans[0], a[1] * num[a[1]]);
    
//    cout<<ans[0]<<" "<<ans[1]<<" ! "<<endl;
    
    for( int i = 2 ; i < a.size() ; i++ )      //依次遍历所有数字
    {
        //若当前数与前一个数相邻
        //答案即为不取该数时的答案与取该数时的答案中较大的一个
        if( a[i] == a[i - 1] + 1 )
            ans[i] = max(ans[i - 1],ans[i - 2] + a[i] * num[a[i]]);
        else        //否则直接等于前一个数的答案加上这个数的和
            ans[i] = ans[i - 1] + a[i] * num[a[i]];
        
//        cout<<a[i]<<" !! "<<endl;
        
    }
    
    sort(ans, ans + a.size());         //排序
    
    cout<<ans[a.size() - 1]<<endl;     //输出最大值
    
    return 0;
}


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

天翊藉君

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

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

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

打赏作者

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

抵扣说明:

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

余额充值