题目一
链接:https://leetcode-cn.com/problems/can-i-win
在 "100 game" 这个游戏中,两名玩家轮流选择从 1 到 10 的任意整数,累计整数和,先使得累计整数和达到或超过 100 的玩家,即为胜者。
如果我们将游戏规则改为 “玩家不能重复使用整数” 呢?
例如,两个玩家可以轮流从公共整数池中抽取从 1 到 15 的整数(不放回),直到累计整数和 >= 100。
给定一个整数 maxChoosableInteger (整数池中可选择的最大数)和另一个整数 desiredTotal(累计和),判断先出手的玩家是否能稳赢(假设两位玩家游戏时都表现最佳)?
你可以假设 maxChoosableInteger 不会大于 20, desiredTotal 不会大于 300。
// 1~choose 拥有的数字
// total 一开始的剩余
// 返回先手会不会赢
public static boolean canIWin0(int choose, int total) {
if (total == 0) {
return true;
}
if ((choose * (choose + 1) >> 1) < total) {
return false;
}
int[] arr = new int[choose];
for (int i = 0; i < choose; i++) {
arr[i] = i + 1;
}
// arr[i] != -1 表示arr[i]这个数字还没被拿走
// arr[i] == -1 表示arr[i]这个数字已经被拿走
// 集合,arr,1~choose
return process(arr, total);
}
// 当前轮到先手拿,
// 先手只能选择在arr中还存在的数字,
// 还剩rest这么值,
// 返回先手会不会赢
public static boolean process(int[] arr, int rest) {
if (rest <= 0) {
return false;
}
// 先手去尝试所有的情况
for (int i = 0; i < arr.length; i++) {
if (arr[i] != -1) {
int cur = arr[i];
arr[i] = -1;
boolean next = process(arr, rest - cur);
arr[i] = cur;
if (!next) {
return true;
}
}
}
return false;
}
// 暴力尝试
public static boolean canIWin1(int choose, int total) {
if (total == 0) {
return true;
}
if ((choose * (choose + 1) >> 1) < total) {
return false;
}
return process1(choose, 0, total);
}
// 当前轮到先手拿,
// 先手可以拿1~choose中的任何一个数字
// status i位如果为0,代表没拿,当前可以拿
// i位为1,代表已经拿过了,当前不能拿
// 还剩rest这么值,
// 返回先手会不会赢
public static boolean process1(int choose, int status, int rest) {
if (rest <= 0) {
return false;
}
for (int i = 1; i <= choose; i++) {
if (((1 << i) & status) == 0) { // i 这个数字,是此时先手的决定!
if (!process1(choose, (status | (1 << i)), rest - i)) {
return true;
}
}
}
return false;
}
// 暴力尝试改动态规划
public static boolean canIWin2(int choose, int total) {
if (total == 0) {
return true;
}
if ((choose * (choose + 1) >> 1) < total) {
return false;
}
int[] dp = new int[1 << (choose + 1)];
// dp[status] == 1 true
// dp[status] == -1 false
// dp[status] == 0 process(status) 没算过!去算!
return process2(choose, 0, total, dp);
}
// 为什么明明status和rest是两个可变参数,却只用status来代表状态(也就是dp)
// 因为选了一批数字之后,得到的和一定是一样的,所以rest是由status决定的,所以rest不需要参与记忆化搜索
public static boolean process2(int choose, int status, int rest, int[] dp) {
if (dp[status] != 0) {
return dp[status] == 1 ? true : false;
}
boolean ans = false;
if (rest > 0) {
for (int i = 1; i <= choose; i++) {
if (((1 << i) & status) == 0) {
if (!process2(choose, (status | (1 << i)), rest - i, dp)) {
ans = true;
break;
}
}
}
}
dp[status] = ans ? 1 : -1;
return ans;
}
题目二
TSP问题 有N个城市,任何两个城市之间的都有距离,任何一座城市到自己的距离都为0。所有点到点的距 离都存在一个N*N的二维数组matrix里,也就是整张图由邻接矩阵表示。现要求一旅行商从k城市 出发必须经过每一个城市且只在一个城市逗留一次,最后回到出发的k城,返回总距离最短的路的 距离。参数给定一个matrix,给定k。
public static int t1(int[][] matrix) {
int N = matrix.length; // 0...N-1
// set
// set.get(i) != null i这座城市在集合里
// set.get(i) == null i这座城市不在集合里
List<Integer> set = new ArrayList<>();
for (int i = 0; i < N; i++) {
set.add(1);
}
return func1(matrix, set, 0);
}
// 任何两座城市之间的距离,可以在matrix里面拿到
// set中表示着哪些城市的集合,
// start这座城一定在set里,
// 从start出发,要把set中所有的城市过一遍,最终回到0这座城市,最小距离是多少
public static int func1(int[][] matrix, List<Integer> set, int start) {
int cityNum = 0;
for (int i = 0; i < set.size(); i++) {
if (set.get(i) != null) {
cityNum++;
}
}
if (cityNum == 1) {
return matrix[start][0];
}
// cityNum > 1 不只start这一座城
set.set(start, null);
int min = Integer.MAX_VALUE;
for (int i = 0; i < set.size(); i++) {
if (set.get(i) != null) {
// start -> i i... -> 0
int cur = matrix[start][i] + func1(matrix, set, i);
min = Math.min(min, cur);
}
}
set.set(start, 1);
return min;
}
//暴力递归
public static int t2(int[][] matrix) {
int N = matrix.length; // 0...N-1
// 7座城 1111111
int allCity = (1 << N) - 1;
return f2(matrix, allCity, 0);
}
// 任何两座城市之间的距离,可以在matrix里面拿到
// set中表示着哪些城市的集合,
// start这座城一定在set里,
// 从start出发,要把set中所有的城市过一遍,最终回到0这座城市,最小距离是多少
public static int f2(int[][] matrix, int cityStatus, int start) {
// cityStatus == cityStatux & (~cityStaus + 1)
if (cityStatus == (cityStatus & (~cityStatus + 1))) {
return matrix[start][0];
}
// 把start位的1去掉,
cityStatus &= (~(1 << start));
int min = Integer.MAX_VALUE;
// 枚举所有的城市
for (int move = 0; move < matrix.length; move++) {
if ((cityStatus & (1 << move)) != 0) {
int cur = matrix[start][move] + f2(matrix, cityStatus, move);
min = Math.min(min, cur);
}
}
cityStatus |= (1 << start);
return min;
}
public static int t3(int[][] matrix) {
int N = matrix.length; // 0...N-1
// 7座城 1111111
int allCity = (1 << N) - 1;
int[][] dp = new int[1 << N][N];
for (int i = 0; i < (1 << N); i++) {
for (int j = 0; j < N; j++) {
dp[i][j] = -1;
}
}
return f3(matrix, allCity, 0, dp);
}
// 任何两座城市之间的距离,可以在matrix里面拿到
// set中表示着哪些城市的集合,
// start这座城一定在set里,
// 从start出发,要把set中所有的城市过一遍,最终回到0这座城市,最小距离是多少
public static int f3(int[][] matrix, int cityStatus, int start, int[][] dp) {
if (dp[cityStatus][start] != -1) {
return dp[cityStatus][start];
}
if (cityStatus == (cityStatus & (~cityStatus + 1))) {
dp[cityStatus][start] = matrix[start][0];
} else {
// 把start位的1去掉,
cityStatus &= (~(1 << start));
int min = Integer.MAX_VALUE;
// 枚举所有的城市
for (int move = 0; move < matrix.length; move++) {
if (move != start && (cityStatus & (1 << move)) != 0) {
int cur = matrix[start][move] + f3(matrix, cityStatus, move, dp);
min = Math.min(min, cur);
}
}
cityStatus |= (1 << start);
dp[cityStatus][start] = min;
}
return dp[cityStatus][start];
}
public static int t4(int[][] matrix) {
int N = matrix.length; // 0...N-1
int statusNums = 1 << N;
int[][] dp = new int[statusNums][N];
for (int status = 0; status < statusNums; status++) {
for (int start = 0; start < N; start++) {
if ((status & (1 << start)) != 0) {
if (status == (status & (~status + 1))) {
dp[status][start] = matrix[start][0];
} else {
int min = Integer.MAX_VALUE;
// start 城市在status里去掉之后,的状态
int preStatus = status & (~(1 << start));
// start -> i
for (int i = 0; i < N; i++) {
if ((preStatus & (1 << i)) != 0) {
int cur = matrix[start][i] + dp[preStatus][i];
min = Math.min(min, cur);
}
}
dp[status][start] = min;
}
}
}
}
return dp[statusNums - 1][0];
}
题目三
你有无限的1*2的砖块,要铺满M*N的区域,
不同的铺法有多少种?
public static int ways1(int N, int M) {
if (N < 1 || M < 1 || ((N * M) & 1) != 0) {
return 0;
}
if (N == 1 || M == 1) {
return 1;
}
int[] pre = new int[M]; // pre代表-1行的状况
for (int i = 0; i < pre.length; i++) {
pre[i] = 1;
}
return process(pre, 0, N);
}
// pre 表示level-1行的状态
// level表示,正在level行做决定
// N 表示一共有多少行 固定的
// level-2行及其之上所有行,都摆满砖了
// level做决定,让所有区域都满,方法数返回
public static int process(int[] pre, int level, int N) {
if (level == N) { // base case
for (int i = 0; i < pre.length; i++) {
if (pre[i] == 0) {
return 0;
}
}
return 1;
}
// 没到终止行,可以选择在当前的level行摆瓷砖
int[] op = getOp(pre);
return dfs(op, 0, level, N);
}
// op[i] == 0 可以考虑摆砖
// op[i] == 1 只能竖着向上
public static int dfs(int[] op, int col, int level, int N) {
// 在列上自由发挥,玩深度优先遍历,当col来到终止列,i行的决定做完了
// 轮到i+1行,做决定
if (col == op.length) {
return process(op, level + 1, N);
}
int ans = 0;
// col位置不横摆
ans += dfs(op, col + 1, level, N); // col位置上不摆横转
// col位置横摆, 向右
if (col + 1 < op.length && op[col] == 0 && op[col + 1] == 0) {
op[col] = 1;
op[col + 1] = 1;
ans += dfs(op, col + 2, level, N);
op[col] = 0;
op[col + 1] = 0;
}
return ans;
}
public static int[] getOp(int[] pre) {
int[] cur = new int[pre.length];
for (int i = 0; i < pre.length; i++) {
cur[i] = pre[i] ^ 1;
}
return cur;
}
// Min (N,M) 不超过 32
public static int ways2(int N, int M) {
if (N < 1 || M < 1 || ((N * M) & 1) != 0) {
return 0;
}
if (N == 1 || M == 1) {
return 1;
}
int max = Math.max(N, M);
int min = Math.min(N, M);
int pre = (1 << min) - 1;
return process2(pre, 0, max, min);
}
// 上一行的状态,是pre,limit是用来对齐的,固定参数不用管
// 当前来到i行,一共N行,返回填满的方法数
public static int process2(int pre, int i, int N, int M) {
if (i == N) { // base case
return pre == ((1 << M) - 1) ? 1 : 0;
}
int op = ((~pre) & ((1 << M) - 1));
return dfs2(op, M - 1, i, N, M);
}
public static int dfs2(int op, int col, int level, int N, int M) {
if (col == -1) {
return process2(op, level + 1, N, M);
}
int ans = 0;
ans += dfs2(op, col - 1, level, N, M);
if ((op & (1 << col)) == 0 && col - 1 >= 0 && (op & (1 << (col - 1))) == 0) {
ans += dfs2((op | (3 << (col - 1))), col - 2, level, N, M);
}
return ans;
}
// 记忆化搜索的解
// Min(N,M) 不超过 32
public static int ways3(int N, int M) {
if (N < 1 || M < 1 || ((N * M) & 1) != 0) {
return 0;
}
if (N == 1 || M == 1) {
return 1;
}
int max = Math.max(N, M);
int min = Math.min(N, M);
int pre = (1 << min) - 1;
int[][] dp = new int[1 << min][max + 1];
for (int i = 0; i < dp.length; i++) {
for (int j = 0; j < dp[0].length; j++) {
dp[i][j] = -1;
}
}
return process3(pre, 0, max, min, dp);
}
public static int process3(int pre, int i, int N, int M, int[][] dp) {
if (dp[pre][i] != -1) {
return dp[pre][i];
}
int ans = 0;
if (i == N) {
ans = pre == ((1 << M) - 1) ? 1 : 0;
} else {
int op = ((~pre) & ((1 << M) - 1));
ans = dfs3(op, M - 1, i, N, M, dp);
}
dp[pre][i] = ans;
return ans;
}
public static int dfs3(int op, int col, int level, int N, int M, int[][] dp) {
if (col == -1) {
return process3(op, level + 1, N, M, dp);
}
int ans = 0;
ans += dfs3(op, col - 1, level, N, M, dp);
if (col > 0 && (op & (3 << (col - 1))) == 0) {
ans += dfs3((op | (3 << (col - 1))), col - 2, level, N, M, dp);
}
return ans;
}
// 严格位置依赖的动态规划解
public static int ways4(int N, int M) {
if (N < 1 || M < 1 || ((N * M) & 1) != 0) {
return 0;
}
if (N == 1 || M == 1) {
return 1;
}
int big = N > M ? N : M;
int small = big == N ? M : N;
int sn = 1 << small;
int limit = sn - 1;
int[] dp = new int[sn];
dp[limit] = 1;
int[] cur = new int[sn];
for (int level = 0; level < big; level++) {
for (int status = 0; status < sn; status++) {
if (dp[status] != 0) {
int op = (~status) & limit;
dfs4(dp[status], op, 0, small - 1, cur);
}
}
for (int i = 0; i < sn; i++) {
dp[i] = 0;
}
int[] tmp = dp;
dp = cur;
cur = tmp;
}
return dp[limit];
}
public static void dfs4(int way, int op, int index, int end, int[] cur) {
if (index == end) {
cur[op] += way;
} else {
dfs4(way, op, index + 1, end, cur);
if (((3 << index) & op) == 0) { // 11 << index 可以放砖
dfs4(way, op | (3 << index), index + 1, end, cur);
}
}
}