Tuyển sinh 🎓 Mình nhận đào tạo lập trình giải thuật thi đấu: thi vào lớp 10 chuyên tin · thi học sinh giỏi tin học cấp tỉnh/thành · thi vào đội tuyển quốc gia · Olympic tin học sinh viên / ICPC · phỏng vấn thuật toán. Bấm vào đây để xem thông tin khóa học → Xem khóa học
Thi thử miễn phí
🏆 Thi thử HSG Tin học tuần này trên ClueOJ
Mở công khai cho mọi người · đề do thầy Nguyễn Việt Nam Sơn biên soạn
Lịch thi, đề cũ & lời giải →

Maximum Non Negative Product in a Matrix (LeetCode 1594)

Maximum Non Negative Product in a Matrix (LeetCode 1594)
Nguồn: LeetCode 1594 Độ khó: Trung bình Quy hoạch động QHĐ trên lưới Ma trận Modulo

Đề bài

LeetCode 1594 – Maximum Non Negative Product in a Matrix (Tích không âm lớn nhất trong ma trận) – độ khó Medium.

Cho ma trận grid kích thước rows x cols. Ban đầu bạn đứng ở góc trên bên trái $(0, 0)$, và ở mỗi bước bạn chỉ được đi sang phải hoặc đi xuống trong ma trận.

Trong tất cả các đường đi bắt đầu từ góc trên bên trái $(0, 0)$ và kết thúc ở góc dưới bên phải $(rows - 1, cols - 1)$, hãy tìm đường đi có tích không âm lớn nhất. Tích của một đường đi là tích của tất cả các số nguyên trong các ô mà đường đi đi qua.

Trả về tích không âm lớn nhất modulo $10^9 + 7$. Nếu tích lớn nhất là số âm thì trả về -1.

Lưu ý rằng phép lấy modulo được thực hiện sau khi đã tìm được tích lớn nhất.

Ràng buộc:

  • $1 \le rows, cols \le 15$
  • $-4 \le$ grid[i][j] $\le 4$

Các ví dụ (ô tô xanh là đường đi cho tích không âm lớn nhất – trong đề gốc là các số in đậm):

Ví dụ 1:

-1-2-3-2-3-3-3-3-2
Input: grid = [[-1,-2,-3],[-2,-3,-3],[-3,-3,-2]]
Output: -1

Giải thích: không thể có tích không âm trên đường đi từ $(0, 0)$ đến $(2, 2)$, nên trả về $-1$.

Ví dụ 2:

1-211-213-41
Input: grid = [[1,-2,1],[1,-2,1],[3,-4,1]]
Output: 8

Giải thích: tích không âm lớn nhất là đường được tô ($1 \times 1 \times (-2) \times (-4) \times 1 = 8$).

Ví dụ 3:

130-4
Input: grid = [[1,3],[0,-4]]
Output: 0

Giải thích: tích không âm lớn nhất là đường được tô ($1 \times 0 \times (-4) = 0$).

Ví dụ 4:

1440-20011-111
Input: grid = [[1,4,4,0],[-2,0,0,1],[1,-1,1,1]]
Output: 2

Giải thích: tích không âm lớn nhất là đường được tô ($1 \times (-2) \times 1 \times (-1) \times 1 \times 1 = 2$).

1594. Maximum Non Negative Product in a Matrix – Medium

You are given a rows x cols matrix grid. Initially, you are located at the top-left corner (0, 0), and in each step, you can only move right or down in the matrix.

Among all possible paths starting from the top-left corner (0, 0) and ending in the bottom-right corner (rows - 1, cols - 1), find the path with the maximum non-negative product. The product of a path is the product of all integers in the grid cells visited along the path.

Return the maximum non-negative product modulo $10^9 + 7$. If the maximum product is negative return -1.

Notice that the modulo is performed after getting the maximum product.

Example 1:

Input: grid = [[-1,-2,-3],
               [-2,-3,-3],
               [-3,-3,-2]]
Output: -1
Explanation: It's not possible to get non-negative product in the path from (0, 0) to (2, 2), so return -1.

Example 2:

Input: grid = [[1,-2,1],
               [1,-2,1],
               [3,-4,1]]
Output: 8
Explanation: Maximum non-negative product is in bold (1 * 1 * -2 * -4 * 1 = 8).

Example 3:

Input: grid = [[1, 3],
               [0,-4]]
Output: 0
Explanation: Maximum non-negative product is in bold (1 * 0 * -4 = 0).

Example 4:

Input: grid = [[ 1, 4,4,0],
               [-2, 0,0,1],
               [ 1,-1,1,1]]
Output: 2
Explanation: Maximum non-negative product is in bold (1 * -2 * 1 * -1 * 1 * 1 = 2).

Constraints:

  • 1 <= rows, cols <= 15
  • -4 <= grid[i][j] <= 4

Lời mở đầu

Bài này mình được một bạn học viên hỏi nên sẵn tiện mình viết bài giải chi tiết để nhiều bạn được tham khảo luôn. Hy vọng mang đến được giá trị cho các bạn 😇.

Về dữ liệu vào / ra của code trong bài
Trên LeetCode các bạn chỉ cần nộp phần class Solution. Để các bạn chạy thử được trên máy (hoặc trang Chạy code online), mọi code trong bài đều có thêm hàm main đọc từ bàn phím theo định dạng: dòng đầu là hai số $rows$ và $cols$, tiếp theo là $rows$ dòng, mỗi dòng $cols$ số của grid; in ra một số là đáp án. Ví dụ 2 của đề viết thành:
3 3
1 -2 1
1 -2 1
3 -4 1
và chương trình in ra 8.

Tóm tắt đề bài: bạn được cho một ma trận kích thước $N \times M$, ô ở hàng $i$ và cột $j$ gọi là ô $(i, j)$ và có giá trị là grid[i][j]. Bạn hiện tại đang đứng tại ô $(0, 0)$, mỗi lần di chuyển, bạn chỉ được phép di chuyển sang phải hoặc xuống dưới. Hãy làm sao để có thể đến được ô $(N - 1, M - 1)$ sao cho tích của các số trên đường đi là lớn nhất và phải là một số nguyên không âm. Nếu như không tồn tại một đường đi có tích là số nguyên không âm thì trả về $-1$, ngược lại thì trả về tích có giá trị lớn nhất. Vì tích khá lớn nên ta đưa ra kết quả mod cho $10^9 + 7$.

Ví dụ 2 cho dễ hình dung: đường $1 \to 1 \to -2 \to -4 \to 1$ có hai số âm nên tích dương $= 8$. Còn đường đi thẳng hàng trên $1 \to -2 \to 1 \to 1 \to 1$ chỉ có một số âm nên tích $= -2$ (âm, không tính). Ví dụ 1 thì đường nào cũng đi qua đúng 5 ô toàn số âm ⇒ tích luôn âm ⇒ đáp án $-1$.

Link nộp bài

Bấm vào nút bên dưới để nộp bài và được hệ thống CLUE OJ chấm tự động.

Nộp bài trên CLUE OJ

Lộ trình lời giải, bấm vào từng phần để nhảy tới:

Cách 1Vét cạn mọi đường đi – O(C(n+m−2, n−1))

Cách tự nhiên nhất: đệ quy từ ô $(0, 0)$, mỗi bước thử đi xuống hoặc sang phải, mang theo tích hiện tại. Tới ô cuối thì cập nhật tích lớn nhất. Cuối cùng nếu tích lớn nhất âm thì trả về $-1$, ngược lại trả về tích đó mod $10^9 + 7$.

Số đường đi là $C(n + m - 2, n - 1)$; với $n = m = 15$ là $C(28, 14) \approx 4 \cdot 10^7$ đường. Cách này dùng để kiểm tra kết quả cho cách 2, và giúp các bạn thấy rõ vì sao cần QHĐ.

#include <bits/stdc++.h>
using namespace std;

class Solution {
public:
    int n, m;
    long long best;
    void go(vector<vector<int>>& a, int i, int j, long long prod) {
        prod *= a[i][j];
        if (i == n - 1 && j == m - 1) {          // tới ô cuối: cập nhật kết quả
            best = max(best, prod);
            return;
        }
        if (i + 1 < n) go(a, i + 1, j, prod);    // đi xuống
        if (j + 1 < m) go(a, i, j + 1, prod);    // sang phải
    }
    int maxProductPath(vector<vector<int>>& a) {
        n = (int) a.size();
        m = (int) a[0].size();
        best = LLONG_MIN;
        go(a, 0, 0, 1);
        if (best < 0) return -1;
        return best % 1000000007;
    }
};

int main() {
    int T;
    cin >> T;
    while (T--) {
        int n, m;
        cin >> n >> m;
        vector<vector<int>> a(n, vector<int>(m));
        for (int i = 0; i < n; i++)
            for (int j = 0; j < m; j++) cin >> a[i][j];
        Solution sol;
        cout << sol.maxProductPath(a) << "\n";
    }
    return 0;
}
import sys
from typing import List

sys.setrecursionlimit(10000)

class Solution:
    def maxProductPath(self, a: List[List[int]]) -> int:
        n, m = len(a), len(a[0])
        best = None

        def go(i, j, prod):
            nonlocal best
            prod *= a[i][j]
            if i == n - 1 and j == m - 1:      # tới ô cuối: cập nhật kết quả
                if best is None or prod > best:
                    best = prod
                return
            if i + 1 < n:
                go(i + 1, j, prod)             # đi xuống
            if j + 1 < m:
                go(i, j + 1, prod)             # sang phải

        go(0, 0, 1)
        if best < 0:
            return -1
        return best % 1000000007

def main():
    data = sys.stdin.read().split()
    t = int(data[0])
    p = 1
    for _ in range(t):
        n, m = int(data[p]), int(data[p + 1])
        a = [[int(data[p + 2 + i * m + j]) for j in range(m)] for i in range(n)]
        p += 2 + n * m
        print(Solution().maxProductPath(a))

main()
Không gian$O(n + m)$ (độ sâu đệ quy)
Thời gian$O\big(C(n+m-2, n-1)\big)$
Chạy được nhưng rất "hên xui"
Trên máy mình, bản C++ với lưới $15 \times 15$ chạy khoảng 0.3 giây (vì mỗi đường đi chỉ là vài phép nhân), nhưng bản Python với lưới $13 \times 13$ đã mất gần 2 giây, lưới $15 \times 15$ thì chắc chắn TLE. Chỉ cần giới hạn tăng lên một chút là số đường đi bùng nổ ngay, nên ta cần một cách làm "chuẩn" hơn.

Cách 2Quy hoạch động lưu 3 trạng thái – O(N·M)

Vì sao chỉ lưu tích lớn nhất là không đủ?

Bài quen thuộc "đường đi có tổng lớn nhất" thì mỗi ô chỉ cần lưu tổng lớn nhất. Nhưng với tích thì có số âm: một tích âm rất bé (ví dụ $-32$) nhân với một số âm ($-4$) lại thành tích dương rất lớn ($128$). Nên ở mỗi ô ta phải lưu thêm cả tích âm bé nhất, và tách riêng trường hợp tích bằng 0.

Định nghĩa trạng thái

Gọi dp[i][j][type] là tích của đường đi từ ô $(0, 0)$ đến ô $(i, j)$, trong đó type thể hiện trạng thái đường đi khi đến ô $(i, j)$ hiện tại:

  • type = 0: tích các số trên đường đi là số âm, dp[i][j][0] lưu tích bé nhất. Ta cần tích âm bé nhất để khi gặp số âm, chuyển sang số dương, ta được tích lớn nhất.
  • type = 1: tích các số trên đường đi là số dương, dp[i][j][1] lưu tích lớn nhất.
  • type = 2: dp[i][j][2] = 0 nếu có đường từ $(0, 0)$ đến $(i, j)$ đi qua số 0 (tích bằng 0).

Giá trị "chưa có" (hàm nonValue(type) trong code): dp[i][j][0] = +∞, dp[i][j][1] = −∞, dp[i][j][2] = −1. Khởi tạo ô đầu: nếu $a[0][0] \lt 0$ thì dp[0][0][0] = a[0][0]; nếu $a[0][0] = 0$ thì dp[0][0][2] = 0; nếu $a[0][0] \gt 0$ thì dp[0][0][1] = a[0][0].

Chuyển trạng thái (kiểu "đẩy" sang ô kế tiếp)

Với mỗi dp[i][j][type] đã có giá trị, ta có 2 tình huống là đến $(i + 1, j)$ hoặc $(i, j + 1)$. Giả sử đến ô $(i + 1, j)$ có giá trị $x = a[i + 1][j]$, ta xét $x$ là 0, âm hay dương:

  • $x = 0$ (hoặc type = 2, tức đường đã có số 0): tích bằng 0 ⇒ dp[i+1][j][2] = 0.
  • $x \lt 0$: đổi dấu. Tích âm (type 0) thành tích dương ⇒ dp[i+1][j][1] = max(dp[i+1][j][1], dp[i][j][0] * x); tích dương (type 1) thành tích âm ⇒ dp[i+1][j][0] = min(dp[i+1][j][0], dp[i][j][1] * x).
  • $x \gt 0$: giữ dấu. Type 0 ⇒ dp[i+1][j][0] = min(..., dp[i][j][0] * x); type 1 ⇒ dp[i+1][j][1] = max(..., dp[i][j][1] * x).

Ta thực hiện tương tự với ô $(i, j + 1)$.

Vì sao âm bé nhất × âm lại cho dương lớn nhất?
Với $x \lt 0$: $p \cdot x$ càng lớn khi $p$ càng bé. Nên trong các tích âm đến được $(i, j)$, chỉ tích bé nhất mới có ích khi đi tiếp qua một số âm. Tương tự, với $x \gt 0$ thì tích dương lớn nhất cho tích dương lớn nhất, tích âm bé nhất cho tích âm bé nhất. Vậy 3 giá trị ở mỗi ô là đủ thông tin.

Kết quả: nếu dp[n-1][m-1][1] có giá trị (có đường tích dương) thì trả về nó mod $10^9 + 7$; ngược lại nếu dp[n-1][m-1][2] có giá trị (có đường tích 0) thì trả về $0$; còn lại trả về $-1$ vì không tồn tại đường có tích bằng 0 hoặc tích dương.

Em nhập lưới (các hàng cách nhau bởi dấu ;) rồi bấm Tiếp để xem từng ô "đẩy" giá trị sang ô bên dưới và bên phải. Trong mỗi ô: số lớn là $a[i][j]$, dòng − là tích âm bé nhất, + là tích dương lớn nhất, 0 là có đường tích 0:

Ví dụ nhanh:

Code dưới đây của mình đã AC với tư tưởng đã trình bày ở trên, anh em có thể tham khảo nhé:

#include <bits/stdc++.h>
using namespace std;

const long long MOD = 1000000007;
const long long inf = 100000000000000007LL;
long long dp[16][16][3];

class Solution {
public:
    long long nonValue(int type) {
        if (type == 0) {
            return inf;
        }
        else if (type == 1) {
            return -inf;
        }
        else {
            return -1;
        }
    }
    int maxProductPath(vector<vector<int>>& a) {
        int n = (int) a.size();
        int m = (int) a[0].size();
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                for (int type = 0; type <= 2; type++) {
                    dp[i][j][type] = nonValue(type);
                }
            }
        }
        if (a[0][0] < 0) {
            dp[0][0][0] = a[0][0];
        }
        else if (a[0][0] == 0) {
            dp[0][0][2] = 0;
        }
        else {
            dp[0][0][1] = a[0][0];
        }
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                for (int type = 0; type <= 2; type++) {
                    if (dp[i][j][type] == nonValue(type)) {
                        continue;
                    }
                    if (i < n - 1) {
                        if (a[i + 1][j] == 0) {
                            dp[i + 1][j][2] = 0;
                        }
                        else if (a[i + 1][j] < 0) {
                            if (type == 0) {
                                dp[i + 1][j][1] = max(dp[i + 1][j][1], dp[i][j][type] * a[i + 1][j]);
                            }
                            else if (type == 1) {
                                dp[i + 1][j][0] = min(dp[i + 1][j][0], dp[i][j][type] * a[i + 1][j]);
                            }
                            else {
                                dp[i + 1][j][2] = 0;
                            }
                        }
                        else {
                            if (type == 0) {
                                dp[i + 1][j][0] = min(dp[i + 1][j][0], dp[i][j][type] * a[i + 1][j]);
                            }
                            else if (type == 1) {
                                dp[i + 1][j][1] = max(dp[i + 1][j][1], dp[i][j][type] * a[i + 1][j]);
                            }
                            else {
                                dp[i + 1][j][2] = 0;
                            }
                        }
                    }
                    if (j < m - 1) {
                        if (a[i][j + 1] == 0) {
                            dp[i][j + 1][2] = 0;
                        }
                        else if (a[i][j + 1] < 0) {
                            if (type == 0) {
                                dp[i][j + 1][1] = max(dp[i][j + 1][1], dp[i][j][type] * a[i][j + 1]);
                            }
                            else if (type == 1) {
                                dp[i][j + 1][0] = min(dp[i][j + 1][0], dp[i][j][type] * a[i][j + 1]);
                            }
                            else {
                                dp[i][j + 1][2] = 0;
                            }
                        }
                        else {
                            if (type == 0) {
                                dp[i][j + 1][0] = min(dp[i][j + 1][0], dp[i][j][type] * a[i][j + 1]);
                            }
                            else if (type == 1) {
                                dp[i][j + 1][1] = max(dp[i][j + 1][1], dp[i][j][type] * a[i][j + 1]);
                            }
                            else {
                                dp[i][j + 1][2] = 0;
                            }
                        }
                    }
                }
            }
        }
        if (dp[n - 1][m - 1][1] != -inf) {
            return dp[n - 1][m - 1][1] % MOD;
        }
        if (dp[n - 1][m - 1][2] != -1) {
            return 0;
        }
        return -1;
    }
};

int main() {
    int T;
    cin >> T;
    while (T--) {
        int n, m;
        cin >> n >> m;
        vector<vector<int>> a(n, vector<int>(m));
        for (int i = 0; i < n; i++)
            for (int j = 0; j < m; j++) cin >> a[i][j];
        Solution sol;
        cout << sol.maxProductPath(a) << "\n";
    }
    return 0;
}
import sys
from typing import List

MOD = 1000000007
INF = 10 ** 18

class Solution:
    # type 0: tích âm (lưu tích bé nhất), type 1: tích dương (lưu tích lớn nhất),
    # type 2: đường đi có số 0 (tích = 0)
    def nonValue(self, t: int) -> int:
        if t == 0:
            return INF
        elif t == 1:
            return -INF
        return -1

    def maxProductPath(self, a: List[List[int]]) -> int:
        n, m = len(a), len(a[0])
        dp = [[[self.nonValue(t) for t in range(3)] for _ in range(m)] for _ in range(n)]
        if a[0][0] < 0:
            dp[0][0][0] = a[0][0]
        elif a[0][0] == 0:
            dp[0][0][2] = 0
        else:
            dp[0][0][1] = a[0][0]
        for i in range(n):
            for j in range(m):
                for t in range(3):
                    if dp[i][j][t] == self.nonValue(t):
                        continue
                    for x, y in ((i + 1, j), (i, j + 1)):   # xuống dưới, sang phải
                        if x >= n or y >= m:
                            continue
                        v = a[x][y]
                        if v == 0 or t == 2:
                            dp[x][y][2] = 0
                        elif v < 0:
                            if t == 0:
                                dp[x][y][1] = max(dp[x][y][1], dp[i][j][t] * v)
                            else:
                                dp[x][y][0] = min(dp[x][y][0], dp[i][j][t] * v)
                        else:
                            if t == 0:
                                dp[x][y][0] = min(dp[x][y][0], dp[i][j][t] * v)
                            else:
                                dp[x][y][1] = max(dp[x][y][1], dp[i][j][t] * v)
        if dp[n - 1][m - 1][1] != -INF:
            return dp[n - 1][m - 1][1] % MOD
        if dp[n - 1][m - 1][2] != -1:
            return 0
        return -1

def main():
    data = sys.stdin.read().split()
    t = int(data[0])
    p = 1
    for _ in range(t):
        n, m = int(data[p]), int(data[p + 1])
        a = [[int(data[p + 2 + i * m + j]) for j in range(m)] for i in range(n)]
        p += 2 + n * m
        print(Solution().maxProductPath(a))

main()
Bản Python gọn hơn một chút
Bản Python giữ nguyên ý tưởng nhưng gom 2 hướng đi (xuống, sang phải) vào một vòng lặp for x, y in ((i + 1, j), (i, j + 1)) và gộp trường hợp "ô kế là 0" với "đường đã có 0" làm một, nên ngắn hơn bản C++. Số nguyên Python không giới hạn nên không lo tràn số.

Em bấm Tiếp trong khung chạy tay dưới đây để xem từng dòng code C++ chạy. Khung "Dữ liệu" vẽ lưới $a$ và 3 bảng dp[.][.][0] (tích âm bé nhất), dp[.][.][1] (tích dương lớn nhất), dp[.][.][2] (có đường tích 0); ô "·" là chưa có giá trị. Để khung chạy tay không quá dài, phần khởi tạo bằng nonValue được gom mỗi ô một bước:

C++▶ Chạy tay từng dòng codeCách 2: QHĐ dp[i][j][0..2] = tích âm bé nhất / tích dương lớn nhất / có số 0.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4const long long MOD = 1000000007;
5const long long inf = 100000000000000007LL;
6long long dp[16][16][3];
7
8class Solution {
9public:
10 long long nonValue(int type) {
11 if (type == 0) {
12 return inf;
13 }
14 else if (type == 1) {
15 return -inf;
16 }
17 else {
18 return -1;
19 }
20 }
21 int maxProductPath(vector<vector<int>>& a) {
22 int n = (int) a.size();
23 int m = (int) a[0].size();
24 for (int i = 0; i < n; i++) {
25 for (int j = 0; j < m; j++) {
26 for (int type = 0; type <= 2; type++) {
27 dp[i][j][type] = nonValue(type);
28 }
29 }
30 }
31 if (a[0][0] < 0) {
32 dp[0][0][0] = a[0][0];
33 }
34 else if (a[0][0] == 0) {
35 dp[0][0][2] = 0;
36 }
37 else {
38 dp[0][0][1] = a[0][0];
39 }
40 for (int i = 0; i < n; i++) {
41 for (int j = 0; j < m; j++) {
42 for (int type = 0; type <= 2; type++) {
43 if (dp[i][j][type] == nonValue(type)) {
44 continue;
45 }
46 if (i < n - 1) {
47 if (a[i + 1][j] == 0) {
48 dp[i + 1][j][2] = 0;
49 }
50 else if (a[i + 1][j] < 0) {
51 if (type == 0) {
52 dp[i + 1][j][1] = max(dp[i + 1][j][1], dp[i][j][type] * a[i + 1][j]);
53 }
54 else if (type == 1) {
55 dp[i + 1][j][0] = min(dp[i + 1][j][0], dp[i][j][type] * a[i + 1][j]);
56 }
57 else {
58 dp[i + 1][j][2] = 0;
59 }
60 }
61 else {
62 if (type == 0) {
63 dp[i + 1][j][0] = min(dp[i + 1][j][0], dp[i][j][type] * a[i + 1][j]);
64 }
65 else if (type == 1) {
66 dp[i + 1][j][1] = max(dp[i + 1][j][1], dp[i][j][type] * a[i + 1][j]);
67 }
68 else {
69 dp[i + 1][j][2] = 0;
70 }
71 }
72 }
73 if (j < m - 1) {
74 if (a[i][j + 1] == 0) {
75 dp[i][j + 1][2] = 0;
76 }
77 else if (a[i][j + 1] < 0) {
78 if (type == 0) {
79 dp[i][j + 1][1] = max(dp[i][j + 1][1], dp[i][j][type] * a[i][j + 1]);
80 }
81 else if (type == 1) {
82 dp[i][j + 1][0] = min(dp[i][j + 1][0], dp[i][j][type] * a[i][j + 1]);
83 }
84 else {
85 dp[i][j + 1][2] = 0;
86 }
87 }
88 else {
89 if (type == 0) {
90 dp[i][j + 1][0] = min(dp[i][j + 1][0], dp[i][j][type] * a[i][j + 1]);
91 }
92 else if (type == 1) {
93 dp[i][j + 1][1] = max(dp[i][j + 1][1], dp[i][j][type] * a[i][j + 1]);
94 }
95 else {
96 dp[i][j + 1][2] = 0;
97 }
98 }
99 }
100 }
101 }
102 }
103 if (dp[n - 1][m - 1][1] != -inf) {
104 return dp[n - 1][m - 1][1] % MOD;
105 }
106 if (dp[n - 1][m - 1][2] != -1) {
107 return 0;
108 }
109 return -1;
110 }
111};
112
113int main() {
114 int n, m;
115 cin >> n >> m;
116 vector<vector<int>> a(n, vector<int>(m));
117 for (int i = 0; i < n; i++)
118 for (int j = 0; j < m; j++) cin >> a[i][j];
119 Solution sol;
120 cout << sol.maxProductPath(a);
121 return 0;
122}

Đánh giá độ phức tạp của cách làm này:

  • Không gian: mảng dp có $N \cdot M \cdot 3$ phần tử ⇒ $O(N \cdot M)$.
  • Thời gian: mỗi ô, mỗi trạng thái đẩy sang tối đa 2 ô kế tiếp ⇒ $O(N \cdot M)$.
Không gian$O(N \cdot M)$
Thời gian$O(N \cdot M)$
Lấy modulo SAU khi đã có tích lớn nhất
Đề nhấn mạnh phép mod thực hiện sau khi tìm được tích lớn nhất, nên trong lúc QHĐ ta phải so sánh tích thật, không được mod giữa chừng (mod rồi thì số lớn có thể thành số bé, so sánh sai). May là đường đi dài nhất có $15 + 15 - 1 = 29$ ô, $|$tích$| \le 4^{29} \approx 2{,}9 \cdot 10^{17}$, vẫn nằm gọn trong long long (khoảng $9{,}2 \cdot 10^{18}$).
Về hằng số inf trong code
inf = 10^17 + 7 ở đây chỉ đóng vai trò "dấu hiệu chưa có giá trị" (so sánh bằng với nonValue(type)). Tích thật có thể vượt $10^{17}$ (tới $4^{29}$) nhưng code vẫn đúng, vì tích luôn có dạng $\pm 2^x 3^y$ nên không bao giờ trùng đúng giá trị $\pm(10^{17} + 7)$, còn phép max/min với giá trị khởi tạo vẫn chọn đúng tích thật. Bạn nào muốn chắc ăn hơn có thể đặt inf = 4e18.
Ghi chú của mình: code C++ và Python của cả 2 cách đã được kiểm tra với 4 ví dụ của đề và 400 bộ test ngẫu nhiên kích thước tới $6 \times 6$ (so với code vét cạn) – khớp hết; thêm 200 bộ test ngẫu nhiên tới $15 \times 15$ so cách 2 bản C++ với bản Python (số nguyên không giới hạn, không lo tràn) – khớp hết, kể cả lưới toàn số $4$ (tích $4^{29}$). Cách 2 chạy tức thì; cách 1 bản C++ với lưới $15 \times 15$ mất khoảng 0.3 giây, bản Python với lưới $13 \times 13$ mất khoảng 1.8 giây.
Rất vui khi anh em nào đã đọc đến những dòng cuối cùng này. Anh em đọc chắc cũng mỏi mắt luôn chứ 😅 vậy anh em nghĩ người viết ra hết đống ở trên còn thế nào nữa 🤪. Vậy nên đừng tiếc gì cho mình một lời bình luận hoặc chia sẻ bài viết để động viên mình nè. Chúc anh em học tốt và gặt hái nhiều thành công 😇
Tự học thuật toán: khó ở đâu?
Đọc xong một bài giải là bước đầu. Phần lớn học sinh tự học sẽ gặp những trở ngại sau:
  • Không biết học gì trước, học gì sau, học lan man nên kiến thức bị hổng.
  • Bí bài không ai gỡ, ngồi cả buổi vẫn không ra rồi bỏ cuộc.
  • Code chạy được nhưng không đủ điểm, không biết sai ở đâu, chậm ở đâu.
  • Không có đề thi thử và người chấm, nên không biết mình đang ở mức nào.
  • Dễ nản giữa chừng khi không có ai theo sát.
Học cùng thầy Nam Sơn, các trở ngại đó được gỡ bỏ:
  • Lộ trình 111 buổi video sắp sẵn từ dòng code đầu tiên đến đề HSG thật của các tỉnh.
  • Thầy trực tiếp gỡ bí, chữa bài cho từng em qua nhóm Zalo riêng.
  • Hệ thống chấm bài tự động 24/7 và thi thử HSG hằng tuần để biết chính xác mình đang ở đâu.
  • Đóng học phí một lần, thầy kèm lâu dài: từ thi chuyên Tin đến HSG quốc gia và cả khi lên đại học.

Nhận xét