这周主要练习了记忆化搜索和基础数据结构。
这次的acm社团面试问了个很有意思的问题,就是bfs能否用栈实现。这个问题刚听到的时候挺懵的,不懂他到底想问啥。面试后好好思考了一下,这个答案是肯定的。bfs的本质就是从一个父状态衍生出多个合法的子状态,并不断判断每一种状态并继续产生每个子状态的子状态...
而我们为什么要用队列实现呢?因为队列按先进先出的特性先处理完更早压入队列的状态,一层一层的进行搜索,直到搜索 终点,此时的步数就是最短步数。用队列实现我们的逻辑和思路会非常清晰,但是如果只是想实现处理状态,和产生子状态直到搜索到目标的话,我认为只需要一个容器就能实现。虽然这个问题的结果可能没什么意义,但是思考这个问题的过程还是让我对搜索的认识更深入了一些。
这题的朴素搜索思路比较简单,
1.搜索方向
2.搜索出口
首先考虑搜索方向,朴素的搜索我们可以考虑搜索每种花摆的个数,并加入sum。
然后是搜索的出口,只需要sum等于目标数m,方案数ans++,回溯,如果sum超过的目标数直接返回。
import java.math.BigInteger;
import java.util.HashMap;
import java.util.Scanner;
public class Main {
static public int sum=0;
static public int m,n;
static int [] arr=new int[1000];
static int []dp=new int[1000];
public static void main(String[] args) {
Scanner in=new Scanner(System.in);
n=in.nextInt();
m=in.nextInt();
for(int j=1;j<=n;j++ ){
arr[j]=in.nextInt();
}
dfs(0,1);
System.out.println(sum);
}
public static void dfs(int now,int dep){
if(now>m)
return;
if(now==m){
sum++;
return;
}
if(dep>n)
return ;
for(int j=0;j<=arr[dep];j++){
dfs(j+now,dep+1);
}
}
}
如果采用记忆优化,搜索方向和出口其实并没有改变,但是效率高了很多
import java.math.BigInteger;
import java.util.HashMap;
import java.util.Scanner;
public class Main {
static public int m,n;
static int mod=(1000007);
static int [] arr=new int[1000];
static int [][]dp=new int[1000][1000];
public static void main(String[] args) {
Scanner in=new Scanner(System.in);
n=in.nextInt();
m=in.nextInt();
for(int j=1;j<=n;j++ ){
arr[j]=in.nextInt();
}
System.out.println(dfs1(0,1));
}
public static int dfs1(int now,int dep){
if(now==m)
return 1;
if(now>m)
return 0;
if(dep>n)
return 0;
if(dp[now][dep]!=0){
return dp[now][dep];
}
int t=0;
for(int j=0;j<=arr[dep];j++){
t=(t+dfs1(now+j,dep+1))%mod;
}
dp[now][dep]=(t%mod);
return t;
}
}
所以设dp[j][i]表示长度为j的a串要变成长度i的b串需要的最少步骤。也就是说此时的a子串完全等于此时的b子串
但是我们现在只去讨论记忆化搜索不讨论状态转移咋得出的,这题让我学到了什么。
首先记忆化搜索本质也就是搜索,既然是搜索,我们就要明确两大点
1.搜索方向
2.搜索出口
首先是搜索方向,这题其实比较复杂。我们去分析每种操作对下一个搜索状态的影响
首先我们可以不进行操作,但是不操作又要保证a与b串匹配,只能当前a的字符与b的字符相同。才能匹配。所以当a[j]==a[i]时即可不进行操作,此时的操作数继承之前的 dp[j-1][i-1]
1.删除一个字符,首先,如果a串当前字符和b串当前字符相同,就没必要执行这一步。如果要删显然必须删除当前不同的a串字符。删除了当前字符,那么此时的状态则有删除之后的子串j-1得到。此时则需要搜索dfs(j-1,i)
2.插入一个字符,如果要在不匹配出插入一个字符,首先此字符肯定是与b串当前位置相同的字符。那么相当于a串的匹配不需要考虑b的这个字符,因为我能直接添加一个与他匹配,转而搜索dfs(j,i-1)
3.替换一个字符,这个操作相对好理解一些,a串当前位置的字符不匹配,则替换该字符为b的字符达到匹配的效果。此时,a和b都不再需要考虑这个字符了,因为我能进行一次操作完成这个字符的匹配,所以转而搜索dfs(j-1,i-1)
这样搜索方向就出来了。
接下来考虑搜索的出口,也就是搜索何时结束。
很显然,当我们从原串向下搜索子串的时候,当搜索的其中一个子串为空,开始返回。
而且因为需要记忆化,所以搜索到其中一个记忆数组已经计算过的时候也能直接返回
最后还有注意搜索的边界,如果字符串从0开始,那么向下搜索判断空串的时候,下标应为-1,而记忆数组的下标不能为负数,所以字符串的下标位置需要整体加一。
代码:(ps:java写算法题好难受)
import java.math.BigInteger;
import java.util.HashMap;
import java.util.Scanner;
public class Main {
public static String a;
public static String b;
public static int[][] dp = new int[2001][2001];
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
a = in.next();
b = in.next();
for(int j=0;j<=a.length();j++){
for(int i=0;i<=b.length();i++)
dp[j][i]=-1;
}
System.out.println(dfs(a.length(),b.length()));
}
public static int dfs(int l1,int l2){
if(dp[l1][l2]!=-1){
return dp[l1][l2];
}
if(l1==0){
return dp[l1][l2]=l2;
}
if(l2==0){
return dp[l1][l2]=l1;
}
if(a.charAt(l1-1)==b.charAt(l2-1)){
return dp[l1][l2]=dfs(l1-1,l2-1);
}
int n=99999999;
for(int j=1;j<=3;j++){
if(j==1){
n=Math.min(dfs(l1-1,l2)+1,n);
}
else if(j==2){
n=Math.min(dfs(l1,l2-1)+1,n);
}
else
n=Math.min(dfs(l1-1,l2-1)+1,n);
}
dp[l1][l2]=n;
return n;
}
}