2021.8.15 每日一题
给你一个大小为 m x n 的网格和一个球。球的起始坐标为 [startRow, startColumn] 。你可以将球移到在四个方向上相邻的单元格内(可以穿过网格边界到达网格之外)。你 最多 可以移动 maxMove 次球。
给你五个整数 m、n、maxMove、startRow 以及 startColumn ,找出并返回可以将球移出边界的路径数量。因为答案可能非常大,返回对 109 + 7 取余 后的结果。
示例 1:
输入:m = 2, n = 2, maxMove = 2, startRow = 0, startColumn = 0
输出:6
示例 2:
输入:m = 1, n = 3, maxMove = 3, startRow = 0, startColumn = 1
输出:12
提示:
1 <= m, n <= 50
0 <= maxMove <= 50
0 <= startRow < m
0 <= startColumn < n
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/out-of-boundary-paths
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
第一时间dfs,明知超时,但是还是倔强了一下
class Solution {
public static final int MOD = (int)1.0e9 + 7;
int m;
int n;
int res = 0;
int[][] dir = {{1,0},{-1,0},{0,1},{0,-1}};
public int findPaths(int m, int n, int maxMove, int startRow, int startColumn) {
//第一时间想到的肯定还是dfs,但是答案很大吓到我了,估计会超范围
//不过先写一下试试吧
this.m = m;
this.n = n;
//因为可以折返,所以不应该有标记走到过的数组
for(int i = 1; i <= maxMove; i++){
dfs(startRow, startColumn, i);
}
return res;
}
public boolean inArea(int x, int y){
return x >= 0 && x < m && y >= 0 && y < n;
}
public void dfs(int x, int y, int k){
if(k == 0 && !inArea(x, y)){
res = (res + 1) % MOD;
return;
}
if(k <= 0)
return;
if(!inArea(x, y))
return;
for(int i = 0; i < 4; i++){
int nx = x + dir[i][0];
int ny = y + dir[i][1];
dfs(nx, ny, k - 1);
}
}
}
动态规划想一下:
想不出来,看了一下题解,先根据状态定义写个记忆化搜索吧,可以在dfs基础上改,好久没写过了
开始想直接加一个记忆化数组memo,然后发现没有返回值好像不太行,因为k这里是从大到小变化的,转移会出问题。然后就加了返回值,可以了
memo[i][j][k]表示还剩下k步,从(i,j)这个点出发走出边界的路径数
记忆化搜索:
class Solution {
public static final int MOD = (int)1.0e9 + 7;
int m;
int n;
int[][] dir = {{1,0},{-1,0},{0,1},{0,-1}};
int[][][] memo;
public int findPaths(int m, int n, int maxMove, int startRow, int startColumn) {
//第一时间想到的肯定还是dfs,但是答案很大吓到我了,估计会超范围
//超时以后回过头来想哪些状态重复了
//想一个很简单的例子,走两步可以回到初始状态,所以可以把之前多少步,走到哪里的结果记录下来
//即memo[i][j][k]表示还剩下k步,从(i,j)这个点出发走出边界的路径数
this.m = m;
this.n = n;
//记忆化数组
memo = new int[m][n][maxMove + 1];
//因为可以折返,所以不应该有标记走到过的数组
//初始化
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
Arrays.fill(memo[i][j], -1);
}
}
return dfs(startRow, startColumn, maxMove);
}
public boolean inArea(int x, int y){
return x >= 0 && x < m && y >= 0 && y < n;
}
public int dfs(int x, int y, int k){
//如果步数小于0,return
if(k < 0)
return 0;
//如果步数够,且出界了,就返回1
if(k >= 0 && !inArea(x, y)){
return 1;
}
//如果不在范围内,返回
if(!inArea(x, y))
return 0;
//如果到了这个点,剩余同样的步数,那么结果是已经存在过的,就直接加这个结果
if(memo[x][y][k] != -1){
return memo[x][y][k];
}
int ans = 0;
for(int i = 0; i < 4; i++){
int nx = x + dir[i][0];
int ny = y + dir[i][1];
ans = (ans + dfs(nx, ny, k - 1)) % MOD;
}
memo[x][y][k] = ans;
return ans;
}
}
官解的动态规划:
根据当前状态推后面的状态
这个动态规划的状态定义发生了改变:dp[i][j][k] 表示k步到达(i,j)这个点的路径数目,所以能这样进行转移
class Solution {
public static final int MOD = (int)1.0e9 + 7;
int[][] dir = {{1,0},{-1,0},{0,1},{0,-1}};
public int findPaths(int m, int n, int maxMove, int startRow, int startColumn) {
//动规写一版,与一般的动规不同的是,这个是从前面的状态推后面的状态
//这种也要熟练的写出来
//dp[i][j][k] 表示k步到达(i,j)这个点的路径数目
int[][][] dp = new int[m][n][maxMove + 1];
//初始化,开始点0步是1,其他都是0
dp[startRow][startColumn][0] = 1;
int res = 0;
for(int k = 0; k < maxMove; k++){
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
int count = dp[i][j][k];
//如果能到达这里
if(count > 0){
for(int[] d : dir){
int ni = i + d[0];
int nj = j + d[1];
//如果在范围内
if(ni >= 0 && ni < m && nj >= 0 && nj < n){
dp[ni][nj][k + 1] = (dp[ni][nj][k + 1] + count) % MOD;
//如果不在范围内,就在结果中加上这个路径数目
}else{
res = (res + count) % MOD;
}
}
}
}
}
}
return res;
}
}
上面是从一个状态往后面的状态推导,即dp[i + x] = dp[i]…
而如果常用的那种思路,dp[i] = dp[i - 1],沿用上面的状态定义:
但是这种思路会导致个什么问题呢,就是我们定义的范围是在这个矩形内,所以想要出边界是无法转移的
所以到了边界上,需要对当前步数为1的情况进行处理,也就是最后处理一下边界情况
或者我认为也可以扩展一层边界,然后到达扩展层就是结果
看了三叶姐的动规,发现状态定义是和记忆化搜索的记忆化数组一样的
dp[i][j][k]表示从(i,j)点出发,步数在k次以内,能走出边界的路径数
记忆化递归是自顶向下,动规是自底向上
因此过程反过来了,是从边界返回到初始点
先处理边界,例如角就有三种可能走出去,边就有一种(一行或者一列)或者两种(多行多列)
然后倒着去找到到达初始点的路径数,这个思路怎么说呢,理解了就好了,感觉一般不会这样想
class Solution {
int MOD = (int)1e9+7;
int m, n, max;
int[][] dirs = new int[][]{{1,0},{-1,0},{0,1},{0,-1}};
public int findPaths(int _m, int _n, int _max, int r, int c) {
m = _m; n = _n; max = _max;
int[][] f = new int[m * n][max + 1];
// 初始化边缘格子的路径数量
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (i == 0) add(i, j, f);
if (j == 0) add(i, j, f);
if (i == m - 1) add(i, j, f);
if (j == n - 1) add(i, j, f);
}
}
// 从小到大枚举「可移动步数」
for (int k = 1; k <= max; k++) {
// 枚举所有的「位置」
for (int idx = 0; idx < m * n; idx++) {
int[] info = parseIdx(idx);
int x = info[0], y = info[1];
for (int[] d : dirs) {
int nx = x + d[0], ny = y + d[1];
if (nx < 0 || nx >= m || ny < 0 || ny >= n) continue;
int nidx = getIdx(nx, ny);
f[idx][k] += f[nidx][k - 1];
f[idx][k] %= MOD;
}
}
}
return f[getIdx(r, c)][max];
}
void add(int x, int y, int[][] f) {
for (int k = 1; k <= max; k++) {
f[getIdx(x, y)][k]++;
}
}
int getIdx(int x, int y) {
return x * n + y;
}
int[] parseIdx(int idx) {
return new int[]{idx / n, idx % n};
}
}
作者:AC_OIer
链接:https://leetcode-cn.com/problems/out-of-boundary-paths/solution/gong-shui-san-xie-yi-ti-shuang-jie-ji-yi-asrz/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
2021.8.16 每日一题
假设有从 1 到 N 的 N 个整数,如果从这 N 个数字中成功构造出一个数组,使得数组的第 i 位 (1 <= i <= N) 满足如下两个条件中的一个,我们就称这个数组为一个优美的排列。条件:
第 i 位的数字能被 i 整除
i 能被第 i 位上的数字整除
现在给定一个整数 N,请问可以构造多少个优美的排列?
示例1:
输入: 2
输出: 2
解释:
第 1 个优美的排列是 [1, 2]:
第 1 个位置(i=1)上的数字是1,1能被 i(i=1)整除
第 2 个位置(i=2)上的数字是2,2能被 i(i=2)整除
第 2 个优美的排列是 [2, 1]:
第 1 个位置(i=1)上的数字是2,2能被 i(i=1)整除
第 2 个位置(i=2)上的数字是1,i(i=2)能被 1 整除
说明:
N 是一个正整数,并且不会超过15。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/beautiful-arrangement
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
简单的全排列问题
class Solution {
int res = 0;
int n;
boolean[] used;
public int countArrangement(int n) {
//看到这个范围,感觉没什么好办法,就是个全排列
this.n = n;
used = new boolean[n + 1];
dfs(1);
return res;
}
//k表示当期第几位
public void dfs(int i){
if(i == n + 1){
res++;
return;
}
for(int k = 1; k <= n; k++){
//如果这个数字使用过了
if(used[k])
continue;
//否则,选一个满足条件的数字递归
if(k / i * i == k || i / k * k == i){
used[k] = true;
dfs(i + 1);
}
//回溯
used[k] = false;
}
}
}
状态压缩+动态规划:
看到最大范围15,想到用状压
mask和往常一样,表示1到n是否被使用过,如果使用过,mask对应位置就是1
如何转移呢?
首先确定mask中有多少个1,即新增的数字要放在哪个位置num
然后遍历mask中为1的位置,也就是当前优美队列中可以包含的数字
然后将这些数字分别放在num这个位置,如果能放进去,那么就可以通过之前的状态来转移
如果放不进去,跳过
最终结果就是f[(1 << n) - 1]
class Solution {
public int countArrangement(int n) {
//再练个状压
int[] f = new int[1 << n];
f[0] = 1;
//mask表示1到n位n个数字,哪个被使用过
//确定这些数字组成的优美排列的情况
for(int mask = 1; mask < (1 << n); mask++){
//mask中1的个数,表示当前数字要被放在第num位
int num = Integer.bitCount(mask);
//num个1,然后确定这些1放在什么位置
for(int i = 0; i < n; i++){
//如果mask的第i位为1,那么假定当前新增的数字就是i + 1
if(((mask >> i) & 1) == 1){
//如果把当前数字i + 1放在第num位可以的话
if(num % (i + 1) == 0 || (i + 1) % num == 0){
f[mask] += f[mask ^ (1 << i)];
}
}
}
}
return f[(1 << n) - 1];
}
}
这个状压的转移我还是没想出来,想想为什么
因为没明确mask的含义,其次没有想到mask中1的数量就是优美排列中数字的个数
其实状压dp这种套路很固定的
首先mask代表每一个数是否被选取过,选取过就是1
其次统计mask中1的个数,就是当前选了多少个数
然后将这几个数分别放在最后一位,然后由前面的状态转移(mask ^ (1 << i))
下次一定要能够自己写出来