(写在前面:这个东西可能很早之前就有了,不过我是蓝桥杯博弈题刷多了,自己总结出来了,所以记录一下吧)
一、从上到下dfs的不足和解决方案
在看模板之前,我们先来看一下传统的从上到下的dfs (深度优先遍历)有什么不足,来看一个最简单的 Fabonacii数列:Fn=Fn-1+Fn-2。看下下面的递归代码:
private static int f(int i)
{
if(i==1||i==2)
return 1;
return f(i-1)+f(i-2);
}
上面这一段递归代码,简单明了,一看就懂。可是效率真的不敢恭维,为什么呢,上一张图解释一下
如上图所示,比如我们要计算f5,那么就得计算 f4和f3,在计算f4的时候,算了一遍f3,然后算完f4, 还得算一遍f3
当n很小的时候,时间方面是hold得住的,比如就简单一个f5,而当n稍微大一点,求个f40就慢得不行了!
更不用说求f1000那样的大数了。
那么我们应该如何减少时间?提高效率?
一种方法是不要采用从顶到下,而是从下到上,这样就可以减少大量的时间
当然这种逆流而上的方法对于求解这道题是很好的,但是这种方法和后面要讨论的博弈题的解题模板没有太多关系,故不涉及太多。事实上, 从下到上需要确定每一个递归出口,在博弈题中通常不那么好确定每一个递归出口;而且从下到上这种思维与博弈的过程是逆向的,理解起来可能会费力。
那么现在问题就变成了,还是从顶到下,我们要怎么减少时间?提高效率?
费时是因为我们一直在做重复工作,我们算了太多遍的f3,说白了,它算了一遍的f3,没有把它保存起来,等到下次用到又得重新算一遍。那我们就这样,它算出一遍f3,就找个本子把它抄下来,f3=2, 这样下次再需要f3,就先看看本子里面有没有f3,没有再算。这样就把重复计算浪费的时间换成在本子上查找的时间。一般来说查找时间都会远远小于重复计算的时间,所以可以缩短时间。
上面说的所有浓缩成几个字:有记忆的dfs
二、有记忆的dfs应用到博弈题
我们知道博弈题一般输入都是一个局面,输出是必胜局,必败局还是平局
那么如何用dfs来解决博弈题目呢
我们知道一个局面和另一个局面之间都是一步,那么我们可以通过所有的下一个可能局面来推出当前局面是必胜还是其他
比如上图,该局面可以有三种走法,第一种导致对手必胜局,所以就是自己失败了;第二种是打平,第三种是让对手失败,也就是自己赢了,既然三种走法中最后一种是让对手必败,那么用最聪明的走法,就走这一步,这个局面就算必胜局了
基于这样的一种思想,我们就可以用dfs来解决博弈题了
先给出一个模板:
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class Main{
static int[] state; //局面状态,不一定就是int数组
static Map<String,Integer>map=new HashMap<String,Integer>();//局面键-必胜信息 用于保存,也就是有记忆所在
public static void main(String[] args) {
}
private static String g(){return "";}//局面键生成函数 TODO
private static int r(List<Integer> list){return 0;}//递推关系函数,博弈题一般都是 -min{list} TODO
private static int dfs()
{
if(map.containsKey(g())) //如果之前有保存,则返回
{
return map.get(g());
}
if(1==1) //递推出口 TODO
{
map.put(g(), 1);
return 1;
}
List<Integer> list=new ArrayList<Integer>();
//for(可能的局面i:可能的局面集*) //TODO
//{
// 走到局面i
// list.add(dfs());
// 复原原来局面 《--- 千万记得复原,这是dfs的精髓所在
//}
map.put(g(), r(list));
return r(list);
}
}
现在就模板里面的元素解释一番:
首先是局面状态 state, 这是保存一个局面状态的东西,必须能找到一个东西能记录下局面,一般都是多维数组
然后是map,这个就是“有记忆力的dfs”的记忆所在,只要算过的f3,都被记录在里面,相当于上文说的那个本子
g()函数:这个函数把一个局面生成一个局面键,局面一般来说都是多维的数组,它是不太可能作为map的键的,所以需要这样一个局面键生成函数,把局面生成局面键,方便map记录
r()函数:递推关系rule函数,它是一种递推关系,看这一节的第一个图,{1,0,-1}->1 就是这样的一个规则
想象一下我们拿到一个局面,如果我们知道所有下一个局面的必胜性,我们会先找找有没有让对手失败的,那样我们就能赢,所以我们会找-1,如果有的话我们就是1。如果没有-1了,那我们会尽量与对手打平,找下一个是0的;如果-1没有,0也没有,所有的下一个局面都是1,都是让对手赢,那么我们就只能等着输了,所以是 -1必败局。
经过上面一番讲解,我们就可以轻松推出来:博弈题的r( list )函数 一般都是 -min{list}
在list里面找min, 再加多一个负号
list:为什么模板里面有一个list呢,这是处理可变分支的。当我们求Fabonacii数列时,每个结点都只有两个子结点,也就是说是双分支的。可是在解决博弈题的时候,子结点的数量一般来说都不是确定的,是可变的,所以我们就用一个list,把每次子结点dfs()出来的值都add进去,然后再交给r()函数处理
三、一道博弈题作为例子
【编程题】(满分34分)
1. 不能放置在已经放置火柴棒的地方(即只能在空格中放置)。
2. 火柴棒的方向只能是竖直或水平放置。
3. 火柴棒不能与其它格子中的火柴“连通”。所谓连通是指两根火柴棒可以连成一条直线,且中间没有其它不同方向的火柴“阻拦”。
例如:图[1.jpg]所示的局面下,可以在C2位置竖直放置(为了方便描述格子位置,图中左、下都添加了标记),但不能水平放置,因为会与A2连通。同样道理,B2,B3,D2此时两种方向都不可以放置。但如果C2竖直放置后,D2就可以水平放置了,因为不再会与A2连通(受到了C2的阻挡)。
4. 游戏双方轮流放置火柴,不可以弃权,也不可以放多根。直到某一方无法继续放置,则该方为负(输的一方)。
游戏开始时可能已经放置了多根火柴。
你的任务是:编写程序,读入初始状态,计算出对自己最有利的放置方法并输出。
如图[1.jpg]的局面表示为:
00-1
-000
0100
即用“0”表示空闲位置,用“1”表示竖直放置,用“-”表示水平放置。
【输入、输出格式要求】
用户先输入整数 n(n<100), 表示接下来将输入 n 种初始局面,每种局面占3行(多个局面间没有空白行)。
程序则输出:每种初始局面情况下计算得出的最佳放置法(行号+列号+放置方式)。
例如:用户输入:
2
0111
-000
-000
1111
----
0010
则程序可以输出:
00-
211
对第一个局面,在第0行第0列水平放置
对第二个局面,在第2行第1列垂直放置
注意:
行号、列号都是从0开始计数的。
对每种局面可能有多个最佳放置方法(解不唯一),只输出一种即可。
例如,对第一个局面,001 也是正解;最第二个局面,201也是正解。
【注意】
请仔细调试!您的程序只有能运行出正确结果的时候才有机会得分!
请把所有类写在同一个文件中,调试好后,存入与【考生文件夹】下对应题号的“解答.txt”中即可。
相关的工程文件不要拷入。
请不要使用package语句。
源程序中只能出现JDK1.5中允许的语法或调用。不能使用1.6或更高版本。
套用模板如下:
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
class Main {
static char[][] state = new char[3][4]; // 局面状态,不一定就是int数组
static Map<String, Integer> map = new HashMap<String, Integer>();// 局面键-必胜信息
// 用于保存,也就是有记忆所在
public static void main(String[] args) {
Scanner s=new Scanner(System.in);
state[0]=s.nextLine().toCharArray();
state[1]=s.nextLine().toCharArray();
state[2]=s.nextLine().toCharArray();
System.out.println(dfs()); //这里只输出该局面的必胜必败信息;如果要完整完成题目,应该是必败局随便输出一个,必胜局找一个让对手输的
}
// 局面键生成函数
private static String g() {
String str="";
for(int i=0;i<3;i++)
for(int j=0;j<4;j++)
str+=state[i][j];
return str;
}
// 递推关系函数,博弈题一般都是 -min{list}
private static int r(List<Integer> list) {
int min=Integer.MAX_VALUE;
for(int i=0;i<list.size();i++)
min=min<list.get(i)?min:list.get(i);
return -min;
}
private static int dfs() {
if (map.containsKey(g())) // 如果之前有保存,则返回
{
return map.get(g());
}
List<Integer> list = new ArrayList<Integer>();
for(int i=0;i<3;i++)
for(int j=0;j<4;j++)
{
if(canPlace(i, j, '-'))
{
state[i][j]='-';
list.add(dfs());
state[i][j]='0'; //还原
}
if(canPlace(i, j, '1'))
{
state[i][j]='1';
list.add(dfs());
state[i][j]='0'; //还原
}
}
if(list.isEmpty()) //递归出口,如果无法放置,则是必败局 (这里是先写上面的递归方法再写递归出口,与模板顺序有点差别)
{
map.put(g(),-1);
return -1;
}
map.put(g(), r(list));
return r(list);
}
//辅助函数,能否在i行j列位置放置ele元素
private static boolean canPlace(int i,int j,char ele) {
if(state[i][j]!='0') //如果该位置已经有火柴
return false;
if(ele=='-')
{
int k;
for(k=i-1;k>0&&state[k][j]!='0';k++); //找到左边第一个非零元素
if(i>=0&&state[i][j]=='-') return false; //如果同样为‘-’,无法放置
for(k=i+1;k<3&&state[k][j]!='0';k++); //找到右边边第一个非零元素
if(k<=3&&state[i][j]=='-') return false; //如果同样为‘-’,无法放置
}
if(ele=='1')
{
int k;
for(k=j-1;k>0&&state[i][k]!='0';k++);
if(i>=0&&state[i][j]=='1') return false;
for(k=i+1;k<2&&state[i][k]!='0';k++);
if(k<=2&&state[i][j]=='1') return false;
}
return true;
}
}
这样简单的套一套模板,思考填上模板的几个函数,就可以解决这道题了
g()函数,这道题直接采用把12个char连起来,作为局面键,例如“-0001010---1”就是一个局面键
r()函数,还是不变的-min{list}
dfs()函数,这个里面有 局面到下一个局面的递推,递推出口,map记录等内容
上面这份代码,自己刚连着打出来的,所以有bug请各位多多担待,主要是套模板那个思想
时候也不早了,改天有空再写一篇博客用该模板解决 《取球博弈》题目
最后说一句,模板的通病就在于兼容性,兼容太多,对问题的针对性就不是很强了,例如在《LOL博弈》也就是《填字母游戏》那道题上,模板只能水到60分,要拿到满分,就要自己对具体的题目自己分析了。