动态规划DP与记忆化搜索DFS 题单刷题(c++实现+AC代码)

洛谷动态规划入门题单:
提单传送门

数字三角形

观察下面的数字金字塔。写一个程序来查找从最高点到底部任意处结束的路径,使路径经过数字的和最大。每一步可以走到左下方的点也可以到达右下方的点。

        7 
      3   8 
    8   1   0 
  2   7   4   4 
4   5   2   6  5 

在上面的样例中,从 7 → 3 → 8 → 7 → 5 7 \to 3 \to 8 \to 7 \to 5 73875 的路径产生了最大


在数字金字塔中一个点可以往左下方或者右下方移动,但是转换到直角三角形,我们就会发现,一个点其实就是由它的上一个点和左上一个点走过来的,我们可以定义: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

#include <iostream>
using namespace std;
const int N=1010;
int dp[N][N];
int n;
int main(){
	cin>>n;
	for (int i=1;i<=n;i++){
		for (int j=1;j<=i;j++){
			int num;
			cin>>num;
			dp[i][j]=num; 
		} 
	}
	for (int i=1;i<=n;i++){
		for (int j=1;j<=i;j++){
			dp[i][j]=max(dp[i-1][j-1],dp[i-1][j])+dp[i][j];
		}
	}
	int ans=0;
	for (int i=1;i<=n;i++){
		ans=max(ans,dp[n][i]);
	}
	cout<<ans;
	return 0;
}

滑雪

题目传送门: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 结束)。当然 25-24-23-……-3-2-1 更长。事实上,这是最长的一条。


这是一道记忆化搜索的题目,所谓记忆化搜索就是有记忆的dfs,当我们使用dfs递归遍历一个点的时候,我们可以把这个状态记忆下来,下次我们再次遍历到这个点的时候,就可以直接返回记忆的这个数值

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

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

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

AC cope

#include <iostream>
#include <cstring>
#include <string>
#include <algorithm>
using namespace std;
const int N=110;
int nums[N][N],dp[N][N]; //第i,j个点的最长目标长度 
int n,m,ans;
const int dx[4]={-1,1,0,0},dy[4]={0,0,-1,1};
bool check(int x,int y){
	for (int i=0;i<4;i++){
		int cx=x+dx[i];
		int cy=y+dy[i];
		if (cx>=1 && cx<=n && cy>=1 && cy<=m && nums[cx][cy]<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 cx=x+dx[i];
		int cy=y+dy[i];
		if (cx>=1 && cx<=n && cy>=1 && cy<=m && nums[cx][cy]<nums[x][y]){
			ans=max(ans,dfs(cx,cy)+1);
		}
	} 
	return dp[x][y]=ans;
}
int main(){
	cin>>n>>m;
	for (int i=1;i<=n;i++){
		for (int j=1;j<=m;j++){
			cin>>nums[i][j];
		}
	}
	int maxV=0;
	for (int i=1;i<=n;i++){
		for (int j=1;j<=m;j++){
			maxV=max(maxV,dfs(i,j));	
		}
	}
	cout<<maxV;
	return 0;
}

挖地雷

挖地雷

在一个地图上有N个地窖(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 最大食物链计数

给你一个食物网,你要求出这个食物网中最大食物链的数量。

(这里的“最大食物链”,指的是生物学意义上的食物链,即最左端是不会捕食其他生物的生产者,最右端是不会被其他生物捕食的消费者。)

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 要谨慎的使用这些药。悲剧的是,用药量没达到最少打败该人所需的属性药药量,则打这个人必输。例如他用 2 个药去打别人,别人却表明 3 个药才能打过,那么相当于你输了并且这两个属性药浪费了。

现在有 n 个好友,给定失败时可获得的经验、胜利时可获得的经验,打败他至少需要的药量。

要求求出最大经验 s,输出 5*s。


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

  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)、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;
}
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Yuleo_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值