Leetcode - 周赛448
目录
- 一,3536. 两个数字的最大乘积
- 二,3537. 填充特殊网格
- 方法一:递归
- 方法二:循环
- 三,3538. 合并得到最小旅行时间
- 四,3539. 魔法序列的数组乘积之和
一,3536. 两个数字的最大乘积
题目列表
本题就是求最大值和次大值的乘积,代码如下:
class Solution {public int maxProduct(int n) {int[] mx = new int[2];while(n > 0){int x = n % 10;if(mx[0] < x){mx[1] = mx[0];mx[0] = x;}else if(mx[1] < x){mx[1] = x;}n /= 10;}return mx[0] * mx[1];}
}
二,3537. 填充特殊网格
题目列表
本题将一个 2 n ∗ 2 n 2^n * 2^n 2n∗2n 的网格,不断均等的分成四个部分,且四个部分都有要求:
- 右上角象限中的所有数字都小于右下角象限中的所有数字
- 右下角象限中的所有数字都小于左下角象限中的所有数字
- 左下角象限中的所有数字都小于左上角象限中的所有数字
- 每个象限也都是一个特殊网格
方法一:递归
可以发现这就是递归,不断的将 2 n ∗ 2 n 2^n * 2^n 2n∗2n 的网格分成 2 n − 1 ∗ 2 n − 1 2^{n-1} * 2^{n-1} 2n−1∗2n−1 的网格,即将原问题变成子问题来解决。根据上述条件,我们的计算顺序:右上 — 右下 —— 左下 —— 左上.
代码如下:
class Solution {public int[][] specialGrid(int n) {int[][] ans = new int[1<<n][1<<n];dfs(0, 1<<n, 0, 1<<n, ans);return ans;}int k = 0;void dfs(int l, int r, int u, int d, int[][] ans){if(r - l == 1){ans[u][l] = k++;return;}int m = (r - l) / 2;dfs(l+m, r, u, d-m, ans); // 右上dfs(l+m, r, u+m, d, ans); // 右下dfs(l, r-m, u+m, d, ans); // 左下dfs(l, r-m, u, d-m, ans); // 左上}
}
方法二:循环
代码如下:
class Solution {public int[][] specialGrid(int n) {int[][] ans = new int[1<<n][1<<n];for(int k = 0; k < n; k++) {for(int i = 0; i < (1 << k); i++) {for(int j = (1 << n) - (1 << k); j < (1 << n); j++) {ans[i+(1<<k)][j] = ans[i][j] + (1 << (2*k));ans[i+(1<<k)][j-(1<<k)] = ans[i+(1<<k)][j] + (1 << (2*k));ans[i][j-(1<<k)] = ans[i+(1<<k)][j-(1<<k)] + (1 << (2*k));}}}return ans;}
}
三,3538. 合并得到最小旅行时间
题目列表
本题大致意思:将长度为 l
的公里分成 n
段,position[i]:表示路标的位置(从0到i的总距离)
,可以将这 n
段公路进行合并(相邻合并),问恰好k
次合并后从0-l
的最小旅行时间。time[i]:表示从position[i]到position[i+1]每公里所需时间
。
这么大致一看,就是划分型DP,不过有一点需要注意,它们的时间合并与路段合并的方式并不相同,举个例子:
position:[0,3,8,10],time = [5,8,3,6]
- 不合并,它的最终结果是
(3 - 0) * 5 + (8 - 5) * 8 + (10 - 8) * 3
- 合并
1,2
下标的路段后,position:[0,8,10],time = [5,11,6]
- 它的结果是
(8 - 0) * 5 + (10 - 8) * 11
- 可以发现,合并前它第一个段路每公里所需时间是
5
,合并后它第一个段路每公里所需时间依然是5
,也就是合并时间只会对后面的路段造成影响。(这点在状态转移时非常重要!!!!)
由于需要在O(1)
的时间求出每段路径的速度,所以需要先对time
进行前缀和的处理,定义dfs(i,j,k):[i,n-1] 这段路恰好分成 k 段所需的最短旅行时间
,对于当前所在的position[i]
路标来说,枚举下一段路的终点 position[nxt]
:
positon[i] 到 position[nxt]
所消耗的时间(pre[i+1] - pre[j]) * (position[nxt] - position[i])
- 下一个状态就变成
dfs(nxt,?,?)
- 上面说了合并时间只会对后面的路段造成影响,所以
j -> i+1(因为前缀和的原因所以+1)
k -> k - (nxt - i - 1)
,[i,nxt]
说明原本有nxt-i
段路,直接合并成了一段,所以k - (nxt - i - 1)
- 上面说了合并时间只会对后面的路段造成影响,所以
- 转移方程
dfs(i,j,k) = min(dfs(nxt, i+1, k-(nxt-i-1)))
代码如下:
// dfs 写法
class Solution {public int minTravelTime(int l, int n, int k, int[] position, int[] time) {int[] pre = new int[n+1];for(int i = 0; i < n; i++){pre[i+1] = pre[i] + time[i];}memo = new int[n-1][n-1][k+1];return dfs(0, 0, k, pre, position);}int[][][] memo;int dfs(int i, int j, int leftK, int[] pre, int[] position){int n = position.length;if(i == n - 1) return leftK == 0 ? 0 : Integer.MAX_VALUE/2;if(memo[i][j][leftK] > 0) return memo[i][j][leftK];int t = pre[i+1] - pre[j];int res = Integer.MAX_VALUE / 2;for(int nxt = i + 1; nxt < Math.min(n, leftK + i + 2); nxt++){res = Math.min(res, dfs(nxt, i+1, leftK-(nxt-i-1), pre, position) + t * (position[nxt] - position[i]));}return memo[i][j][leftK] = res;}
}// 递推写法
class Solution {public int minTravelTime(int l, int n, int k, int[] position, int[] time) {int[] pre = new int[n+1];for(int i = 0; i < n; i++){pre[i+1] = pre[i] + time[i];}int[][][] f = new int[n][n][k+1];for(int[][] a : f){for(int[] b : a){Arrays.fill(b, Integer.MAX_VALUE / 2);}}for(int j = 0; j < n; j++){f[n-1][j][0] = 0;}for(int i = n - 2; i >= 0; i--){for(int j = 0; j <= i; j++){int t = pre[i + 1] - pre[j];for(int leftK = 0; leftK < k + 1; leftK++){for(int nxt = i + 1; nxt < Math.min(n, leftK + i + 2); nxt++){f[i][j][leftK] = Math.min(f[i][j][leftK], f[nxt][i+1][leftK-(nxt-i-1)] + t * (position[nxt] - position[i]));}}}}return f[0][0][k];}
}
四,3539. 魔法序列的数组乘积之和
题目列表
本题题意:从 nums
数组中选择 m
个数(可重复选),且要求 s u m ( 2 i ) sum(2^{i}) sum(2i)(i 表示nums的下标)二进制形式中1的个数为 k
,求所有可能组合的 nums[i]
的乘积之和。
- 假设
m = 10, nums = [2,9]
,如果选择了 4 个 2,6 个 9 这种选法合法,那么它有 10 ! 4 ! 6 ! \frac{10!}{4!6!} 4!6!10! 种排列组合,这种选择的贡献就是 10 ! 4 ! 6 ! ∗ n u m s [ 0 ] 4 ∗ n u m s [ 1 ] 6 \frac{10!}{4!6!}*nums[0]^4*nums[1]^6 4!6!10!∗nums[0]4∗nums[1]6,将它的形式稍作变换 10 ! ∗ n u m s [ 0 ] 4 4 ! ∗ n u m s [ 1 ] 6 6 ! 10!*\frac{nums[0]^4}{4!}*\frac{nums[1]^6}{6!} 10!∗4!nums[0]4∗6!nums[1]6,也就是说对于每个nums[i]
,可以将它单独拆分成 n u m s [ i ] j j ! \frac{nums[i]^j}{j!} j!nums[i]j 的形式来计算,最终再乘以m!
- 此时可以使用 d f s dfs dfs 来计算,它的参数一定包含
i: nums的下标
m: 剩余的选择次数
s: sum(2^i),用来计算二进制中1的个数是否为 k
,但是这么计算的话可能会超出long
的数据范围,所以不能将它作为参数- 从二进制的加法性质出发,可以发现,
低位的二进制先进行加法计算的话,它只会影响高位的二进制运算结果,对于低于它的二进制运算结果没有任何影响
。也就是说,我们只需要记录s>>i
就行,不过这样记录的话就还需要额外使用一个参数leftK
,来记录还需要有几个1
- 从二进制的加法性质出发,可以发现,
- 定义
dfs(i,m,s,leftK):
在上述情况下,剩余元素的贡献值 - 对于
nums[i]
来说,枚举选择j = 0,1,2,...,m
个nums[i]
,接下来问题变成了- 当前下标为
i+1
时 - 剩余可以选择的次数为
m - j
- ( S + j ∗ 2 i ) > > ( i + 1 ) = > ( s + j ) > > 1 (S + j * 2^i) >> (i+1) => (s + j) >> 1 (S+j∗2i)>>(i+1)=>(s+j)>>1,
S 表示 [0,i] 的总和
- 剩余二进制 1 的个数为
leftK - (s + j) & 1
- 即
dfs(i+1,m-j,(s+1)>>1,leftK-(s+j)&1)
- 当前下标为
- d f s ( i , m , s , l e f t K ) = ∑ 0 m d f s ( i + 1 , m − j , ( s + j ) > > 1 , l e f t K − ( s + j ) m o d 2 ) ∗ n u m s [ i ] j j ! dfs(i,m,s,leftK) = \sum_{0}^{m} dfs(i+1,m-j,(s+j)>>1,leftK-(s+j)mod2) * \frac{nums[i]^j}{j!} dfs(i,m,s,leftK)=∑0mdfs(i+1,m−j,(s+j)>>1,leftK−(s+j)mod2)∗j!nums[i]j
- 答案为 d f s ( 0 , m , 0 , k ) ∗ m ! dfs(0,m,0,k)*m! dfs(0,m,0,k)∗m!
代码如下:
class Solution {private static final int MOD = 1_000_000_007;private static final int MX = 31;private static final long[] fac = new long[MX]; // n!private static final long[] inv_f = new long[MX]; // 1/n!// 预处理static {fac[0] = 1;for(int i = 1; i < MX; i++){fac[i] = fac[i-1] * i % MOD;}// 1/n! * n = 1/(n-1)!inv_f[MX-1] = (int)pow(fac[MX-1], MOD-2);for(int i = MX - 2; i >= 0; i--){inv_f[i] = inv_f[i+1] * (i+1) % MOD;}}static long pow(long x, int y){long res = 1;while(y > 0){if(y % 2 == 1){res = res * x % MOD;}x = x * x % MOD;y /= 2;}return res;}public int magicalSum(int m, int k, int[] nums) {int n = nums.length;int[][] powV = new int[n][m+1];// nums[i]^jfor(int i = 0; i < n; i++){powV[i][0] = 1;for(int j = 1; j <= m; j++){powV[i][j] = (int)((long)powV[i][j-1] * nums[i] % MOD);}}memo = new long[n][m+1][k+1][m/2+1];for(long[][][] a : memo){for(long[][] b : a){for(long[] c : b){Arrays.fill(c, -1L);}}}return (int)(dfs(0, m, 0, k, powV) * fac[m] % MOD);}long[][][][] memo;long dfs(int i, int m, int s, int k, int[][] powV){int cnt = Integer.bitCount(s);if(cnt + m < k) return 0; // 之后没有 k 个 1,直接返回 0 if(i == powV.length) return m == 0 && cnt == k ? 1 : 0; // 选了 m 个数且 1 的个数为 k,返回 1;否则返回 0if(memo[i][m][k][s] != -1) return memo[i][m][k][s];long res = 0;for(int j = 0; j <= m; j++){int bit = (s + j) & 1; // 计算当前二进制位为0/1if(k >= bit)res = (res + dfs(i+1, m-j, (s+j)>>1, k-bit, powV) * powV[i][j] % MOD * inv_f[j] % MOD) % MOD;}return memo[i][m][k][s] = res;}
}