请先阅读以下博客:
https://www.cnblogs.com/jyroy/p/10274414.html 记忆化搜索算法介绍
https://blog.youkuaiyun.com/u014665013/article/details/45201479
http://blog.sina.com.cn/s/blog_14d68bd870102we10.html
问题描述
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
资源约定:
峰值内存消耗 < 256M
CPU消耗 < 1000ms
请严格按要求输出,不要画蛇添足地打印类似:“请您输入...” 的多余内容。
所有代码放在同一个源文件中,调试通过后,拷贝提交该源码。
这道题,我也参考了很多的博客,有的用暴力搜索,有的用动态规划法,还有的虽然两者都有介绍,但思维跳跃却太大,没有一个清晰的思考的过程,只是贴了个代码,讲了一下写法,让人很难理解。在此,我想尽我最大的努力去还原我的思考过程,一点一点的去深入剖析这个问题!
首先思考如何把矩阵的每条可选路径遍历得出,先不做比较,只是单纯的得到路径的可能性
package lqb2014;
import java.util.Scanner;
public class Task9_3 {
static int n;
static int m;
static int[][] num;
static int ans[];
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
n=sc.nextInt();
m=sc.nextInt();
num=new int[n][m];//迷宫
ans=new int[n+m-1];//从入口到出口走过的格子
for(int i=0;i<n;++i)
for(int j=0;j<m;++j)
num[i][j]=sc.nextByte();
dfs(0,0,0);
}
static void dfs(int row,int col,int k) {
if(row==n-1&&col==m-1) {//到达出口
for(int i=0;i<ans.length-1;++i)
System.out.print(ans[i]);
System.out.print(num[row][col]+"\n");
}
if(row<n&&col<m) {/*防止数组越界,如果没有这句判断,row最后会等于n,虽然跳过if(row<n),
但会进入if(col<m),此时ans[k]=num[row][col]中的row就越界了,col同理*/
if(row<n) {
ans[k]=num[row][col];
dfs(row+1,col,k+1);//传参数的好处是下一层执行完毕返回后不会对上一层的参数造成影响
}
if(col<m) {
ans[k]=num[row][col];
dfs(row,col+1,k+1);
}
}
}
}
运行:
在此框架上,我们再进一步思考,如何在遍历每一条路径的同时顺便附加一个条件,那就是比较每个格子内宝物的价值。
一般选择的方向为2个以下(例如 李白打酒 走楼梯)适合用递归,本题中的选择的方向为向下或者向右,也可以理解为有关于数组的,而回溯的话就是选择的方向较大的(例如 六角幻方) 递归:进入下一个函数之前进行判断较好 。
这类问题画出解答树将清晰明了,便于理解
package lqb2014;
import java.util.Scanner;
public class Task9 {
static int n,m,k,count=0;
static int num[][];
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
n=sc.nextInt();
m=sc.nextInt();
k=sc.nextInt();
num=new int[n][m];
for(int i=0;i<n;++i) {
for(int j=0;j<m;++j) {
num[i][j]=sc.nextInt();
}
}
slove(0,0,0,0);
System.out.println(count);
}
static void slove(int row,int col,int maxVal,int amt) {//分别为所在行,列,当前已拿宝物最大值,已拿宝物数
if(row==n-1&&col==m-1) {
if(amt==k||(amt==k-1&&num[row][col]>maxVal)) {
count++;
if(count>1000000007)
count=count%1000000007;
}
}
if(row<n&&col<m) //数组容量大一点也行,因为最后的row会比n大1,col同理
{
if(row<n) {//向下走
if(maxVal<num[row][col])
slove(row+1,col,num[row][col],amt+1);//拿
slove(row+1,col,maxVal,amt);//不拿
}
if(col<m) {//向右走
if(maxVal<num[row][col])
slove(row,col+1,num[row][col],amt+1);//拿
slove(row,col+1,maxVal,amt);//不拿
}
}
}
}
运行:
到此为止,我们已经初步 解决了这个问题,但是递归本就耗时,再加上如此高的复杂度和递归深度,该算法实际可操作性不高,耗时几何倍数上升。此时,我们想到可用空间换时间的方法来解决,就本题而言,是非常适合用记忆搜索法,用可接受的空间换取大量被消耗的时间。
记忆化搜索实际上是递归来实现的,但是递归的过程中有许多的结果是被反复计算的,这样会大大降低算法的执行效率。而记忆化搜索是在递归的过程中,将已经计算出来的结果保存起来,当之后的计算用到的时候直接取出结果,避免重复运算,因此极大的提高了算法的效率。
package lqb2014;
import java.util.Scanner;
public class Task9 {
static int n,m,k;
static int num[][];
static int[][][][]dp;
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
n=sc.nextInt();
m=sc.nextInt();
k=sc.nextInt();
num=new int[n][m];
dp=new int[n+1][m+1][13][13];
for(int i=0;i<n;++i) {
for(int j=0;j<m;++j) {
num[i][j]=sc.nextInt();
}
}
for(int i=0;i<n;++i)
for(int j=0;j<m;++j)
for(int x=0;x<13;++x)
for(int y=0;y<13;++y)
dp[i][j][x][y]=-1;
slove2(0,0,0,0);
System.out.println(dp[0][0][0][0]);
}
static int slove2(int row,int col,int maxVal,int amt) {//分别为所在行,列,当前已拿宝物最大值,已拿宝物数
if(dp[row][col][maxVal][amt]!=-1)
return dp[row][col][maxVal][amt];
if(row==n-1&&col==m-1) {
if(amt==k||(amt==k-1&&num[row][col]>maxVal))
return dp[row][col][maxVal][amt]=1;
else
return dp[row][col][maxVal][amt]=0;
}
long s=0;
if(row<n&&col<m) //数组容量大一点也行,因为最后的row会比n大1,col同理
{
if(row<n) {
if(maxVal<num[row][col])
s+=slove2(row+1,col,num[row][col],amt+1);
s+=slove2(row+1,col,maxVal,amt);
}
if(col<m) {
if(maxVal<num[row][col])
s+=slove2(row, col+1, num[row][col],amt+1);
s+=slove2(row, col+1, maxVal,amt);
}
}
return dp[row][col][maxVal][amt]=(int)(s%1000000007);
}
}
运行:
两种算法效率比较
两者的效率差距悬殊,暴力解法在数据量稍大一些就基本无用了,而记忆搜索法 要好很多,我写了一个程序比较了一下,我假设迷宫是正方形的(n=m),k始终为2,取了几组数据,画了个图如下所示(记忆搜索法太小了基本看不见):
之后又单独把记忆搜索法单独多取了几组数据比较了一下(n=m取1~100) ,图如下
两种方法其实各有弊端吧,在相同规模数据量下,一个牺牲了时间,一个牺牲了空间,就本题而言记忆搜索法更优,其他题目视情况自己取舍吧!可能会有其他更优的解法,暂时想不到。
哎呀,感觉自己讲的还是不够清楚,能力有限,精力也有限,见谅!
如果你已经理解了整体的思想,也得动手自己写代码啊,大部分精髓都在代码里体现了,我没细讲,真的表达不好,如果有错的话,望请指出!