动态规划DP与记忆化搜索DFS 题单刷题(c++实现+AC代码)
洛谷动态规划入门题单:
提单传送门
数字三角形
观察下面的数字金字塔。写一个程序来查找从最高点到底部任意处结束的路径,使路径经过数字的和最大。每一步可以走到左下方的点也可以到达右下方的点。
7 3 8 8 1 0 2 7 4 4 4 5 2 6 5 在上面的样例中,从 $7 \to 3 \to 8 \to 7 \to 5$ 的路径产生了最大
在数字金字塔中一个点可以往左下方或者右下方移动,但是转换到直角三角形,我们就会发现,一个点其实就是由它的上一个点和左上一个点走过来的,我们可以定义:dp[i] [j]表示当前(i,j)点的最大路径长度,因此易得状态转移方程:
d
p
[
i
]
[
j
]
=
m
a
x
(
d
p
[
i
−
1
]
[
j
−
1
]
,
d
p
[
i
−
1
]
[
j
]
)
+
n
u
m
s
[
i
]
[
j
]
dp[i][j]=max(dp[i-1][j-1],dp[i-1][j])+nums[i][j]
dp[i][j]=max(dp[i−1][j−1],dp[i−1][j])+nums[i][j]
最后枚举最后一行,取得最大值即可。
AC code
int n,m; const int N=2e3+10; int nums[N][N],dp[N][N]; signed main() { cin>>n; for (int i=1;i<=n;i++) { for (int j=1;j<=i;j++) { cin>>nums[i][j]; } } /* dp[i][j]: 当前位于(i,j)点的最大路径和 */ dp[1][1]=nums[1][1]; for (int i=1;i<=n;i++) { for (int j=1;j<=i;j++) { dp[i][j]=max(dp[i-1][j],dp[i-1][j-1])+nums[i][j]; } } int res=0; for (int i=1;i<=n;i++) { res=max(res,dp[n][i]); } cout<<res; return 0; }
滑雪
题目传送门:[洛谷:滑雪]([P1434 SHOI2002] 滑雪 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn))
Michael 喜欢滑雪。这并不奇怪,因为滑雪的确很刺激。可是为了获得速度,滑的区域必须向下倾斜,而且当你滑到坡底,你不得不再次走上坡或者等待升降机来载你。Michael 想知道在一个区域中最长的滑坡。区域由一个二维数组给出。数组的每个数字代表点的高度。下面是一个例子:
1 2 3 4 5 16 17 18 19 6 15 24 25 20 7 14 23 22 21 8 13 12 11 10 9
一个人可以从某个点滑向上下左右相邻四个点之一,当且仅当高度会减小。在上面的例子中,一条可行的滑坡为 24−17−16−124−17−16−1(从 2424 开始,在 11 结束)。当然 2525-2424-2323-……-33-22-11 更长。事实上,这是最长的一条。
这是一道记忆化搜索的题目,所谓记忆化搜索就是有记忆的dfs,当我们使用dfs递归遍历一个点的时候,我们可以把这个状态记忆下来,下次我们再次遍历到这个点的时候,就可以直接返回记忆的这个数值。
-
对于一个位置的上下左右四个方向寻找比当前点小的位置,然后递归进入,当遇到了谷底的时候便返回,此时dp[x] [y]=1。
-
每一次dfs的移动都会+1,即当前的移动步数增加
-
如果记忆过则直接返回。
AC cope
int n,m; const int N=5e3+10; int nums[N][N],dp[N][N]; const int dx[4]{0,0,-1,1}; const int dy[4]{-1,1,0,0}; bool check(int x,int y) { for (int i=0;i<4;i++) { int mx=x+dx[i]; int my=y+dy[i]; if (mx>=1 && mx<=n && my>=1 && my<=m && nums[mx][my]<nums[x][y]) { //不是谷底 return true; } } return false; } int dfs(int x,int y) { if (dp[x][y]) return dp[x][y]; if (!check(x,y)) return dp[x][y]=1; int ans=0; for (int i=0;i<4;i++) { int mx=x+dx[i]; int my=y+dy[i]; if (mx>=1 && mx<=n && my>=1 && my<=m && nums[mx][my]<nums[x][y]) { //选择四周小于中间值的位置,进行递归 ans=max(ans,dfs(mx,my)+1); } } return dp[x][y]=ans; } signed main() { cin>>n>>m; for (int i=1;i<=n;i++) { for (int j=1;j<=m;j++) { cin>>nums[i][j]; } } int ans=0; for (int i=1;i<=n;i++) { for (int j=1;j<=m;j++) { ans=max(ans,dfs(i,j)); } } cout<<ans; return 0; }
挖地雷
在一个地图上有�N个地窖(�≤20)(N≤20),每个地窖中埋有一定数量的地雷。同时,给出地窖之间的连接路径。当地窖及其连接的数据给出之后,某人可以从任一处开始挖地雷,然后可以沿着指出的连接往下挖(仅能选择一条路径),当无连接时挖地雷工作结束。设计一个挖地雷的方案,使某人能挖到最多的地雷。
方法一: 深度优先搜索
- 以每一个点分别作为开始位置,进行递归,并且在递归中记录当前的路径,直到到达终点为止。
- 终点指的是,这一个点没有后续的连通地窖,或者这个某个地窖之前已经被访问过了,则终止
- 在终止后更新能够挖到的最长路径,并且更新这条最长度的路径
//TODO: Write code here int n,m; const int N=1e3+10; int nums[N],dp[N],vis[N],path[N],temp[N],res,ans,len; int Map[N][N]; bool end(int cur) { for (int i=1;i<=n;i++) { if (!vis[i] && Map[cur][i]) return false; } return true; } void dfs(int cur,int sum,int step) { if (end(cur)) { //到达了结尾 if (res<sum) { //更新 res=sum; len=step; for (int i=1;i<len;i++) { path[i]=temp[i]; } } return; } for (int i=1;i<=n;i++) { //如果没有被访问过,并且是个通路 if (!vis[i] && Map[cur][i]) { vis[i]=true; temp[step]=i; dfs(i,sum+nums[i],step+1); vis[i]=false; } } return ; } signed main() { cin>>n; for (int i=1;i<=n;i++) { //每个地窖的地雷数量 cin>>nums[i]; } for (int i=1;i<=n-1;i++) { //连接图 for (int j=i+1;j<=n;j++) { cin>>Map[i][j]; } } int maxn=0; for (int i=1;i<=n-1;i++) { memset(vis,false,sizeof(vis)); temp[1]=i; vis[1]=true; dfs(i,nums[i],2); } for (int i=1;i<len;i++) { cout<<path[i]<<' '; } cout<<endl; cout<<res; return 0; }
方案二: DP求最大连通子序列
设dp[i] 为以i为终点地窖能够挖到的最大的地雷数量。
状态转移方程得: 其中nums为i点的地雷数量
d
p
[
i
]
=
m
a
x
(
d
p
[
i
]
,
d
p
[
j
]
+
n
u
m
s
[
i
]
)
dp[i]=max(dp[i],dp[j]+nums[i])
dp[i]=max(dp[i],dp[j]+nums[i])
通过对最大上升子序列问题的启发,我们可以知道,j < i ,那么我们就可以通过求解dp[j] 来计算得到 dp[i] 的最大值,如果 dp[i] < dp[j]+nums[i],则以i为终点的能挖到的地雷的数量少于以j为终点前一点,以i为终点能够挖到的地雷的数量 则更新最大值,并且记录此时的路径。
路径的回溯:我们通过记录当前点的前驱节点,来进行递归与回溯
//TODO: Write code here int n,m; const int N=4e3+10; int nums[N],dp[N],Map[N][N],path[N],ans,res,pos; void dfs(int i) { if (path[i]) { //如果i有前驱,则继续dfs dfs(path[i]); } cout<<i<<' '; } signed main() { cin>>n; for (int i=1;i<=n;i++) { cin>>nums[i]; } for (int i=1;i<=n-1;i++) { for (int j=i+1;j<=n;j++) { cin>>Map[i][j]; //i到j存在连通地窖 } } /* dp[i]表示以i为终点能够挖到的最大的地雷数量 */ dp[1]=nums[1]; for (int i=2;i<=n;i++) { dp[i]=nums[i]; for (int j=i-1;j>=1;j--) { //如果j->i存在路径并且以i为终点的地雷数小于以j为终点的地雷数+i的地雷数 if (Map[j][i] && dp[i]<dp[j]+nums[i]) { //更新 dp[i]=dp[j]+nums[i]; path[i]=j; //记录i的前驱为j } } if (ans<dp[i]) { ans=dp[i]; //最大地雷数 pos=i; //pos得到此时i结尾的最大地雷数量的i的前驱 } } dfs(pos); cout<<endl; cout<<ans; return 0; }
最大食物链计数
[题目传送门](P4017 最大食物链计数 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn))
给你一个食物网,你要求出这个食物网中最大食物链的数量。
(这里的“最大食物链”,指的是生物学意义上的食物链,即最左端是不会捕食其他生物的生产者,最右端是不会被其他生物捕食的消费者。)
Delia 非常急,所以你只有 11 秒的时间。
由于这个结果可能过大,你只需要输出总数模上 8011200280112002 的结果。
这道题与上一道挖地雷有点像,我们使用DFS深度优先搜索+记忆化的过程
其中记忆化的过程,我们使用dp数组,dp[i]表示以i为消费者的最大的食物链的数量。
- 我们通过vector容器+数组实现类似于连通图的实现,消费者作为所有的key,然后把被吃的生产者push进他的孩子列表中。
- 首先我们可以发现,最顶级消费者一定是食物链的顶端,所以我们可以找到所有的最顶级消费者,从最顶级的消费者开始DFS。
- 当这个消费者没有孩子时,即以它为消费者的最大的食物链就是1,即只包含它自身。
- 然后递归到每一个生物中,递归+记忆化,最后把所有的食物链数量dp相加,得到的就是以i为顶级消费者的所有的食物链的数量。
- 不只有一个顶级消费者,因此循环搜索所有的顶级消费者。
AC code
//TODO: Write code here int n,m; const int N=5e5+10,mod=80112002; bool top[N]; int dp[N]; v<int> eat[N]; int dfs(int cur) { //dp[cur]以cur为消费者的食物链的数量 if (dp[cur]) return dp[cur]; if (!eat[cur].size()) return dp[cur]=1;//最底层为1 int ans=0; for (int i=0;i<eat[cur].size();i++) { ans+=dfs(eat[cur][i]); ans%=mod; } return dp[cur]=ans; } signed main() { cin>>n>>m; for (int i=1;i<=m;i++) { int a,b; cin>>a>>b; eat[b].emplace_back(a); //记录b吃a top[a]=true; //被吃的标记为true } int res=0; for (int i=1;i<=n;i++) { //没被吃的就是顶级消费者 if (!top[i]) { res=(res+dfs(i))%mod; } } cout<<res; return 0; }
采药
辰辰是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”如果你是辰辰,你能完成这个任务吗?
- 01背包板子题
很容易知道01背包的动态转换公式(二维数组) dp[i] [j]表示把第i件物品装进容量为j的背包时的最大价值:
d
p
[
i
]
[
j
]
=
m
a
x
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
−
1
]
[
j
−
w
[
i
]
]
+
v
[
i
]
)
:
选第
i
件物品
d
p
[
i
]
[
j
]
=
d
p
[
i
−
1
]
[
j
]
;
不选第
i
件物品
dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]): 选第i件物品 \\ dp[i][j]=dp[i-1][j]; 不选第i件物品
dp[i][j]=max(dp[i−1][j],dp[i−1][j−w[i]]+v[i]):选第i件物品dp[i][j]=dp[i−1][j];不选第i件物品
这里使用dp一维数组来实现这道题,我们使用dp[i]表示以i为背包容量时的最大价值:
d
p
[
i
]
=
m
a
x
(
d
p
[
i
]
,
d
p
[
i
−
w
[
i
]
]
+
v
[
i
]
)
dp[i]=max(dp[i],dp[i-w[i]]+v[i])
dp[i]=max(dp[i],dp[i−w[i]]+v[i])
AC code
//TODO: Write code here int n,m; const int N=1e5+10; int nums[N],w[N],V[N],dp[N]; signed main() { cin>>n>>m; for (int i=1;i<=m;i++) { cin>>w[i]>>V[i]; } /* dp[i]表示以i为背包容量的最大价值 */ for (int i=1;i<=m;i++) { for (int j=n;j>=w[i];j--) { dp[j]=max(dp[j],dp[j-w[i]]+V[i]); } } cout<<dp[n]; return 0; }
疯狂的采药
LiYuxiang 是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同种类的草药,采每一种都需要一些时间,每一种也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”如果你是 LiYuxiang,你能完成这个任务吗?此题和原题的不同点:\11. 每种草药可以无限制地疯狂采摘。22. 药的种类眼花缭乱,采药时间好长好长啊!师傅等得菊花都谢了!
- 完全背包板子题
注意与01背包的区别:
- 01背包,遍历物品的时候从后往前,防止一个物品被放入背包多次
- 完全背包,遍历物品的时候从前往后,可以使得物品被放入多次
AC code
//TODO: Write code here int n,m; const int N=1e5+10; int nums[N],dp[N],W[N],V[N]; signed main() { cin>>n>>m; for (int i=1;i<=m;i++) { cin>>W[i]>>V[i]; } for (int i=1;i<=m;i++) { for (int j=W[i];j<=n;j++) { dp[j]=max(dp[j],dp[j-W[i]]+V[i]); } } cout<<dp[n]; return 0; }
5倍经验值
现在 absi2011 拿出了 �x 个迷你装药物(嗑药打人可耻…),准备开始与那些人打了。
由于迷你装药物每个只能用一次,所以 absi2011 要谨慎的使用这些药。悲剧的是,用药量没达到最少打败该人所需的属性药药量,则打这个人必输。例如他用 22 个药去打别人,别人却表明 33 个药才能打过,那么相当于你输了并且这两个属性药浪费了。
现在有 �n 个好友,给定失败时可获得的经验、胜利时可获得的经验,打败他至少需要的药量。
要求求出最大经验 �s,输出 5�5s。
观察题目可以发现两个状态:
- 剩余的药物足够打败下一个敌人:打败别人,获得它胜利所获得的经验,不打败它,获得它失败的经验
- 药物不够,打不过敌人,直接获取失败的经验。
- 注意:根据贪心的思想,当我们选择不打败敌人的时候,直接选择投降,即不消耗药物。
//TODO: Write code here int n,m; const int N=2e3+10; int nums[N],lose[N],win[N],num[N],dp[N]; signed main() { cin>>n>>m; for (int i=1;i<=n;i++) { cin>>lose[i]>>win[i]>>num[i]; } /* dp[i]:使用i瓶药,可以获得的最大经验数量 */ for (int i=1;i<=n;i++) { for (int j=m;j>=0;j--) { //可以不使用药,直接投降 if (j<num[i]) { dp[j]+=lose[i]; continue; } dp[j]=max(dp[j]+lose[i],dp[j-num[i]]+win[i]); } } cout<<dp[m]*5; return 0; }
过河卒
棋盘上 �A 点有一个过河卒,需要走到目标 �B 点。卒行走的规则:可以向下、或者向右。同时在棋盘上 �C 点有一个对方的马,该马所在的点和所有跳跃一步可达的点称为对方马的控制点。因此称之为“马拦过河卒”。
棋盘用坐标表示,�A 点 (0,0)(0,0)、�B 点 (�,�)(n,m),同样马的位置坐标是需要给出的。
现在要求你计算出卒从 �A 点能够到达 �B 点的路径的条数,假设马的位置是固定不动的,并不是卒走一步马走一步。
设dp[i] [j] 表示(i,j)点的路径条数。
则会有如下状态转移方程:
d
p
[
i
]
[
j
]
=
d
p
[
i
−
1
]
[
j
]
+
d
p
[
i
]
[
j
−
1
]
dp[i][j]=dp[i-1][j]+dp[i][j-1]
dp[i][j]=dp[i−1][j]+dp[i][j−1]
AC code
int n,m; const int N=1e5+10; PI M; int q,p; int dp[5000][5000],Map[30][30]; signed main() { cin>>q>>p>>M.first>>M.second; q+=2,p+=2; int i=M.first,j=M.second; i+=2,j+=2; Map[i][j]=1; Map[i-2][j+1]=1; //右上 Map[i-1][j+2]=1; //右 Map[i+1][j+2]=1; //右下 Map[i+2][j+1]=1; Map[i+2][j-1]=1; Map[i+1][j-2]=1; Map[i-1][j-2]=1; Map[i-2][j-1]=1; dp[2][1]=1; for (int i=1;i<=22;i++) { for (int j=2;j<=22;j++) { if (Map[i][j]==1) { continue; } dp[i][j]=dp[i-1][j]+dp[i][j-1]; } } cout<<dp[q][p]; return 0; }
本文来自博客园,作者:hugeYlh,转载请注明原文链接:https://www.cnblogs.com/helloylh/p/17209612.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)