概念
dfs
dfs的目的是要达到被搜索结构的目标地址,下面是dfs的代码逻辑。
void dfs(int x,int y,int cnt) //x,y为地址坐标,cnt为深度
{
if(x>n-1 || y>m-1 || cnt>k) //剪枝
return;
if(x==n-1 && y==m-1 ){ //到达目标
ans=ans+1;
return;
}
for(int i=0;i<4;i++) //遍历情况
{
int tx=x+dir[0][i];
int ty=y+dir[1][i];
if(!book[tx][ty]) { //book标记
book[tx][ty]=1;
dfs(tx,ty,cnt+1);
book[tx][ty]=0;
}
}
return;
}
dp
- 状态 :例如背包问题中,前i 个物品在空间为j的状态下的最大价值。
- 状态转移方程:边界的定义,状态和状态之间的关系
- 储存当前最优状态的值
#include<string.h>
int dp[100][600];
int N[100];
int dfs(int num,int sum){
if(dp[num][sum]!=-1)return dp[num][sum];
if(sum==500)
return dp[num][sum]=1;
if(num>=100||sum>500)
return dp[num][sum]=0;
int ans=0;
ans+=dfs(num+1,sum);//选
ans+=dfs(num+1,sum+N[num]);//不选
return dp[num][sum]=ans;
}
int main()
{
memset(dp,-1,sizeof(dp));
}
地宫取宝
资源限制
时间限制:1.0s 内存限制:256.0MB
问题描述
X 国王有一个地宫宝库。是 n x m 个格子的矩阵。每个格子放一件宝贝。每个宝贝贴着价值标签。
地宫的入口在左上角,出口在右下角。
小明被带到地宫的入口,国王要求他只能向右或向下行走。
走过某个格子时,如果那个格子中的宝贝价值比小明手中任意宝贝价值都大,小明就可以拿起它(当然,也可以不拿)。
当小明走到出口时,如果他手中的宝贝恰好是k件,则这些宝贝就可以送给小明。
请你帮小明算一算,在给定的局面下,他有多少种不同的行动方案能获得这k件宝贝。
输入格式
输入一行3个整数,用空格分开:n m k (1<=n,m<=50, 1<=k<=12)
接下来有 n 行数据,每行有 m 个整数 Ci (0<=Ci<=12)代表这个格子上的宝物的价值
输出格式
要求输出一个整数,表示正好取k个宝贝的行动方案数。该数字可能很大,输出它对 1000000007 取模的结果。
样例输入
2 2 2
1 2
2 1
样例输出
2
样例输入
2 3 2
1 2 3
2 1 5
样例输出
14
思路
dfs对于较大数组遍历的时候,因为多次递归会超时,这里提出一个解决的办法,将每次遍历的值储存在dp中,这里有几个注意的点:
- 要判断该状态之前是否遍历,这个对dp初始化为-1
- dfs 的出口,满足条件返回1,不满足条件返回0,dfs首先返回的一定是叶子节点,是一个置顶向上的过程
- 对于中间过程,一个状态对应多个状态,该状态的值是后面状态的和值
- 最后,每一次返回都要进行记录一次当前状态的值,便于下次直接调用
代码
#include<iostream>
#include<stdio.h>
#include<algorithm>
#include<math.h>
#include<string.h>
using namespace std;
typedef long long ll;
int n,m,k;
int dir[2][2]={{1,0},{0,1}};
int dp[55][55][15][15];
int N[55][55];
ll dfs(int x,int y,int num,int value)
{
if(dp[x][y][num][value+1]!=-1)
return dp[x][y][num][value+1];
if(x==n && y==m){
if(num==k || (num==k-1 && N[x][y]>value))
return dp[x][y][num][value+1]=1;
else return dp[x][y][num][value+1]=0;
}
ll cnt=0;
for(int i=0;i<2;i++)
{
int tx=x+dir[0][i];
int ty=y+dir[1][i];
if(tx>n || ty >m)
continue;
if(value < N[x][y])
cnt+=dfs(tx,ty,num+1,N[x][y]);
cnt+=dfs(tx,ty,num,value);
}
return dp[x][y][num][value+1]=cnt%1000000007;
}
int main()
{
cin>>n>>m>>k;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
cin>>N[i][j];
memset(dp,-1,sizeof(dp));
dfs(1,1,0,-1);
cout<<dp[1][1][0][0];
}
格子刷油漆
问题描述
X国的一段古城墙的顶端可以看成 2*N个格子组成的矩形(如下图所示),现需要把这些格子刷上保护漆。
你可以从任意一个格子刷起,刷完一格,可以移动到和它相邻的格子(对角相邻也算数),但不能移动到较远的格子(因为油漆未干不能踩!)
比如:a d b c e f 就是合格的刷漆顺序。
c e f d a b 是另一种合适的方案。
当已知 N 时,求总的方案数。当N较大时,结果会迅速增大,请把结果对 1000000007 (十亿零七) 取模。
输入格式
输入数据为一个正整数(不大于1000)
输出格式
输出数据为一个正整数。
样例输入
2
样例输出
24
样例输入
3
样例输出
96
样例输入
22
样例输出
359635897
思路
随着n的增大,递归的深度越深,运算量会急剧增加,不适合用深搜,联想递推即动态规划来解决这个问题。
dp[0][i]表示不返回,只去。dp[1][i]表示返回到出发列的另一行。表示列数为i时,一共有多少种方法。
- 从顶点出发,下一步走向该列的下一行,dp[0][i]=2*dp[0][i-1]
- 从顶点出发,下一步走向下一列,之后返回上一列,dp[0][i]=2*2*dp[0][i-2]
- 从顶点出发,下一步走向下一列,之后继续下一列,dp[1][i]=2*dp[1][i-1]
dp[0][i]=2*dp[0][i-1]+4*dp[0][i-2]+2*dp[1][i-1]
dp[1][i]=2*dp[1][i-1]
sum=4*dp[0][i]
- 从中间一个点出发,注意这个地方dp[0][i]包含dp[1][i]的方案
- sum=(2*dp[0][i-1]*dp[1][n-i+1]+dp[1][i]*2*dp[0][n-i])*2
当然,还需要注意初始值的情况,当n=1的时候,只有两种走法,当n=2的时候,从一个顶点出发有6种情况。
代码
#include<iostream>
#include<string>
#include<algorithm>
#include<math.h>
using namespace std;
typedef long long ll;
const int mod=1000000007;
ll dp[2][1005];
ll ans;
int n;
int main()
{
cin>>n;
if(n==1)
{
cout<<2<<endl;
return 0;
}
dp[0][1]=1;
dp[0][2]=6;
dp[1][1]=1;
dp[1][2]=2;
for(int i=3;i<=n;i++)
{
dp[1][i]=(2*dp[1][i-1])%mod;
dp[0][i]=(2*dp[0][i-1]+dp[1][i]+4*dp[0][i-2])%mod;
}
ans=4*dp[0][n]%mod;
for(int i=2;i<n;i++)
ans=(ans+4*(dp[0][i-1]*dp[1][n-i+1]+dp[1][i]*dp[0][n-i]))%mod;
cout<<ans<<endl;
return 0;
}
解决办法
dfs超时的时候,记录下当前状态的值,但一定要优先记录最下层的递归值。dp就需要找到递推公式,并且分情当前状态。
总结
- 一般dp的题目用dfs可以解答,但在有时间条件的限制下,dfs超时了就要用到dp,一半dp用for循环完成,但在一些dfs的题目中,用dfs遍历状态,dp来加快计算,减少遍历次数。
- 动态规划的重点在于,定义节点的状态,每个节点的最优状态可以从之前某个或某几个节点通过运算得到,了解这个关系。