阅读视图
OpenAI与聚变初创公司Helion洽谈大型能源交易,奥尔特曼辞职避嫌
现货黄金突破4440美元/盎司
亚马逊:Leo卫星项目计划加快发射节奏
房利美与房地美响应特朗普2000亿美元指令,加大MBS购买力度
汇丰任命首位首席AI官
Apollo限制旗下私募信贷基金赎回,投资者的申请赎回比例超过11%
Alphabet无人机业务挺进密集城区测试
雅诗兰黛洽谈收购西班牙Puig,意欲打造美妆巨头
OpenAI在投资者文件中将对微软的依赖列为风险
道达尔计划将把海上风电资金用于开采美国石油和天然气
今年前两月长江经济带外贸进出口值同比增长19%
美联储4月维持利率不变的概率为92.8%
苹果将于6月召开年度全球开发者大会,预计发布一系列AI功能
丰田将投资10亿美元,扩建美国肯塔基、印第安纳州工厂产能
美股三大指数集体收涨,大型科技股普涨
每日一题-构造乘积矩阵🟡
给你一个下标从 0 开始、大小为 n * m 的二维整数矩阵 grid ,定义一个下标从 0 开始、大小为 n * m 的的二维矩阵 p。如果满足以下条件,则称 p 为 grid 的 乘积矩阵 :
- 对于每个元素
p[i][j],它的值等于除了grid[i][j]外所有元素的乘积。乘积对12345取余数。
返回 grid 的乘积矩阵。
示例 1:
输入:grid = [[1,2],[3,4]] 输出:[[24,12],[8,6]] 解释:p[0][0] = grid[0][1] * grid[1][0] * grid[1][1] = 2 * 3 * 4 = 24 p[0][1] = grid[0][0] * grid[1][0] * grid[1][1] = 1 * 3 * 4 = 12 p[1][0] = grid[0][0] * grid[0][1] * grid[1][1] = 1 * 2 * 4 = 8 p[1][1] = grid[0][0] * grid[0][1] * grid[1][0] = 1 * 2 * 3 = 6 所以答案是 [[24,12],[8,6]] 。
示例 2:
输入:grid = [[12345],[2],[1]] 输出:[[2],[0],[0]] 解释:p[0][0] = grid[0][1] * grid[0][2] = 2 * 1 = 2 p[0][1] = grid[0][0] * grid[0][2] = 12345 * 1 = 12345. 12345 % 12345 = 0 ,所以 p[0][1] = 0 p[0][2] = grid[0][0] * grid[0][1] = 12345 * 2 = 24690. 24690 % 12345 = 0 ,所以 p[0][2] = 0 所以答案是 [[2],[0],[0]] 。
提示:
1 <= n == grid.length <= 1051 <= m == grid[i].length <= 1052 <= n * m <= 1051 <= grid[i][j] <= 109
[Java]基于扩展欧几里得算法求逆元
Problem: 8026. 构造乘积矩阵
[TOC]
思路
本题最简单的解法是前缀后缀积的解法,因为已经有很多题解了,这里就不提了。比赛的时候我一直想基于扩展欧几里得算法求逆元来求解,但是细节没有处理好,比赛没有通过。赛后参考@雪景式大佬的逆元解法,才把细节处理好。
解题方法
由于本题要求每个位置除自己以外的元素乘积,很自然的会想到使用总乘积除以当前位置数字的解法。由于数字太大,需要取模,但是本题的模是12345,不是一个质数,我们在分母的数字是没有办法直接使用费马小定理来求逆元的,也就是$p=12345$时,$a^{p-2}\mod p$是无法求得$a$正确的逆元的。
对于$p=12345$,这种不是质数的,我们可以基于扩展欧几里得算法求逆元,由于扩展欧几里得算法需要互质,而$12345= 35823$, 我们需要对矩阵中是这三个质数倍数的数字做处理,将3,5,823的计数存下来,而对应的数字需要除掉所有的3,5,823的因数,再去做矩阵中所有数字的乘积,用于后面和逆元相乘,得到当前位置的结果。
接着就是如何使用扩展欧几里得算法得到当前矩阵位置数字除掉3,5,823后数值$a$的逆元了。
根据裴蜀定理,如果$a,m$互质,则有:$ax+my = 1$,此时$ax \equiv 1(\mod m)$, 也就是$x$就是$a$的逆元,我们可以辗转相除法求出$a,m$的最大公因数$gcd(a,m)$,在这个过程中对应的系数$x,y$就可以求出来,而$x$就是我们要的逆元。具体可以参考下面ex_gcd和inv两个函数。
当我们求出每个位置的逆元后,就可以求结果了:
- 对于除了当前位置以外,还有可以同时被3,5,823整除,即被12345整除的因数,直接返回结果0
- 对于除了当前位置以外,不能被12345整除的因数,则用总乘积乘以当前位置的逆元,还需要乘以剩下所有的3(若有),5(若有),823(若有),最后乘以剩下的3,5,823的幂的部分要使用快速幂。
复杂度
- 时间复杂度:$O(mn\log U)$,其中$\log U$为扩展欧几里得算法和快速幂的复杂度,这部分对数复杂度取决于矩阵中最大数字的大小,以及3,5,823的最大因数个数。
- 空间复杂度: $O(1)$
Code
###Java
class Solution {
int MOD = 12345; //12345 = 3*5*823
public int[][] constructProductMatrix(int[][] grid) {
int m = grid.length;
int n = grid[0].length;
int cnt3 = 0;
int cnt5 = 0;
int cnt823 = 0;
long multiAll = 1;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
int[] tmp = numMod(grid[i][j]);
cnt3 += tmp[0];
cnt5 += tmp[1];
cnt823 += tmp[2];
multiAll = multiAll * tmp[3] % MOD;
}
}
int[][] res = new int[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
int[] tmp = numMod(grid[i][j]);
if(cnt3-tmp[0] > 0 && cnt5-tmp[1] > 0 && cnt823-tmp[2] > 0) res[i][j] = 0;
else{
long curRes = multiAll*inv(tmp[3], MOD)%MOD;
curRes = curRes*pow(3, cnt3-tmp[0])%MOD;
curRes = curRes*pow(5, cnt5-tmp[1])%MOD;
curRes = curRes*pow(823, cnt823-tmp[2])%MOD;
res[i][j] = (int)curRes;
}
}
}
return res;
}
//计算a^b%MOD
private long pow(int a, int b){
long res = 1;
long a1 = a;
while(b > 0){
if((b&1) > 0) res = (res*a1)%MOD;
a1 = (a1*a1)%MOD;
b = b/2;
}
return res;
}
public int[] numMod(int num){
int cnt3Cur = 0;
int cnt5Cur = 0;
int cnt823Cur = 0;
while(num%3 == 0){
num /= 3;
cnt3Cur++;
}
while(num%5 == 0){
num /= 5;
cnt5Cur++;
}
while(num%823 == 0){
num /= 823;
cnt823Cur++;
}
return new int[]{cnt3Cur, cnt5Cur, cnt823Cur, num};
}
//扩展欧几里得求逆元
public int[] ex_gcd(int a, int b){
if(b == 0) return new int[]{a,1,0};
else {
int[] res = ex_gcd(b, a%b);
return new int[]{res[0], res[2], res[1]-(a/b)*res[2]};
}
}
//保证a、m互质的情况下才可以使用
public int inv(int a, int m){
int[] res = ex_gcd(a, m);
if(res[0] != 1) return -1;
return (res[1]+m)%m;
}
}
前后缀分解(Python/Java/C++/C/Go/JS/Rust)
请先完成本题的一维版本:238. 除了自身以外数组的乘积。
把矩阵平铺成一维数组,就是 238 题了。我们需要算出每个数左边所有数的乘积,以及右边所有数的乘积。
先算出从 $\textit{grid}[i][j]$ 的下一个元素开始,到最后一个元素 $\textit{grid}[n-1][m-1]$ 的乘积,记作 $\textit{suf}[i][j]$。这可以从最后一个数 $\textit{grid}[n-1][m-1]$ 开始,倒着遍历 $\textit{grid}$ 得到。
然后算出从第一个数 $\textit{grid}[0][0]$ 开始,到 $\textit{grid}[i][j]$ 的上一个元素的乘积,记作 $\textit{pre}[i][j]$。这可以从第一行第一列开始,正着遍历得到。
那么
$$
p[i][j] = \textit{pre}[i][j]\cdot \textit{suf}[i][j]
$$
代码实现时,可以先初始化 $p[i][j]=\textit{suf}[i][j]$,然后在计算 $\textit{pre}[i][j]$ 的过程中,把 $\textit{pre}[i][j]$ 乘到 $\textit{p}[i][j]$ 中,就得到了最终答案。这样写的话,$\textit{pre}$ 和 $\textit{suf}$ 可以直接用单个变量表示,无需创建数组。
代码实现时,注意取模。为什么可以在中途取模?原理见 模运算的世界:当加减乘除遇上取模。
class Solution:
def constructProductMatrix(self, grid: List[List[int]]) -> List[List[int]]:
MOD = 12345
n, m = len(grid), len(grid[0])
p = [[0] * m for _ in range(n)]
suf = 1 # 后缀乘积
for i in range(n - 1, -1, -1):
for j in range(m - 1, -1, -1):
p[i][j] = suf # p[i][j] 先初始化成后缀乘积
suf = suf * grid[i][j] % MOD
pre = 1 # 前缀乘积
for i, row in enumerate(grid):
for j, x in enumerate(row):
p[i][j] = p[i][j] * pre % MOD # 乘上前缀乘积
pre = pre * x % MOD
return p
class Solution {
public int[][] constructProductMatrix(int[][] grid) {
final int MOD = 12345;
int n = grid.length;
int m = grid[0].length;
int[][] p = new int[n][m];
long suf = 1; // 后缀乘积
for (int i = n - 1; i >= 0; i--) {
for (int j = m - 1; j >= 0; j--) {
p[i][j] = (int) suf; // p[i][j] 先初始化成后缀乘积
suf = suf * grid[i][j] % MOD;
}
}
long pre = 1; // 前缀乘积
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
p[i][j] = (int) (p[i][j] * pre % MOD); // 乘上前缀乘积
pre = pre * grid[i][j] % MOD;
}
}
return p;
}
}
class Solution {
public:
vector<vector<int>> constructProductMatrix(vector<vector<int>>& grid) {
constexpr int MOD = 12345;
int n = grid.size(), m = grid[0].size();
vector p(n, vector<int>(m));
long long suf = 1; // 后缀乘积
for (int i = n - 1; i >= 0; i--) {
for (int j = m - 1; j >= 0; j--) {
p[i][j] = suf; // p[i][j] 先初始化成后缀乘积
suf = suf * grid[i][j] % MOD;
}
}
long long pre = 1; // 前缀乘积
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
p[i][j] = p[i][j] * pre % MOD; // 乘上前缀乘积
pre = pre * grid[i][j] % MOD;
}
}
return p;
}
};
int** constructProductMatrix(int** grid, int gridSize, int* gridColSize, int* returnSize, int** returnColumnSizes) {
const int MOD = 12345;
int n = gridSize, m = gridColSize[0];
int** p = malloc(n * sizeof(int*));
*returnSize = n;
*returnColumnSizes = malloc(n * sizeof(int));
for (int i = 0; i < n; i++) {
p[i] = malloc(m * sizeof(int));
(*returnColumnSizes)[i] = m;
}
long long suf = 1; // 后缀乘积
for (int i = n - 1; i >= 0; i--) {
for (int j = m - 1; j >= 0; j--) {
p[i][j] = suf; // p[i][j] 先初始化成后缀乘积
suf = suf * grid[i][j] % MOD;
}
}
long long pre = 1; // 前缀乘积
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
p[i][j] = p[i][j] * pre % MOD; // 乘上前缀乘积
pre = pre * grid[i][j] % MOD;
}
}
return p;
}
func constructProductMatrix(grid [][]int) [][]int {
const mod = 12345
n, m := len(grid), len(grid[0])
p := make([][]int, n)
suf := 1 // 后缀乘积
for i := n - 1; i >= 0; i-- {
p[i] = make([]int, m)
for j := m - 1; j >= 0; j-- {
p[i][j] = suf // p[i][j] 先初始化成后缀乘积
suf = suf * grid[i][j] % mod
}
}
pre := 1 // 前缀乘积
for i, row := range grid {
for j, x := range row {
p[i][j] = p[i][j] * pre % mod // 乘上前缀乘积
pre = pre * x % mod
}
}
return p
}
var constructProductMatrix = function(grid) {
const MOD = 12345;
const n = grid.length, m = grid[0].length;
const p = Array.from({ length: n }, () => Array(m).fill(0));
let suf = 1; // 后缀乘积
for (let i = n - 1; i >= 0; i--) {
for (let j = m - 1; j >= 0; j--) {
p[i][j] = suf; // p[i][j] 先初始化成后缀乘积
suf = suf * grid[i][j] % MOD;
}
}
let pre = 1; // 前缀乘积
for (let i = 0; i < n; i++) {
for (let j = 0; j < m; j++) {
p[i][j] = p[i][j] * pre % MOD; // 乘上前缀乘积
pre = pre * grid[i][j] % MOD;
}
}
return p;
};
impl Solution {
pub fn construct_product_matrix(grid: Vec<Vec<i32>>) -> Vec<Vec<i32>> {
const MOD: i64 = 12345;
let n = grid.len();
let m = grid[0].len();
let mut p = vec![vec![0; m]; n];
let mut suf = 1; // 后缀乘积
for i in (0..n).rev() {
for j in (0..m).rev() {
p[i][j] = suf as i32; // p[i][j] 先初始化成后缀乘积
suf = suf * grid[i][j] as i64 % MOD;
}
}
let mut pre = 1; // 前缀乘积
for i in 0..n {
for j in 0..m {
p[i][j] = (p[i][j] as i64 * pre % MOD) as i32; // 乘上前缀乘积
pre = pre * grid[i][j] as i64 % MOD;
}
}
p
}
}
复杂度分析
- 时间复杂度:$\mathcal{O}(nm)$,其中 $n$ 和 $m$ 分别是 $\textit{grid}$ 的行数和列数。
- 空间复杂度:$\mathcal{O}(1)$。返回值不计入。
专题训练
见下面动态规划题单的「专题:前后缀分解」。
分类题单
- 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
- 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
- 单调栈(基础/矩形面积/贡献法/最小字典序)
- 网格图(DFS/BFS/综合应用)
- 位运算(基础/性质/拆位/试填/恒等式/思维)
- 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
- 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
- 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
- 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
- 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
- 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
- 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)
欢迎关注 B站@灵茶山艾府