动态规划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[i1][j1],dp[i1][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递归遍历一个点的时候,我们可以把这个状态记忆下来,下次我们再次遍历到这个点的时候,就可以直接返回记忆的这个数值

  1. 对于一个位置的上下左右四个方向寻找比当前点小的位置,然后递归进入,当遇到了谷底的时候便返回,此时dp[x] [y]=1。

  2. 每一次dfs的移动都会+1,即当前的移动步数增加

  3. 如果记忆过则直接返回。

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),每个地窖中埋有一定数量的地雷。同时,给出地窖之间的连接路径。当地窖及其连接的数据给出之后,某人可以从任一处开始挖地雷,然后可以沿着指出的连接往下挖(仅能选择一条路径),当无连接时挖地雷工作结束。设计一个挖地雷的方案,使某人能挖到最多的地雷。


方法一: 深度优先搜索

  1. 以每一个点分别作为开始位置,进行递归,并且在递归中记录当前的路径,直到到达终点为止。
  2. 终点指的是,这一个点没有后续的连通地窖,或者这个某个地窖之前已经被访问过了,则终止
  3. 在终止后更新能够挖到的最长路径,并且更新这条最长度的路径
//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为消费者的最大的食物链的数量。

  1. 我们通过vector容器+数组实现类似于连通图的实现,消费者作为所有的key,然后把被吃的生产者push进他的孩子列表中。
  2. 首先我们可以发现,最顶级消费者一定是食物链的顶端,所以我们可以找到所有的最顶级消费者,从最顶级的消费者开始DFS。
  3. 当这个消费者没有孩子时,即以它为消费者的最大的食物链就是1,即只包含它自身。
  4. 然后递归到每一个生物中,递归+记忆化,最后把所有的食物链数量dp相加,得到的就是以i为顶级消费者的所有的食物链的数量。
  5. 不只有一个顶级消费者,因此循环搜索所有的顶级消费者。

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[i1][j],dp[i1][jw[i]]+v[i]):选第i件物品dp[i][j]=dp[i1][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[iw[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


观察题目可以发现两个状态:

  1. 剩余的药物足够打败下一个敌人:打败别人,获得它胜利所获得的经验,不打败它,获得它失败的经验
  2. 药物不够,打不过敌人,直接获取失败的经验。
  • 注意:根据贪心的思想,当我们选择不打败敌人的时候,直接选择投降,即不消耗药物。
//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),同样马的位置坐标是需要给出的。

img

现在要求你计算出卒从 �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[i1][j]+dp[i][j1]

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;
}
posted @   hugeYlh  阅读(150)  评论(0编辑  收藏  举报  
编辑推荐:
· 从 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)
点击右上角即可分享
微信分享提示