题目链接
第276场周赛第三题
https://leetcode-cn.com/problems/solving-questions-with-brainpower/
题目内容
给你一个下标从0开始的二维整数数组questions
,其中questions[i] = [pointsi, brainpoweri]
。
这个数组表示一场考试里的一系列题目,你需要 按顺序 (也就是从问题 0
开始依次解决),针对每个问题选择 解决 或者 跳过 操作。解决问题i
将让你 获得pointsi
的分数,但是你将 无法 解决接下来的 brainpoweri 个问题(即只能跳过接下来的 brainpoweri 个问题)。如果你跳过问题i
,你可以对下一个问题决定使用哪种操作。
比方说,给你questions = [[3, 2], [4, 3], [4, 4], [2, 5]]
:
如果问题0
被解决了, 那么你可以获得 3
分,但你不能解决问题 1
和2
。
如果你跳过问题 0
,且解决问题1
,你将获得4
分但是不能解决问题2
和3
。
请你返回这场考试里你能获得的 最高 分数。
解决思路
裸dfs(超时)
纯dfs写法就是设置边界是题目号超过了题目数返回,否则,在第i题时,要不跳过这道题,进入下一题;要不就做这道题,跳过questions[i][1]题。
class Solution {
public:
int ans;
Solution()
{
ans=0;
}
void dfs(vector<vector<int>>& questions,int step,int score)
{
if(step>=questions.size())//当前题目号超过了题目数,就表示已经枚举完一种做题方式,查询此种方案的答案
{
if(score>ans)
{
ans=score;
return;
}
}
else
{
dfs(questions,step+questions[step][1]+1,score+questions[step][0]);//做当前的题目
dfs(questions,step+1,score);//跳过这道题目
}
}
long long mostPoints(vector<vector<int>>& questions) {
dfs(questions,0,0);
return ans;
}
};
这种写法,会在第21个测试用例超时,原因是这种递归方式,包含了许多重复的情况计算,因此我们需要对其进行优化。
记忆化搜索
上述方法包含了大量重复的情况,因此我们采取记忆化搜索的方式,将每一次得到的值进行一个保存,以便于后续dfs的时候直接返回。
typedef long long ll;
class Solution {
public:
int n;
vector<ll>dp;
void dp_init(int n)
{
dp.resize(n+1);
}
ll dfs(vector<vector<int>>& questions,int i)
{
if(i>=n)
return 0;
else if(dp[i])
return dp[i];//如果值得到保存,直接返回,不用往下做重复的递归
else
return dp[i]=max(dfs(questions,i+1),dfs(questions,i+questions[i][1]+1)+questions[i][0]);//取两种情况的最大值
}
long long mostPoints(vector<vector<int>>& questions) {
n = questions.size();
dp_init(n);
return dfs(questions,0);
}
};
用此方法可以通过所有测试用例
动态规划
但是,递归的方法由于涉及到了堆栈的操作,效率相对递推法而言总是会慢一些,因此我们是否能够采用递归(动态规划)的方式进行求解呢?
利用动态规划,我们可以设置dp[i]
为解决第i
到第n-1
个问题的最大分数,而在每种状态的选择当中,我们有选或者不选两种状态。因此,当选中时,dp[i]=questions[i][0]+dp[i+questions[i][1]+1]
;不选时,dp[i]=dp[i+1]
。我们只要将这两种答案比较下,取最大值即可。
另外,需要注意的是,由于此时的最优子结构大小是从后往前的顺序的,i
越小,dp[i]
所包含的结构规模就越大!因此我们遍历的顺序也需要从后往前,倒序进行dp填表!
typedef long long ll;
class Solution {
public:
long long mostPoints(vector<vector<int>>& questions) {//反向dp
int n = questions.size();
vector<ll>dp(n+1);
int i;
for(i=n-1;i>=0;i--)
{
if(i+ll(questions[i][1])+1>=n)//如果越界了,那就直接返回当前的问题分数即可。
{
dp[i] = max(ll(questions[i][0]),dp[i+1]);
}
else
{
dp[i] = max(questions[i][0]+dp[i+questions[i][1]+1],dp[i+1]);//否则,则比较选或者不选两种情况
}
}
return dp[0];//最开始的解包含了整个规模
}
};
由此可见,相比记忆化的方法,时空规模有了一定的减小。
注意点
这道题最重要的点就是反向的动态规划,以往的传统动态规划题都是i
从0
到n
进行遍历的,而这道题刚好反了过来,这说明我们的遍历顺序,在很大程度上和最优子结构之间的包含覆盖关系有紧密的关联。我们在选择i
的遍历方向上,一定要根据子结构的覆盖关系来进行选取,后遍历的结构规模,一般都是大于前面的!