题解:U540617 捡松果
题目描述
这里有一个\(N \times M\)的二维数组。
松鼠从\((1,1)\)开始行动。
每次可以选择往下走或者往右走。
松鼠每达到一个点就可以获得这个点所有的松果。
求松鼠走到点$(N,M) $最多能够获得多少松果。
解题思路
通过题目描述,我们可以得知每次移动只能往下或者往右。
由此我们可以推断出我们从起点移动到当前点的最大收益只与当前点上方的点和左边的点相关。
我们取这两个点从起点开始收益最大的点作为它转移的来源,并且再加上在当前点所新获得的收益。
得出递归式:\(f(i,j) = max(f(i,j-1),f(i-1,j))+a_{i,j}\)
由此我们可以写出代码:
#include<bits/stdc++.h>
#define debug(a) cout<<#a<<"="<<a<<'\n';
#define il inline
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int maxn = 1e4+10;
const int maxm = 1e4+10;
int n,m;
int a[maxn][maxm];
int f(int i,int j){
if(i == 0 || j == 0)return 0;
return max(f(i-1,j),f(i,j-1))+a[i][j];
}
int main(){
// freopen("test.in","r",stdin);
// freopen("test.out","w",stdout);
ios::sync_with_stdio(0),cout.tie(0),cin.tie(0);
cin>>n>>m;
for(int i = 1;i <= n;i++)for(int j = 1;j <= m;j++)cin>>a[i][j];//循环输入
cout<<f(n,m);//递归搜索
return 0;
}
由于我们搜索时每次会分裂出两个子状态并堆积到栈内。
所以我们这个算法的时间复杂度为\(O(2^N)\)。
看到数据范围\(N \leq 10^4\)。
我们就可以先排除掉普通递归的做法了。
但是我们可以对我们的普通递归进行一次优化。
我们思考我们的普通递归算法为何会拥有如此高的时间复杂度。
这个问题归根于我们每次搜索时都有可能会对已经搜索过的点进行二次搜索。
我们只需要消除这部分重叠,就能将时间复杂度降为\(O(NM)\)(即每个点只计算一次,不进行重复计算)。
我们在搜索时记录每次当前点所获得到的收益值,下次搜索到该点时我们可以直接通过二维数组直接得到这个信息。
这个优化算法我们就叫做记忆化搜索。
#include<bits/stdc++.h>
#define debug(a) cout<<#a<<"="<<a<<'\n';
#define il inline
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int maxn = 1e4+10;
const int maxm = 1e4+10;
int n,m;
int a[maxn][maxm];
bool vis[maxn][maxn];
int F[maxn][maxn];
int f(int i,int j){//记忆化搜索
if(i == 0 || j == 0)return 0;
if(vis[i][j])return F[i][j];//如果记忆过(i,j)这个坐标点,则直接返回我们记忆过的值
vis[i][j] = 1;
return F[i][j] = max(f(i-1,j),f(i,j-1))+a[i][j];//记忆该坐标点的收益值
}
int main(){
// freopen("test.in","r",stdin);
// freopen("test.out","w",stdout);
ios::sync_with_stdio(0),cout.tie(0),cin.tie(0);
cin>>n>>m;
for(int i = 1;i <= n;i++)for(int j = 1;j <= m;j++)cin>>a[i][j];//循环输入
cout<<f(n,m);//递归搜索
return 0;
}
当然,我们还可以注意到这个问题的一些特性。
例如:重叠子问题,最优子结构,无后效性等。
重叠子问题:在暴力搜索时需要重复计算。
最优子结构:我们需要通过最优子结构来求得最优总结构。
无后效性:只要状态一致,具体经过怎样的步骤到达这一状态是没有影响的。
这让我们意识到我们可以通过动态规划算法来解决这个问题。
我们只需要优先遍历我们的前置子状态,我们后续依赖于前置子状态的状态才能被计算出来。
在这个问题中,即为我们优先计算上方以及左方,那我们其它的点都可以被上方的点和左方的点转移出来。
#include<bits/stdc++.h>
#define debug(a) cout<<#a<<"="<<a<<'\n';
#define il inline
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int maxn = 1e4+10;
const int maxm = 1e4+10;
int n,m;
int a[maxn][maxm];
int dp[maxn][maxm];
int main(){
// freopen("test.in","r",stdin);
// freopen("test.out","w",stdout);
ios::sync_with_stdio(0),cout.tie(0),cin.tie(0);
cin>>n>>m;
for(int i = 1;i <= n;i++)for(int j = 1;j <= m;j++)cin>>a[i][j];
for(int i = 1;i <= n;i++)
{
for(int j = 1;j <= m;j++)
{
dp[i][j] = max(dp[i-1][j],dp[i][j-1])+a[i][j];
}
}
cout<<dp[n][m];
return 0;
}
我们根据代码可以得知,记忆化搜索算法的常数操作过多,所以动态规划算法是这类问题的最优解。

浙公网安备 33010602011771号