题目:
Given an array nums of integers, you can perform operations on the array.
In each operation, you pick any nums[i]
and delete it to earn nums[i]
points. After, you must delete every element equal to nums[i] - 1
or nums[i] + 1
.
You start with 0 points. Return the maximum number of points you can earn by applying such operations.
解法1:复杂版动态规划
三个相邻数字只能留1个,价值为其个数乘以其数值。
以连续的区间为单位处理,各个连续区间的答案简单相加即为整体答案。
一个连续区间,在选择1个以后,就会分裂为两个连续区间,进入递归处理。具体选哪一个没有好策略,因此这里可以用动态规划筛选。
第一步,判断给定nums有哪几个连续区间。
第二步,分别用动态规划求这些连续区间的解。
第三步,所有区间结果相加即为答案。
- 选择状态
dp[i][j]
表示在连续闭区间[i,j]
上选数(各个数字的个数由数组ncount[]
存储)所能得到的最大值。
i = nums[k]; j = nums[t];
在nums中,从k号位到t号位数字是连续的。 - 状态转移方程
注意k-2和k+2都可能出现数组越界。
d p [ i ] [ j ] = { m a x { d p [ i ] [ k − 2 ] + d p [ k + 2 ] [ j ] + k ∗ n c o u n t [ k ] } i < = k < = j , k − 2 > = i , k + 2 < = j m a x { d p [ k + 2 ] [ j ] + k ∗ n c o u n t [ k ] } i < = k < = j , k − 2 < i , k + 2 < = j m a x { d p [ i ] [ k − 2 ] + k ∗ n c o u n t [ k ] } i < = k < = j , k − 2 > = i , k + 2 > j m a x { k ∗ n c o u n t [ k ] } i < = k < = j , k − 2 < i , k + 2 > j dp[i][j] = \begin{cases} max \lbrace dp[i][k-2]+dp[k+2][j]+k*ncount[k] \rbrace & i<=k<=j,k-2>=i,k+2<=j \\ max \lbrace dp[k+2][j]+k*ncount[k] \rbrace & i<=k<=j,k-2<i,k+2<=j \\ max \lbrace dp[i][k-2]+k*ncount[k] \rbrace & i<=k<=j,k-2>=i,k+2>j \\ max \lbrace k*ncount[k] \rbrace & i<=k<=j,k-2<i,k+2>j \end{cases} dp[i][j]=⎩⎪⎪⎪⎨⎪⎪⎪⎧max{dp[i][k−2]+dp[k+2][j]+k∗ncount[k]}max{dp[k+2][j]+k∗ncount[k]}max{dp[i][k−2]+k∗ncount[k]}max{k∗ncount[k]}i<=k<=j,k−2>=i,k+2<=ji<=k<=j,k−2<i,k+2<=ji<=k<=j,k−2>=i,k+2>ji<=k<=j,k−2<i,k+2>j - 边界
dp[i][i] = i*n_count[i]
- 处理顺序
nums中出现的最大数字为n,最小数字为m
i / j | m | m+1 | … | n |
---|---|---|---|---|
m | init() | |||
m+1 | 0 | init() | ||
… | … | … | … | |
n | 0 | 0 | 0 | init() |
涉及到的点在当前点的左方或下方,因此处理顺序为从左上到右下。
class Solution {
public:
int deleteAndEarn(vector<int>& nums) {
//统计数值出现的次数,寻找nums的连续区间
if(nums.empty()) return 0;
const int maxn = 10001;
static int ncount[maxn]={};
sort(nums.begin(), nums.end());
vector<int> range;
range.emplace_back(nums[0]);
int last = nums[0]; //上一个访问的数值
int n=0, m=maxn; //出现的最大,最小的数值
for(auto x:nums){
ncount[x]++;
n = max(x, n);
m = min(x, m);
if(x-last>1){
//如果间断了
range.emplace_back(last);
range.emplace_back(x);
}
last = x;
}
range.emplace_back(nums.back());
//动态规划
static int dp[maxn][maxn]={};
for(int i=m; i<=n; i++){
dp[i][i] = i * ncount[i];
}
for(int l=1; l<=n-m; l++){
for(int i=m; i<n; i++){
int j = i + l;
if(j<=n){
for(int k=i; k<=j; k++){
int point;
if(k-2>=i && k+2<=j) point = dp[i][k-2]+dp[k+2][j]+k*ncount[k];
else if(k-2>=i) point = dp[i][k-2]+k*ncount[k];
else if(k+2<=j) point = dp[k+2][j]+k*ncount[k];
else point = k*ncount[k];
if(point>dp[i][j]) dp[i][j] = point;
}
}
}
}
//组合各区间的答案
int t=range.size(), ans=0;
for(int i=0; i<t; i+=2){
ans += dp[range[i]][range[i+1]];
}
return ans;
}
};
结果超时,答案是对的。
还有更简单的动态规划方案。
解法2:动态规划
先把nums从小到大排,问题规模为数字区间的上下限。 设数字最小值为m,最大值为n。
缩小规模:从后往前考察,先判断当前数字n删或不删,若删则判断闭区间[m, n-2],若不删则判断[m, n-1],达到缩小规模。
还原规模:
- 已知闭区间[m, n-2]的最大值,可以得到不删n-1时[m, n-1]的最大值,和删n时[m, n]的最大值。
- 已知闭区间[m, n-1]的最大值,可以得到不删n时[m, n]的最大值。
综上,可以通过当前数字删或不删,不同程度地缩小问题规模到[m, n-2]和[m, n-1]。
- 选择状态
dp[i]表示闭区间[m, i]中,能得到的最大值。 - 状态转移方程
d p [ i ] = { m a x { d p [ i − 1 ] , d p [ i − 2 ] + i ∗ n c o u n t [ i ] } m + 2 < = i < = n m a x { d p [ i − 1 ] , i ∗ n c o u n t [ i ] } i = m + 1 dp[i] = \begin{cases} max \lbrace dp[i-1], dp[i-2]+i*ncount[i] \rbrace & m+2<=i<=n \\ max \lbrace dp[i-1], i*ncount[i] \rbrace & i=m+1 \end{cases} dp[i]={max{dp[i−1],dp[i−2]+i∗ncount[i]}max{dp[i−1],i∗ncount[i]}m+2<=i<=ni=m+1
其中ncount[i]
表示数字i在nums中出现的次数。
- 边界
dp[m] = m*ncount[m]
- 处理顺序
从左到右,直到dp[n]。
犯错点:并不是只有连续区间才可以处理,不连续的区间也可以处理
class Solution {
public:
int deleteAndEarn(vector<int>& nums) {
if(nums.empty()) return 0;
const int maxn = 10001;
int m=maxn, n=0;
int ncount[maxn]={};
for(auto x:nums){
m = min(m, x);
n = max(n, x);
ncount[x]++;
}
int dp[n+1];
dp[m] = m * ncount[m];
for(int i=m+1; i<=n; i++){
if(i==m+1) dp[i] = max(dp[i-1], i*ncount[i]);
else dp[i] = max(dp[i-1], dp[i-2]+i*ncount[i]);
}
return dp[n];
}
};