目录
一.两人玩游戏
题目:有一个正整数数组,A和B两个人轮流拿数组的最左或最右的数值,返回最终的最高分数。
暴力递归版本
java
public static int win1(int[] arr) {
if (arr == null || arr.length == 0) {
return 0;
}
// 0.。N-1范围上,先手获得最优分和后手获得最优分,返回较大的
return Math.max(f(arr, 0, arr.length - 1), s(arr, 0, arr.length - 1));
}
// 先手函数
// 当前该你拿,arr[i..j]
// 返回你的最好分数
public static int f(int[] arr, int i, int j) {
if (i == j) {
return arr[i];
}
// i..j
return Math.max(arr[i] + s(arr, i + 1, j), arr[j] + s(arr, i, j - 1));
}
// 后手函数
// 当前步该你拿,是对方在arr[i..j]范围上拿
// 返回你的最好分数
public static int s(int[] arr, int i, int j) {
if (i == j) {
return 0;
}
// i..j 我在哪个范围选数,是由对手决定的,对手一定会将较少的留给你
return Math.min(f(arr, i + 1, j), f(arr, i, j - 1));
}
记忆化搜索版本
java
public static int win1(int[] arr) {
if (arr == null || arr.length == 0) {
return 0;
}
int[][] fdp=new int[arr.length][arr.length];
int[][] sdp=new int[arr.length][arr.length];
for(int i=0;i<arr.length;i++) {
for(int j=0;j<arr.length;j++) {
fdp[i][j]=-1;
sdp[i][j]=-1;
}
}
// 0.。N-1范围上,先手获得最优分和后手获得最优分,返回较大的
return Math.max(f(arr, 0, arr.length - 1, fdp, sdp), s(arr, 0, arr.length - 1, fdp, sdp));
}
// 先手函数
// 当前该你拿,arr[i..j]
// 返回你的最好分数
public static int f(int[] arr, int i, int j, int[][] fdp, int[][] sdp) {
if (i == j) {
return arr[i];
}
if(fdp[i][j]!=-1) {
return fdp[i][j];
}
// i..j
fdp[i][j]=Math.max(arr[i] + s(arr, i + 1, j), arr[j] + s(arr, i, j - 1));
return fdp[i][j];
}
// 后手函数
// 当前步该你拿,是对方在arr[i..j]范围上拿
// 返回你的最好分数
public static int s(int[] arr, int i, int j, int[][] fdp, int[][] sdp) {
if (i == j) {
return 0;
}
if(sdp[i][j]!=-1) {
return sdp[i][j];
}
// i..j 我在哪个范围选数,是由对手决定的,对手一定会将较少的留给你
sdp[i][j]=Math.min(f(arr, i + 1, j), f(arr, i, j - 1));
return sdp[i][j];
}
动态规划版本
每个位置都依赖于该位置左侧和下侧一个格子。所以这里可以按照对角线交替求解。
java
public static int win2(int[] arr) {
if (arr == null || arr.length == 0) {
return 0;
}
int[][] f = new int[arr.length][arr.length];
int[][] s = new int[arr.length][arr.length];
for (int j = 0; j < arr.length; j++) {
f[j][j] = arr[j];
for (int i = j - 1; i >= 0; i--) {
f[i][j] = Math.max(arr[i] + s[i + 1][j], arr[j] + s[i][j - 1]);
s[i][j] = Math.min(f[i + 1][j], f[i][j - 1]);
}
}
return Math.max(f[0][arr.length - 1], s[0][arr.length - 1]);
}
关键点:把图画出来,标出base case,找出一般位置的依赖关系。
二.象棋游戏
题目:马起始位于(0,0)位置,随后给出目标位置(x, y),要求马必须走k步到达目标位置,问从起始到达目标方案数
暴力递归版本
java
public static int getWays(int x, int y, int step) {
return process(x, y, step);
}
// 潜台词:从(0, 0)位置出发
// 要去往(x, y)位置,必须跳step步
// 返回方法数
public static int process(int x, int y, int step) {
// 越界判断,有多少种到达越界位置,0种方法
if (x < 0 || x > 8 || y < 0 || y > 9) {
return 0;
}
// 已经跳完了,不能动了
if (step == 0) {
return (x == 0 && y == 0) ? 1 : 0;
}
// 要到达的位置不越界,也有步数可以跳
return process(x - 1, y + 2, step - 1)
+ process(x + 1, y + 2, step - 1)
+ process(x + 2, y + 1, step - 1)
+ process(x + 2, y - 1, step - 1)
+ process(x + 1, y - 2, step - 1)
+ process(x - 1, y - 2, step - 1)
+ process(x - 2, y - 1, step - 1)
+ process(x - 2, y + 1, step - 1);
}
动态规划版本
java
public static int dpWays(int x, int y, int step) {
if (x < 0 || x > 8 || y < 0 || y > 9 || step < 0) {
return 0;
}
int[][][] dp = new int[9][10][step + 1];
dp[0][0][0] = 1;
for (int h = 1; h <= step; h++) {
for (int r = 0; r < 9; r++) {
for (int c = 0; c < 10; c++) {
dp[r][c][h] += getValue(dp, r - 1, c + 2, h - 1);
dp[r][c][h] += getValue(dp, r + 1, c + 2, h - 1);
dp[r][c][h] += getValue(dp, r + 2, c + 1, h - 1);
dp[r][c][h] += getValue(dp, r + 2, c - 1, h - 1);
dp[r][c][h] += getValue(dp, r + 1, c - 2, h - 1);
dp[r][c][h] += getValue(dp, r - 1, c - 2, h - 1);
dp[r][c][h] += getValue(dp, r - 2, c - 1, h - 1);
dp[r][c][h] += getValue(dp, r - 2, c + 1, h - 1);
}
}
}
return dp[x][y][step];
}
public static int getValue(int[][][] dp, int row, int col, int step) {
if (row < 0 || row > 8 || col < 0 || col > 9) {
return 0;
}
return dp[row][col][step];
}
三.鲍勃存活
问题:有一个大小为N*M的网格,给出鲍勃的起始位置(a,b),鲍勃必须走K步,求鲍勃活下来的概率(不越界)。
4^k就是Bob走K步的所有情况,所以概率就是用生存的( 方法数/4^k )
暴力递归版本
java
public static String bob1(int N, int M, int i, int j, int K) {
long all = (long) Math.pow(4, K);
long live = process(N, M, i, j, K);
long gcd = gcd(all, live);
return String.valueOf((live / gcd) + "/" + (all / gcd));
}
// N*M的区域,Bob从(row,col)位置出发,走rest步之后,获得的生存方法数
public static long process(int N, int M, int row, int col, int rest) {
if (row < 0 || row == N || col < 0 || col == M) {
return 0;
}
// row, col没越界
if (rest == 0) {
return 1;
}
// 还没走完,row, col也没有越界
long live = process(N, M, row - 1, col, rest - 1);
live += process(N, M, row + 1, col, rest - 1);
live += process(N, M, row, col - 1, rest - 1);
live += process(N, M, row, col + 1, rest - 1);
return live;
}
public static long gcd(long m, long n) {
return n == 0 ? m : gcd(n, m % n);
}
记忆化、动态规划套路一样。。。
四.凑钱方案数问题
题目:有一个无重复元素的正整数数组,里面的元素可以重复使用任意次,给定一个目标面值aim,求达到目标aim的组合方法数。
暴力递归版本
java
// arr里都是正数,没有重复值,每一个值代表一种货币,每一种货币可以使用无限张
// 最终要找的零钱数就是aim
// 找零方法数返回
public static int way1(int[] arr, int aim) {
return process(arr, 0, aim);
}
// 可以自由使用arr[index...]所有的面值
// 需要搞定的钱数是rest
// 返回找零的方法数
public static int process(int[] arr, int index, int rest) {
if(index==arr.length) {
return rest==0?1:0;
}
// arr[index] 0张 1张 ... 不要超过rest的钱数
int ways=0;
for(int zhang=0;arr[index]*zhang<=rest;zhang++) {
ways+=process(arr, index+1, rest-arr[index]*zhang);
}
return ways;
}
动态规划版本
java
public static int way2(int[] arr, int aim) {
if(arr==null||arr.length==0)){
return 0;
}
int[][] dp=new int[arr.length+1][aim+1];
dp[arr.length][0]=1;
for(int index=arr.length-1;index>=0;index--) {
for(int rest=0;rest<=aim;rest++) {
int ways=0;
for(int zhang=0;arr[index]*zhang<=rest;zhang++) {
ways+=dp[index+1][rest-arr[index]*zhang];
}
dp[index][rest] = ways;
}
}
return dp[0][aim];
}
上面第三层的循环可以进行优化
? = a + b + c + ...
X = b + c + ...
=> ? = X + a
java
public static int way2(int[] arr, int aim) {
if(arr==null||arr.length==0)){
return 0;
}
int[][] dp=new int[arr.length+1][aim+1];
dp[arr.length][0]=1;
for(int index=arr.length-1;index>=0;index--) {
for(int rest=0;rest<=aim;rest++) {
dp[index][rest]=dp[index+1][rest];
if(rest-arr[index]>=0) {
dp[index][rest]+=dp[index][rest-arr[index]];
}
}
}
return dp[0][aim];
}