题意:给出一个大小为n的序列,以标号0作为起点开始跳,每次最多向右移动k个位置,求到达序列最右端时所经过所有点的权值之和最大为多少。
首先上经典dp,转移公式为f[i] = max(f[i-j]+nums[i], f[i]),其中j∈[1,k],得到O(n*k)复杂度的向右递推,数据范围不支持。
考虑维护每一段长度为k的最值,也就是利用数据结构存储f[i-k]到f[i],然后每次取最大值。考虑到效率因素,一般情况下对于最大值的维护heap(priority_queue)比set好用,但是由于维护过程需要及时清理失效的f[i-k],堆并不支持复杂的删除操作,因此还是需要用到set。又因为此题数据的特点,可能存在f[i-k]到f[i]之间有相同数值的情况,需要使用效率更低的multiset。
这次的复杂度是O(n*logn),但一般而言,C++的STL中set与multiset效率极低,因此只能作为水题方法。
#include <set>
#define INF 1000000000
class Solution {
public:
int maxResult(vector<int>& nums, int k) {
int sz = nums.size();
vector<int>f(nums.size(), -INF);
multiset<int>s;
multiset<int>::iterator it;
f[0] = nums[0];
s.insert(f[0]);
int last;
for (int i = 1; i < nums.size(); ++i){
it = s.end();
it--;
last = *it;
f[i] = max(f[i], nums[i]+last);
if (s.size() == k){
it = s.find(f[i-k]);
s.erase(it);
}
s.insert(f[i]);
}
return f[sz-1];
}
};
运行结果:
考虑进一步的优化,每次向右移动一位,同时需要维护一段长度为k的最大值。也就是最终目的是求这个最大值。一般最大值会保持不变,改变的可能有两种,一是之前f[i-k]是最大值,然后因为i右移使得这个最大值失效了;二是新的f[i]成为了最大值。对于第一种,如果能找到一种方法使得数据结构里面能够快速找到一个次大值,则最大值即使消失也只需要O(1)的代价来保证新的最大值的产生,而第二种可能的解决方法更为简单,只需要将整个数据结构清空即可。
在需要维护的数据结构中,最大值与次大值大小关系是可以传递的,也就是说对于次大值,同样需要相应的比次大值小一点的值来作为候补,那么答案呼之欲出,我们只需要维护一个递减的序列,每次新的值要加入的时候,若是序列最右端比它小,就把这个元素删除掉,直到最新的f[i]成为某个原来元素的次大值、或是成为新的最大值为止,这样仍然保证了整个序列是递减的。同时,当最左端的元素失效的时候,需要将它删除。
于是采用双端队列deque来实现这个数据结构。不难看出整个过程是O(n)的,因为每个元素只进队出队最多一次。deque中记录点的序号index,否则无法知道队头会在何时失效
class Solution {
public:
int maxResult(vector<int>& nums, int k) {
deque<int>dq;
dq.push_back(0);
int sz = nums.size();
vector<int>f(sz);
f[0] = nums[0];
for (int i = 1; i < sz; ++i){
int now = dq.front();
f[i] = f[now] + nums[i];
while(!dq.empty() && f[dq.back()] < f[i])
dq.pop_back();
dq.push_back(i);
if (i-dq.front() >= k)
dq.pop_front();
}
return f[sz-1];
}
};
最终结果如下:
可能是太久没做题了,感觉可以的东西就直接上了set、map等等。前几天有道题也是维护区间和,直接无脑就要上线段树,最后想了想树状数组又短又快为什么不呢?
在leetcode上面做题的人经常为了节省空间,直接对函数中的实参进行覆盖利用,个人认为这并不是一个好的习惯。