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

Dịch chuyển tức thời

Dịch chuyển tức thời
Nguồn: Codeforces Group Độ khó: Khó BFS Nén số

Đề bài

Trong một trò chơi di chuyển trên bảng số có quy tắc như sau:

  • Bảng số gồm có $N$ dòng và $M$ cột; các dòng được đánh số 1 đến $N$, từ trên xuống dưới; các cột được đánh số từ 1 đến $M$, từ trái sang phải. Ô ở dòng thứ $u$ giao với cột thứ $v$ được gọi là ô $(u, v)$. Ô $(u, v)$ chứa một số nguyên $A_{uv}$ không âm.
  • Từ ô $(u, v)$, người chơi có thể di chuyển sang một ô có chung cạnh: $(u - 1, v)$, $(u + 1, v)$, $(u, v - 1)$, $(u, v + 1)$ hoặc di chuyển sang một ô khác có cùng giá trị, và không thể di chuyển vào ô có giá trị bằng 0. Mỗi lần di chuyển tốn một đơn vị thời gian.

Yêu cầu: cho vị trí ô xuất phát và ô đích, tìm thời gian nhỏ nhất đi từ ô xuất phát về ô đích theo luật của trò chơi.

Dữ liệu vào

  • Dòng đầu tiên gồm hai số nguyên dương $N$ và $M$ là số dòng và số cột của bảng.
  • Dòng thứ hai gồm bốn số $x, y, z, t$ mô tả xuất phát ở ô $(x, y)$ và đích ở ô $(z, t)$.
  • $N$ dòng sau, mỗi dòng gồm $M$ số nguyên không âm mô tả bảng số.

Kết quả ra: gồm một số nguyên dương là số đơn vị thời gian nhỏ nhất để đi từ ô xuất phát đến ô đích thoả mãn yêu cầu.

Lưu ý: mỗi số nguyên cách nhau một dấu cách. Dữ liệu đảm bảo luôn có đường đi từ xuất phát đến đích.

  • Có 40% số test: $N, M \le 100$, $A_{uv} \lt 10^9$ và các số nguyên dương trong bảng phân biệt;
  • Có 20% số test khác: $N, M \le 1000$, $A_{uv} \lt 10^9$ và các số nguyên dương trong bảng phân biệt;
  • Có 20% số test khác: $N, M \le 1000$, $A_{uv} \lt 10^9$ và các số nguyên dương trong bảng lặp lại không quá hai lần;
  • Có 20% số test còn lại: $N, M \le 1000$, $A_{uv} \lt 10^9$ và các số trong bảng có thể lặp lại nhiều lần.
Ví dụ 1 – input
5 4
1 1 5 4
1 2 3 4
5 0 0 6
7 0 8 9
0 0 10 0
11 12 13 14
output
9
Giải thích

Có thể đi như các đỉnh được tô đậm: 1, 2, 3, 4, 6, 9, 8, 10, 13, 14.

1234
5006
7089
00100
11121314
Ví dụ 2 – input
5 4
1 1 5 4
1 2 3 4
5 0 0 6
7 0 8 6
0 0 6 0
3 4 7 9
output
4
Giải thích

Có thể đi như các đỉnh được tô đậm: 1, 5, 7, 7, 9 (từ ô số 7 ở dòng 3 dịch chuyển tức thời sang ô số 7 ở dòng 5).

1234
5006
7086
0060
3479

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 😇.

Dành cho học viên · Ưu tiênNộp bài trên hệ thống CLUE OJ

Link này chỉ học viên đã tham gia tổ chức mới vào được. Học viên nên ưu tiên nộp ở đây vì sẽ được chấm nhanh hơn.

Nộp bài trên CLUE OJ
Dành cho tất cả mọi ngườiNộp bài trên Codeforces

Bạn nào muốn thử nộp bài để hệ thống chấm xem có đúng hay không thì dùng link này nhé. Chưa có tài khoản thì tạo tài khoản rồi đăng nhập nộp bài thử hen.

Nộp bài trên Codeforces

Tóm tắt đề bài một cách đơn giản nhất để bạn nào chưa hiểu: cho 1 mê cung có $n$ dòng và $m$ cột. Từ một ô bất kì ta có thể di chuyển sang một ô chung cạnh hoặc một ô khác có giá trị bằng ô đó, và ta không được di chuyển vào ô có giá trị bằng 0. Ta cần tìm đường đi ngắn nhất từ ô có toạ độ $\{x, y\}$ đến ô có toạ độ $\{z, t\}$ và in ra số bước đi của đường đi ngắn nhất đó. Biết $n, m \le 1000$, $0 \le a[i][j] \lt 10^9$, dữ liệu vào luôn đảm bảo có đường đi; đề bài chia thành 4 subtask lận luôn nha.

Kiến thức cần có
Để có thể làm tốt được bài này thì bạn cần phải có kiến thức cơ bản về đồ thị nha. Cụ thể bạn sẽ cần kiến thức về danh sách kề (adjacency list, các bạn hiểu đơn giản là mảng các vector); tìm kiếm theo chiều sâu (DFS – depth-first search); tìm kiếm theo chiều rộng (BFS – breadth-first search); và kiến thức về di chuyển các ô chung cạnh trong mảng 2 chiều – mẹo tạo 2 mảng dx, dy, chốc nữa mình sẽ nói rõ.

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

Tham khảoCách trâu nhất: DFS thử mọi đường đi

Đầu tiên ta hãy thử xét với trường hợp các số nguyên dương trong mê cung là phân biệt. Các bạn cần lưu ý chỗ "các số nguyên dương trong mê cung là phân biệt": số 0 không phải là số nguyên dương nha, nên trong mê cung vẫn có thể có nhiều số 0 đó. Khi các số nguyên dương phân biệt thì không có 2 ô nào cùng giá trị, tức là không thể dịch chuyển tức thời, chỉ đi được sang ô chung cạnh thôi.

Cách làm ta dễ nghĩ tới nhất chính là vét cạn: ta đi thử mọi đường đi có thể có từ đỉnh nguồn đến mọi đỉnh, từ đó liên tục cập nhật mảng dis là mảng lưu khoảng cách từ đỉnh nguồn đến mọi đỉnh. Sau khi thử mọi trường hợp thì mảng dis sẽ lưu khoảng cách ngắn nhất từ đỉnh nguồn đến mọi đỉnh, bao gồm cả đích rồi. Và cách làm trâu nhất là dùng DFS nha; cách này các bạn tham khảo thôi vì nếu mà có chút kiến thức về đồ thị là nhận ra bài này sẽ dùng BFS ngay, mình sẽ nói ở đoạn sau nhé.

Các bạn hiểu DFS tức là mình sẽ đi thăm một đỉnh $\{i, j\}$ chẳng hạn: lúc đầu mình sẽ đánh dấu là đã thăm đỉnh $\{i, j\}$ rồi, sau đó duyệt qua các đỉnh mà từ đỉnh $\{i, j\}$ có thể đi sang được, nếu có đỉnh chưa thăm thì ta sẽ thăm đỉnh đó bằng cách gọi đệ quy.

Mẹo duyệt 4 ô chung cạnh: cách làm gọn nhất là tạo 2 mảng dx, dy lưu các giá trị giúp ta thay đổi $i$, $j$. Đi sang ô bên trên sẽ là $\{i - 1, j + 0\}$; sang ô bên trái là $\{i + 0, j - 1\}$; sang bên phải là $\{i + 0, j + 1\}$; xuống bên dưới là $\{i + 1, j + 0\}$. Vậy ta sẽ lưu dx[4] = {-1, 0, 0, 1}, dy[4] = {0, -1, 1, 0}. Từ đó nếu muốn duyệt 4 ô chung cạnh thì chỉ việc dùng 1 vòng for rồi cộng lần lượt dx[k], dy[k] vào $i$, $j$ là được rồi.

int dx[4] = {-1, 0, 0, 1}, dy[4] = {0, -1, 1, 0};
for(int k = 0; k < 4; ++k){
    int adjx = i + dx[k], adjy = j + dy[k];
    // {adjx, adjy} là ô chung cạnh thứ k của ô {i, j}
}

Mẹo không cần kiểm tra ra ngoài bảng: ta còn phải check các ô chung cạnh đó có nằm trong mảng không; ví dụ ta đang ở vị trí $\{1, 1\}$ thì sẽ không có ô bên trên hay ô bên trái. Giải pháp của mình ở đây là: vì ta không thể đi tới các ô có giá trị bằng 0, mà lúc khai báo biến toàn cục thì mặc định mọi phần tử sẽ bằng 0, vậy ta sẽ khai báo mảng a là mảng toàn cục và khai báo thừa ra để có các phần tử ngoài rìa, ví dụ int a[1005][1005]. Ta sẽ chỉ dùng từ $\{1, 1\}$ đến tối đa $\{1000, 1000\}$ thôi, còn các giá trị nằm ngoài sẽ mặc định là 0. Vậy khi duyệt ta chỉ cần check xem ô ta định tới có khác 0 không là được.

Sau đây là source code cho các bạn tham khảo cách làm trên nha, lưu ý là chỉ đúng khi các số nguyên dương phân biệt thôi đó:

Xem code DFS (chỉ để tham khảo)
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;
typedef pair<int, int> ii;
const int N = 1000;
int n, m;
int a[N + 5][N + 5];
bool visited[N + 5][N + 5];
int dis[N + 5][N + 5], dx[4] = {-1, 0, 0, 1}, dy[4] = {0, -1, 1, 0};
void findShortestPathDFS(int i, int j){
    visited[i][j] = true;
    for(int k = 0; k < 4; ++k){
        int adjx = i + dx[k], adjy = j + dy[k];
        if(a[adjx][adjy] && !visited[adjx][adjy]){
            dis[adjx][adjy] = min(dis[adjx][adjy], dis[i][j] + 1);
            findShortestPathDFS(adjx, adjy);
        }
    }
    visited[i][j] = false;
}
int main(){
    ios_base::sync_with_stdio(0);
    cin.tie(0);
    int x, y, z, t;
    cin >> n >> m >> x >> y >> z >> t;
    for(int i = 1; i <= n; ++i){
        for(int j = 1; j <= m; ++j){
            cin >> a[i][j];
        }
    }
    fill(&dis[0][0], &dis[N + 4][N + 5], INT_MAX);
    dis[x][y] = 0;
    findShortestPathDFS(x, y);
    cout << dis[z][t];
    return 0;
}
C++▶ Chạy tay từng dòng codeDFS thử mọi đường đi (chỉ để tham khảo): đi sâu, cập nhật dis bằng min, rồi gỡ visited để thử đường khác.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2#define endl '\n'
3using namespace std;
4typedef long long ll;
5typedef pair<int, int> ii;
6const int N = 1000;
7int n, m;
8int a[N + 5][N + 5];
9bool visited[N + 5][N + 5];
10int dis[N + 5][N + 5], dx[4] = {-1, 0, 0, 1}, dy[4] = {0, -1, 1, 0};
11void findShortestPathDFS(int i, int j){
12 visited[i][j] = true;
13 for(int k = 0; k < 4; ++k){
14 int adjx = i + dx[k], adjy = j + dy[k];
15 if(a[adjx][adjy] && !visited[adjx][adjy]){
16 dis[adjx][adjy] = min(dis[adjx][adjy], dis[i][j] + 1);
17 findShortestPathDFS(adjx, adjy);
18 }
19 }
20 visited[i][j] = false;
21}
22int main(){
23 ios_base::sync_with_stdio(0);
24 cin.tie(0);
25 int x, y, z, t;
26 cin >> n >> m >> x >> y >> z >> t;
27 for(int i = 1; i <= n; ++i){
28 for(int j = 1; j <= m; ++j){
29 cin >> a[i][j];
30 }
31 }
32 fill(&dis[0][0], &dis[N + 4][N + 5], INT_MAX);
33 dis[x][y] = 0;
34 findShortestPathDFS(x, y);
35 cout << dis[z][t];
36 return 0;
37}

Giải thích một số chỗ trong cách làm trên:

  1. Hàm findShortestPathDFS(int i, int j): đầu tiên ta đánh dấu đỉnh $\{i, j\}$ là đã thăm, tức là true (mặc định khi khai báo biến toàn cục thì tất cả đều là false, ta quy ước là chưa thăm). Sau đó ta đi duyệt các đỉnh chung cạnh với nó. if(a[adjx][adjy] && !visited[adjx][adjy]) nghĩa là đầu tiên ta check a[adjx][adjy] có khác 0 không, nếu có thì check tiếp đỉnh $\{adjx, adjy\}$ đã được thăm chưa. Nếu chưa thì ta cập nhật dis là khoảng cách thấp nhất có thể, sau đó gọi đệ quy. Vì ta đang thử mọi đường đi có thể nên sau cùng ta lại phải gán visited[i][j] = false; để thử sang đường đi khác nha.
  2. ios::sync_with_stdio(0); cin.tie(0); 2 dòng đầu tiên của hàm main sẽ giúp đẩy nhanh tốc độ nhập xuất dữ liệu nếu các bạn đang code theo ngôn ngữ C++ (để hiểu vì sao thì các bạn cứ gõ mấy từ này lên Google là sẽ ra rất nhiều bài viết giải thích nhé), nên kinh nghiệm trong các bài mà cần nhập xuất nhiều thì ta nên sử dụng 2 dòng này; ví dụ với bài này thì đầu vào có thể lên tới $10^6$ số. Tất nhiên nếu các bạn code theo cú pháp của C thì chẳng cần mấy dòng đó làm gì, tuy nhiên thường khi code các bài lập trình thi đấu thế này ta dùng C++ vì cú pháp ngắn gọn cũng như nhiều hỗ trợ của nó, vậy nên bạn nào code C++ thì nhớ thêm 2 dòng này vào đầu hàm main nhé.
  3. fill(&dis[0][0], &dis[N + 4][N + 5], INT_MAX); là ta điền toàn bộ mảng dis bằng số to nhất, để có thể cập nhật min, vì mọi giá trị lần đầu tiên so sánh sẽ đều nhỏ hơn INT_MAX mà cập nhật lại. Nếu không làm vậy thì mặc định mọi phần tử trong mảng dis sẽ bằng 0 vì ta khai báo là biến toàn cục.
  4. dis[x][y] = 0; tất nhiên là khoảng cách từ đỉnh nguồn tới chính nó bằng 0 rồi. Sau khi chạy xong hàm trên ta chỉ việc in ra dis[z][t] là khoảng cách ngắn nhất tới đỉnh $\{z, t\}$.

Với cách trên thì chắc chắn ta không thể AC (Accepted – được chấp nhận) được bài này rồi; thậm chí chúng ta còn chưa chắc có thể ăn được hoàn toàn subtask 1 cơ, vì số đường đi trên lưới tăng theo cấp số mũ, và đệ quy sâu tới hàng chục nghìn tầng còn có thể tràn stack nữa.

60%Subtask 1, 2: các số nguyên dương phân biệt – BFS

Mình gộp subtask 1 với 2 vào làm 1 vì với cách dùng BFS thì ta hoàn toàn có thể ăn được cả subtask 1 và 2 rồi. Thậm chí cách làm bằng BFS này còn đơn giản và dễ cài đặt hơn cách trên đó.

Thuật toán BFS sẽ ưu tiên mở rộng các đỉnh gần đỉnh nguồn hơn, từ đó với đồ thị không có trọng số thì BFS sẽ cho đường đi ngắn nhất giữa 2 đỉnh. Ở bài trên, để đi từ vị trí này sang vị trí khác chung cạnh thì ta đều tốn 1 đơn vị thời gian, hay chính là đồ thị không có trọng số đó.

Mở đầu việc cài đặt BFS thì ta vẫn cần mảng dis để lưu khoảng cách, mảng dx, dy giúp ta di chuyển. Ta cần 1 queue (hàng đợi) rỗng; đầu tiên cho đỉnh nguồn vào đó, đánh dấu là đã thăm và khoảng cách đến nó dĩ nhiên bằng 0. Rồi ta lặp chừng nào hàng đợi còn phần tử: lấy đỉnh đầu ra, gọi là đỉnh $\{i, j\}$, xoá nó khỏi hàng đợi; duyệt các đỉnh mà từ đỉnh $\{i, j\}$ có thể đi tới (ở đây là các đỉnh chung cạnh và khác 0); nếu chưa thăm thì cập nhật khoảng cách cho chúng – cái hay ở đây là ta chỉ việc gán luôn khoảng cách bằng dis[i][j] + 1 vì BFS luôn cho ta đường đi ngắn nhất – rồi đưa đỉnh đó vào hàng đợi và đánh dấu đã thăm. Bạn nào còn chưa rõ thì mình tiến cử tham khảo về BFS ở link này nha: VNOI Wiki – Tìm kiếm theo chiều rộng.

Và để tối ưu thì: trong quá trình BFS thường ta sẽ có đường đi ngắn nhất từ đỉnh nguồn đến mọi đỉnh, nhưng ở bài này ta chỉ cần từ đỉnh nguồn tới đỉnh đích thôi. Cho nên khi BFS chạm tới đỉnh đích thì đó cũng đồng thời là khoảng cách ngắn nhất cần tìm, ta dừng BFS và trả về khoảng cách đó luôn nha.

Xem code subtask 1, 2 (BFS)
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;
typedef pair<int, int> ii;
const int N = 1000;
int n, m;
int a[N + 5][N + 5];
bool visited[N + 5][N + 5];
int dis[N + 5][N + 5], dx[4] = {-1, 0, 0, 1}, dy[4] = {0, -1, 1, 0};
int findShortestPathBFS(int x, int y, int z, int t){
    queue<ii> q;
    q.push((ii){x, y});
    visited[x][y] = true;
    dis[x][y] = 0;
    if(z == x && t == y){
        return 0;
    }
    while(!q.empty()){
        int i = q.front().first, j = q.front().second;
        q.pop();
        for(int k = 0; k < 4; ++k){
            int adjx = i + dx[k], adjy = j + dy[k];
            if(a[adjx][adjy] && !visited[adjx][adjy]){
                dis[adjx][adjy] = dis[i][j] + 1;
                if(adjx == z && adjy == t){
                    return dis[z][t];
                }
                q.push((ii){adjx, adjy});
                visited[adjx][adjy] = true;
            }
        }
    }
    return dis[z][t];
}
int main(){
    ios_base::sync_with_stdio(0);
    cin.tie(0);
    int x, y, z, t;
    cin >> n >> m >> x >> y >> z >> t;
    for(int i = 1; i <= n; ++i){
        for(int j = 1; j <= m; ++j){
            cin >> a[i][j];
        }
    }
    cout << findShortestPathBFS(x, y, z, t);
    return 0;
}
import sys
from collections import deque
N = 1000
a = [[0] * (N + 5) for _ in range(N + 5)]
visited = [[False] * (N + 5) for _ in range(N + 5)]
dis = [[10**18] * (N + 5) for _ in range(N + 5)]
dx = [-1, 0, 0, 1]
dy = [0, -1, 1, 0]
def findShortestPathBFS(x, y, z, t, n, m):
    q = deque()
    q.append((x, y))
    visited[x][y] = True
    dis[x][y] = 0
    if z == x and t == y:
        return 0
    while q:
        i, j = q.popleft()
        for k in range(4):
            adjx, adjy = i + dx[k], j + dy[k]
            if 1 <= adjx <= n and 1 <= adjy <= m:
                if a[adjx][adjy] and not visited[adjx][adjy]:
                    dis[adjx][adjy] = dis[i][j] + 1
                    if adjx == z and adjy == t:
                        return dis[z][t]
                    q.append((adjx, adjy))
                    visited[adjx][adjy] = True
    return dis[z][t]
data = list(map(int, sys.stdin.read().strip().split()))
n, m, x, y, z, t = data[:6]
arr = data[6:]
idx = 0
for i in range(1, n + 1):
    for j in range(1, m + 1):
        a[i][j] = arr[idx]
        idx += 1
print(findShortestPathBFS(x, y, z, t, n, m))
C++▶ Chạy tay từng dòng codeSubtask 1, 2: BFS loang theo 4 ô chung cạnh, gặp đích là dừng. Chưa xử lý dịch chuyển nên Ví dụ 2 sẽ ra sai (9 thay vì 4).
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2#define endl '\n'
3using namespace std;
4typedef long long ll;
5typedef pair<int, int> ii;
6const int N = 1000;
7int n, m;
8int a[N + 5][N + 5];
9bool visited[N + 5][N + 5];
10int dis[N + 5][N + 5], dx[4] = {-1, 0, 0, 1}, dy[4] = {0, -1, 1, 0};
11int findShortestPathBFS(int x, int y, int z, int t){
12 queue<ii> q;
13 q.push((ii){x, y});
14 visited[x][y] = true;
15 dis[x][y] = 0;
16 if(z == x && t == y){
17 return 0;
18 }
19 while(!q.empty()){
20 int i = q.front().first, j = q.front().second;
21 q.pop();
22 for(int k = 0; k < 4; ++k){
23 int adjx = i + dx[k], adjy = j + dy[k];
24 if(a[adjx][adjy] && !visited[adjx][adjy]){
25 dis[adjx][adjy] = dis[i][j] + 1;
26 if(adjx == z && adjy == t){
27 return dis[z][t];
28 }
29 q.push((ii){adjx, adjy});
30 visited[adjx][adjy] = true;
31 }
32 }
33 }
34 return dis[z][t];
35}
36int main(){
37 ios_base::sync_with_stdio(0);
38 cin.tie(0);
39 int x, y, z, t;
40 cin >> n >> m >> x >> y >> z >> t;
41 for(int i = 1; i <= n; ++i){
42 for(int j = 1; j <= m; ++j){
43 cin >> a[i][j];
44 }
45 }
46 cout << findShortestPathBFS(x, y, z, t);
47 return 0;
48}

Giải thích một số chỗ trong cách làm trên:

  1. queue<ii> q; q.push((ii){x, y}); – mình đã typedef pair<int, int> ii; nên tạo 1 hàng đợi với các phần tử là pair; lúc push vào thì mình ép kiểu (ii) thôi.
  2. if(z == x && t == y) – nếu đích trùng với xuất phát thì trả về 0 luôn; nếu không làm vậy thì trường hợp này thuật toán sẽ duyệt qua toàn mảng luôn đó.
  3. int i = q.front().first, j = q.front().second; là lấy phần tử đầu của queue; q.pop() là xoá đỉnh đầu vừa lấy khỏi hàng đợi.
  4. if(adjx == z && adjy == t) – gặp đích là dừng BFS rồi trả về kết quả luôn nha.

Đánh giá độ phức tạp:

  • Không gian: $O(3 \cdot N^2)$ vì ta dùng tận 3 mảng kích cỡ $[N][N]$.
  • Thời gian: $O(6 \cdot n \cdot m)$. Lúc đọc vào mất $O(n \cdot m)$; độ phức tạp của BFS là $O(V + E)$ (số đỉnh + số cạnh), bài trên có $n \cdot m$ đỉnh và mỗi đỉnh nối với 4 đỉnh nên có khoảng $4 \cdot n \cdot m$ cạnh, BFS là $O(5 \cdot n \cdot m)$. Tổng lại là $O(6 \cdot n \cdot m)$.

Với cách trên thì ta mới ăn được 59/100 test thôi, chỉ ăn được các trường hợp mà các số nguyên dương trong mê cung là phân biệt.

+20%Subtask 3: số lặp lại không quá 2 lần – thêm cạnh "dịch chuyển" bằng map

Ở subtask này thì các số nguyên dương có thể bị lặp lại nha. Vậy từ đỉnh $\{i, j\}$ ta không chỉ có thể đi qua 4 đỉnh chung cạnh nữa mà còn có thể đi qua các đỉnh có cùng giá trị. Giải pháp của ta sẽ là dùng map để lưu vị trí các đỉnh có cùng giá trị, phần tử của map sẽ là một vector<ii>; lưu ý là ta sẽ không lưu các giá trị 0 nha.

Điều vô cùng quan trọng: mỗi giá trị chỉ cần "dịch chuyển" 1 lần
Giả sử có 4 ô có giá trị bằng 5, và ô số 5 gần nguồn nhất (gọi là ô 5 số 1) có dis = 3. Từ ô đó ta có thể dịch chuyển sang bất kì ô có giá trị 5 khác, nên dis của 3 ô số 5 còn lại (ô 5 số 2, 3, 4) sẽ bằng 4. Sau đó khi ta duyệt tới ô 5 số 2, thì các ô 5 số 1, 3, 4 cũng là các đỉnh kề với nó, vậy có cần duyệt lại không? Câu trả lời là không: bước sang ô 5 số 3 hoặc 4 thì dis của chúng sẽ lên 5, chỉ có tăng; còn bước về ô 5 số 1 thì cũng chẳng để làm gì. Kết luận: khi các đỉnh có cùng giá trị đã được duyệt 1 lần rồi thì không cần duyệt lại lần nữa, duyệt lại cũng không thay đổi được kết quả.

Điều trên vô cùng quan trọng nha, vì nếu không có điều trên thì ô 5 số 1 duyệt qua ô 5 số 2, 3, 4 mất 3 bước; rồi ô 5 số 2 lại đi sang ô 5 số 1, 3, 4 mất 3 bước nữa; cứ vậy tổng cộng mất tận 12 bước. Với $k$ ô cùng giá trị thì sẽ thành $k^2$ bước, mà $k$ có thể lên tới $10^6$ thì TLE chắc luôn. Vậy ta cần thêm 1 map nữa để check rằng với 1 giá trị bất kì, ta đã duyệt qua các đỉnh có cùng giá trị đó chưa. Nếu chưa thì duyệt, nếu rồi thì thôi.

Em nhập bảng (các dòng cách nhau bởi dấu /), ô xuất phát, ô đích rồi bấm Tiếp để xem BFS loang từng ô: số nhỏ ở góc mỗi ô là dis, ô viền tím là ô được dịch chuyển tức thời tới (mỗi giá trị chỉ dịch chuyển 1 lần):

Ví dụ nhanh:
Xem code subtask 3 (BFS + map)
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;
typedef pair<int, int> ii;
const int N = 1000;
int n, m;
int a[N + 5][N + 5];
bool visited[N + 5][N + 5];
int dis[N + 5][N + 5], dx[4] = {-1, 0, 0, 1}, dy[4] = {0, -1, 1, 0};
map<int, vector<ii>> mp;
map<int, bool> visitedVal;
int findShortestPathBFS(int x, int y, int z, int t){
    visitedVal[0] = true;
    queue<ii> q;
    q.push((ii){x, y});
    visited[x][y] = true;
    dis[x][y] = 0;
    if(z == x && t == y){
        return 0;
    }
    while(!q.empty()){
        int i = q.front().first, j = q.front().second;
        q.pop();
        for(int k = 0; k < 4; ++k){
            int adjx = i + dx[k], adjy = j + dy[k];
            if(a[adjx][adjy] && !visited[adjx][adjy]){
                dis[adjx][adjy] = dis[i][j] + 1;
                if(adjx == z && adjy == t){
                    return dis[z][t];
                }
                q.push((ii){adjx, adjy});
                visited[adjx][adjy] = true;
            }
        }
        if(!visitedVal[a[i][j]]){
            for(ii tmp : mp[a[i][j]]){
                if(!visited[tmp.first][tmp.second]){
                    dis[tmp.first][tmp.second] = dis[i][j] + 1;
                    if(tmp.first == z && tmp.second == t){
                        return dis[z][t];
                    }
                    q.push((ii){tmp.first, tmp.second});
                    visited[tmp.first][tmp.second] = true;
                }
            }
            visitedVal[a[i][j]] = true;
        }
    }
    return dis[z][t];
}
int main(){
    ios_base::sync_with_stdio(0);
    cin.tie(0);
    int x, y, z, t;
    cin >> n >> m >> x >> y >> z >> t;
    for(int i = 1; i <= n; ++i){
        for(int j = 1; j <= m; ++j){
            cin >> a[i][j];
            if(a[i][j]){
                mp[a[i][j]].push_back((ii){i, j});
            }
        }
    }
    cout << findShortestPathBFS(x, y, z, t);
    return 0;
}
import sys
from collections import deque, defaultdict
N = 1000
a = [[0] * (N + 5) for _ in range(N + 5)]
visited = [[False] * (N + 5) for _ in range(N + 5)]
dis = [[10**18] * (N + 5) for _ in range(N + 5)]
dx = [-1, 0, 0, 1]
dy = [0, -1, 1, 0]
mp = defaultdict(list)
visitedVal = defaultdict(bool)
def findShortestPathBFS(x, y, z, t, n, m):
    visitedVal[0] = True
    q = deque()
    q.append((x, y))
    visited[x][y] = True
    dis[x][y] = 0
    if z == x and t == y:
        return 0
    while q:
        i, j = q.popleft()
        for k in range(4):
            adjx, adjy = i + dx[k], j + dy[k]
            if 1 <= adjx <= n and 1 <= adjy <= m:
                if a[adjx][adjy] and not visited[adjx][adjy]:
                    dis[adjx][adjy] = dis[i][j] + 1
                    if adjx == z and adjy == t:
                        return dis[z][t]
                    q.append((adjx, adjy))
                    visited[adjx][adjy] = True
        if not visitedVal[a[i][j]]:
            for (u, v) in mp[a[i][j]]:
                if not visited[u][v]:
                    dis[u][v] = dis[i][j] + 1
                    if u == z and v == t:
                        return dis[z][t]
                    q.append((u, v))
                    visited[u][v] = True
            visitedVal[a[i][j]] = True

    return dis[z][t]
data = list(map(int, sys.stdin.read().strip().split()))
n, m, x, y, z, t = data[:6]
arr = data[6:]
idx = 0
for i in range(1, n + 1):
    for j in range(1, m + 1):
        a[i][j] = arr[idx]
        if a[i][j]:
            mp[a[i][j]].append((i, j))
        idx += 1
print(findShortestPathBFS(x, y, z, t, n, m))
C++▶ Chạy tay từng dòng codeSubtask 3: thêm map mp lưu vị trí các ô cùng giá trị, map visitedVal để mỗi giá trị chỉ "dịch chuyển" 1 lần.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2#define endl '\n'
3using namespace std;
4typedef long long ll;
5typedef pair<int, int> ii;
6const int N = 1000;
7int n, m;
8int a[N + 5][N + 5];
9bool visited[N + 5][N + 5];
10int dis[N + 5][N + 5], dx[4] = {-1, 0, 0, 1}, dy[4] = {0, -1, 1, 0};
11map<int, vector<ii>> mp;
12map<int, bool> visitedVal;
13int findShortestPathBFS(int x, int y, int z, int t){
14 visitedVal[0] = true;
15 queue<ii> q;
16 q.push((ii){x, y});
17 visited[x][y] = true;
18 dis[x][y] = 0;
19 if(z == x && t == y){
20 return 0;
21 }
22 while(!q.empty()){
23 int i = q.front().first, j = q.front().second;
24 q.pop();
25 for(int k = 0; k < 4; ++k){
26 int adjx = i + dx[k], adjy = j + dy[k];
27 if(a[adjx][adjy] && !visited[adjx][adjy]){
28 dis[adjx][adjy] = dis[i][j] + 1;
29 if(adjx == z && adjy == t){
30 return dis[z][t];
31 }
32 q.push((ii){adjx, adjy});
33 visited[adjx][adjy] = true;
34 }
35 }
36 if(!visitedVal[a[i][j]]){
37 for(ii tmp : mp[a[i][j]]){
38 if(!visited[tmp.first][tmp.second]){
39 dis[tmp.first][tmp.second] = dis[i][j] + 1;
40 if(tmp.first == z && tmp.second == t){
41 return dis[z][t];
42 }
43 q.push((ii){tmp.first, tmp.second});
44 visited[tmp.first][tmp.second] = true;
45 }
46 }
47 visitedVal[a[i][j]] = true;
48 }
49 }
50 return dis[z][t];
51}
52int main(){
53 ios_base::sync_with_stdio(0);
54 cin.tie(0);
55 int x, y, z, t;
56 cin >> n >> m >> x >> y >> z >> t;
57 for(int i = 1; i <= n; ++i){
58 for(int j = 1; j <= m; ++j){
59 cin >> a[i][j];
60 if(a[i][j]){
61 mp[a[i][j]].push_back((ii){i, j});
62 }
63 }
64 }
65 cout << findShortestPathBFS(x, y, z, t);
66 return 0;
67}

Giải thích một số chỗ trong cách làm trên:

  1. map<int, vector<ii>> mp; map<int, bool> visitedVal; – 2 map lần lượt dùng để lưu vị trí của các giá trị trùng nhau và check xem với 1 giá trị bất kì ta đã từng duyệt qua các đỉnh có cùng giá trị đó chưa.
  2. visitedVal[0] = true; – ta gán vậy vì giá trị 0 không được xét nên không có chuyện dịch chuyển giữa các ô 0.
  3. if(!visitedVal[a[i][j]]) – check xem với giá trị a[i][j] thì các đỉnh trùng với nó ta đã duyệt chưa.
  4. for(ii tmp : mp[a[i][j]]) – duyệt qua danh sách các vị trí có giá trị bằng a[i][j] bằng vòng lặp for each; biến tmp sẽ lần lượt bằng từng phần tử trong vector đó.
  5. if(!visited[tmp.first][tmp.second]) – check đỉnh đó đã thăm chưa, rồi làm tương tự ở trên: gán dis = dis[i][j] + 1, gặp đích thì dừng BFS và trả về luôn, không thì cho vào hàng đợi và đánh dấu đã thăm.
  6. visitedVal[a[i][j]] = true; – đánh dấu rằng với giá trị a[i][j] thì các đỉnh có giá trị đó đã được duyệt rồi.
  7. if(a[i][j]) mp[a[i][j]].push_back((ii){i, j}); – lúc đọc vào, ô nào khác 0 thì push vị trí của nó vào vector tương ứng.

Đánh giá độ phức tạp:

  • Không gian: tốn hơn subtask 1, 2 vì dùng thêm 2 map, nhưng với giới hạn đề bài cho là 1 GB thì không phải lo nha.
  • Thời gian: cách trên thậm chí chạy đúng với các giá trị lặp nhiều hơn 2 lần. Nhưng nó sẽ bị chậm ở chỗ thêm phần tử vào map mất $O(\log N)$, thêm $n$ phần tử là $O(n \log n)$, và map bản chất là cây cân bằng nên còn mất thêm thời gian cân bằng lại cây mỗi lần thêm vào. Chưa kể mỗi lần truy cập 1 phần tử trong map lại tốn $\log N$, nên ta sẽ bị TLE ở những test có rất nhiều giá trị.

Cách trên thì ta sẽ không còn bị WA (wrong answer – trả đáp án sai) nữa mà chỉ bị TLE (time limit exceeded – quá thời gian) ở một số test. Với code C++ thì được 80/100, còn code Python thì được 88/100.

Gợi ý nhỏ: thử unordered_map. Nếu code trên các bạn thay 2 chỗ khai báo map thành unordered_map, tức là lưu theo hash table (bảng băm) thay vì tree (cây), thì chi phí tìm kiếm trung bình chỉ là $O(1)$ cho mỗi lần tìm và không tốn chi phí cân bằng cây. Thực tế sửa lại một chút như vậy thôi mà code C++ nộp được 99/100, thêm 11 test; code Python được 89/100, thêm 1 test.
// Sửa 2 dòng khai báo:
map<int, vector<ii>> mp;
map<int, bool> visitedVal;
// thành:
unordered_map<int, vector<ii>> mp;
unordered_map<int, bool> visitedVal;
Mình cũng cảnh báo là về mặt lý thuyết thì unordered_map trường hợp xấu nhất khi tìm 1 phần tử có thể lên đến $O(N)$ nhé các bạn, đó là khi hàm băm không tốt dẫn đến xung đột. Trong khi map lưu theo cơ chế cây nên mọi trường hợp đều đảm bảo $O(\log N)$, nên tuy độ phức tạp cao hơn nhưng dùng map sẽ yên tâm hơn. Vậy không phải lúc nào dùng unordered_map cũng tốt hơn map 🤣. Kinh nghiệm là nếu code dùng map mà bị TLE thì cứ thử thay bằng unordered_map rồi nộp lại xem, nếu được thêm test thì tốt, còn nếu thấy 1 số test đang từ AC chuyển sang TLE thì quay xe lại map thôi 🤪

100%Subtask 4: rời rạc hoá (nén số) + danh sách kề – AC

Ta nhận ra rằng để AC được bài này thì phải tìm cách không dùng map mà vẫn truy xuất được vị trí của các đỉnh có cùng giá trị, và vẫn check được với 1 giá trị bất kì ta đã duyệt qua các vị trí cùng giá trị với nó chưa. Ở trên ta dùng map vì $a[i][j] \lt 10^9$, ta không thể tạo mảng $10^9$ phần tử được vì sẽ tràn bộ nhớ. Vậy để AC được ta cần biết tới phương pháp rời rạc hoá, hay trong lập trình thi đấu quen gọi là kĩ thuật nén số. Bạn nào chưa biết thì mình tiến cử link này nha: VNOI Wiki – Rời rạc hoá và ứng dụng.

Đúng như tên gọi thôi, ta sẽ nén số lại sao cho nó nhỏ hơn để vừa với tầm mà ta có thể kiểm soát, và vẫn giữ được quan hệ lớn bé. Ví dụ mảng a = {100, 10, 1000, 25, 100} sẽ được nén thành a = {3, 1, 4, 2, 3}:

Vị trí12345
a ban đầu10010100025100
a sau khi nén31423

Ta vẫn giữ được các mối quan hệ lớn bé hay bằng mà số đã nhỏ hơn rất nhiều rồi đó. Và dù mảng có 5 số rất to như $\{10^9, 10^9 + 1, 10^9 + 2, 10^9 + 3, 10^9 + 4\}$ thì cũng nén về $\{1, 2, 3, 4, 5\}$; vậy các số nén về sẽ nằm trong khoảng từ 1 tới kích thước của mảng. Mê cung có tối đa $10^6$ phần tử, vậy ta hoàn toàn có thể nén các số trong mê cung (ngoại trừ số 0) về các giá trị từ 1 tới $10^6$. Sau khi nén, ta dùng danh sách kề (1 mảng các vector) để lưu vị trí các đỉnh có cùng giá trị, và 1 mảng đánh dấu visitedVal để check giá trị đó đã được duyệt chưa.

Về cài đặt: ta cần 1 biến lưu được giá trị của đỉnh và vị trí của đỉnh đó. Các bạn có thể dùng tuple nhưng để code gọn và dễ hiểu nhất thì mình dùng struct: tạo kiểu dữ liệu mới là Data gồm 3 biến int val, i, j. Ta tạo hàm init() giúp đọc đầu vào, nén số và lưu vị trí các đỉnh có cùng giá trị:

#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;
typedef pair<int, int> ii;
const int N = 1000;
struct Data {
    int val, i, j;
};
int n, m, x, y, z, t;
int a[N + 5][N + 5];
vector<ii> adj[N * N + 5];
void init(){
    cin >> n >> m >> x >> y >> z >> t;
    vector<Data> b;
    for(int i = 1; i <= n; ++i){
        for(int j = 1; j <= m; ++j){
            cin >> a[i][j];
            if(a[i][j]){
                b.push_back((Data){a[i][j], i, j});
            }
        }
    }
    sort(b.begin(), b.end(),
         [](const Data &a, const Data &b) -> bool {
             return a.val < b.val;
         });
    a[b[0].i][b[0].j] = 1;
    adj[1].push_back((ii){b[0].i, b[0].j});
    int cnt = 1;
    for(int i = 1; i < (int)b.size(); ++i){
        if(b[i].val != b[i - 1].val){
            cnt++;
        }
        a[b[i].i][b[i].j] = cnt;
        adj[cnt].push_back((ii){b[i].i, b[i].j});
    }
}

Giải thích hàm init() ở trên:

  1. vector<ii> adj[N * N + 5]; – danh sách kề lưu vị trí các đỉnh có cùng giá trị; mảng có tối đa $10^6 = 10^3 \cdot 10^3$ phần tử nên giá trị sau nén tối đa là $10^6$, chỉ cần độ lớn $N \cdot N + 5$ (thừa ra 5 cho chắc).
  2. vector<Data> b; – vector lưu lại giá trị + vị trí của các đỉnh.
  3. if(a[i][j]) b.push_back((Data){a[i][j], i, j}); – chỉ đỉnh nào khác 0 mới cho vào, và nhớ ép kiểu dữ liệu nha.
  4. sort(...) – mình dùng biểu thức lambda để sort vector b tăng dần theo val. Vì chỉ sort một lần nên viết lambda cho gọn; bạn nào không quen thì hoàn toàn có thể viết hàm comp riêng.
  5. a[b[0].i][b[0].j] = 1; – phần tử nhỏ nhất trong mê cung được nén thành 1, và adj[1].push_back(...) cho vị trí của nó vào danh sách kề của giá trị 1.
  6. int cnt = 1 và vòng for bên dưới: duyệt b từ phần tử thứ 2, nếu giá trị khác phần tử trước đó thì tăng cnt; gán a[b[i].i][b[i].j] = cnt rồi cho vị trí đó vào adj[cnt].

Vậy chỉ cần gọi init() là ta đã đọc xong đầu vào, nén số và làm xong danh sách kề rồi. Giờ ta thực hiện như subtask 3 thôi, khác ở chỗ không dùng map nữa mà dùng danh sách kề và 1 mảng đánh dấu visitedVal có độ lớn $N \cdot N + 5$.

#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;
typedef pair<int, int> ii;
const int N = 1000;
struct Data {
    int val, i, j;
};
int n, m, x, y, z, t;
int a[N + 5][N + 5];
vector<ii> adj[N * N + 5];
void init(){
    cin >> n >> m >> x >> y >> z >> t;
    vector<Data> b;
    for(int i = 1; i <= n; ++i){
        for(int j = 1; j <= m; ++j){
            cin >> a[i][j];
            if(a[i][j]){
                b.push_back((Data){a[i][j], i, j});
            }
        }
    }
    sort(b.begin(), b.end(),
         [](const Data &a, const Data &b) -> bool {
             return a.val < b.val;
         });
    a[b[0].i][b[0].j] = 1;
    adj[1].push_back((ii){b[0].i, b[0].j});
    int cnt = 1;
    for(int i = 1; i < (int)b.size(); ++i){
        if(b[i].val != b[i - 1].val){
            cnt++;
        }
        a[b[i].i][b[i].j] = cnt;
        adj[cnt].push_back((ii){b[i].i, b[i].j});
    }
}
bool visited[N + 5][N + 5], visitedVal[N * N + 5];
int dis[N + 5][N + 5], dx[4] = {-1, 0, 0, 1}, dy[4] = {0, -1, 1, 0};
int findShortestPathBFS(){
    visitedVal[0] = true;
    queue<ii> q;
    q.push((ii){x, y});
    visited[x][y] = true;
    dis[x][y] = 0;
    while(!q.empty()){
        int i = q.front().first, j = q.front().second;
        q.pop();
        for(int k = 0; k < 4; ++k){
            int adjx = i + dx[k], adjy = j + dy[k];
            if(a[adjx][adjy] && !visited[adjx][adjy]){
                dis[adjx][adjy] = dis[i][j] + 1;
                if(adjx == z && adjy == t){
                    return dis[z][t];
                }
                q.push((ii){adjx, adjy});
                visited[adjx][adjy] = true;
            }
        }
        if(!visitedVal[a[i][j]]){
            for(ii tmp : adj[a[i][j]]){
                if(!visited[tmp.first][tmp.second]){
                    dis[tmp.first][tmp.second] = dis[i][j] + 1;
                    if(tmp.first == z && tmp.second == t){
                        return dis[z][t];
                    }
                    q.push((ii){tmp.first, tmp.second});
                    visited[tmp.first][tmp.second] = true;
                }
            }
            visitedVal[a[i][j]] = true;
        }
    }
    return dis[z][t];
}
int main(){
    ios_base::sync_with_stdio(0);
    cin.tie(0);
    init();
    cout << findShortestPathBFS();
    return 0;
}
import sys
from collections import deque

def main():
    data = sys.stdin.buffer.read().split()
    n, m, x, y, z, t = map(int, data[:6])
    W = m + 2
    a = [0] * ((n + 2) * W)          # mảng 1 chiều có viền 0 xung quanh
    idx = 6
    for i in range(1, n + 1):
        base = i * W
        for j in range(1, m + 1):
            a[base + j] = int(data[idx]); idx += 1

    # nén số: mỗi giá trị khác 0 -> 1 số thứ tự, đồng thời gom các ô cùng giá trị
    comp = {}
    adj = []
    for p in range(len(a)):
        v = a[p]
        if v:
            c = comp.get(v)
            if c is None:
                c = comp[v] = len(adj)
                adj.append([])
            adj[c].append(p)
            a[p] = c + 1                # giá trị sau nén nằm trong [1, số giá trị khác nhau]
    visitedVal = [False] * (len(adj) + 1)
    visitedVal[0] = True

    src, dst = x * W + y, z * W + t
    if src == dst:
        print(0); return
    dis = [-1] * len(a)
    dis[src] = 0
    q = deque([src])
    while q:
        u = q.popleft()
        d = dis[u] + 1
        for v in (u - W, u - 1, u + 1, u + W):
            if a[v] and dis[v] < 0:
                dis[v] = d
                if v == dst:
                    print(d); return
                q.append(v)
        c = a[u]
        if not visitedVal[c]:
            visitedVal[c] = True
            for v in adj[c - 1]:
                if dis[v] < 0:
                    dis[v] = d
                    if v == dst:
                        print(d); return
                    q.append(v)
    print(dis[dst])

main()
C++▶ Chạy tay từng dòng codeSubtask 4 (AC): nén giá trị về 1..cnt, dùng danh sách kề adj[] và mảng visitedVal[] thay cho map.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2#define endl '\n'
3using namespace std;
4typedef long long ll;
5typedef pair<int, int> ii;
6const int N = 1000;
7struct Data {
8 int val, i, j;
9};
10int n, m, x, y, z, t;
11int a[N + 5][N + 5];
12vector<ii> adj[N * N + 5];
13void init(){
14 cin >> n >> m >> x >> y >> z >> t;
15 vector<Data> b;
16 for(int i = 1; i <= n; ++i){
17 for(int j = 1; j <= m; ++j){
18 cin >> a[i][j];
19 if(a[i][j]){
20 b.push_back((Data){a[i][j], i, j});
21 }
22 }
23 }
24 sort(b.begin(), b.end(),
25 [](const Data &a, const Data &b) -> bool {
26 return a.val < b.val;
27 });
28 a[b[0].i][b[0].j] = 1;
29 adj[1].push_back((ii){b[0].i, b[0].j});
30 int cnt = 1;
31 for(int i = 1; i < (int)b.size(); ++i){
32 if(b[i].val != b[i - 1].val){
33 cnt++;
34 }
35 a[b[i].i][b[i].j] = cnt;
36 adj[cnt].push_back((ii){b[i].i, b[i].j});
37 }
38}
39bool visited[N + 5][N + 5], visitedVal[N * N + 5];
40int dis[N + 5][N + 5], dx[4] = {-1, 0, 0, 1}, dy[4] = {0, -1, 1, 0};
41int findShortestPathBFS(){
42 visitedVal[0] = true;
43 queue<ii> q;
44 q.push((ii){x, y});
45 visited[x][y] = true;
46 dis[x][y] = 0;
47 while(!q.empty()){
48 int i = q.front().first, j = q.front().second;
49 q.pop();
50 for(int k = 0; k < 4; ++k){
51 int adjx = i + dx[k], adjy = j + dy[k];
52 if(a[adjx][adjy] && !visited[adjx][adjy]){
53 dis[adjx][adjy] = dis[i][j] + 1;
54 if(adjx == z && adjy == t){
55 return dis[z][t];
56 }
57 q.push((ii){adjx, adjy});
58 visited[adjx][adjy] = true;
59 }
60 }
61 if(!visitedVal[a[i][j]]){
62 for(ii tmp : adj[a[i][j]]){
63 if(!visited[tmp.first][tmp.second]){
64 dis[tmp.first][tmp.second] = dis[i][j] + 1;
65 if(tmp.first == z && tmp.second == t){
66 return dis[z][t];
67 }
68 q.push((ii){tmp.first, tmp.second});
69 visited[tmp.first][tmp.second] = true;
70 }
71 }
72 visitedVal[a[i][j]] = true;
73 }
74 }
75 return dis[z][t];
76}
77int main(){
78 ios_base::sync_with_stdio(0);
79 cin.tie(0);
80 init();
81 cout << findShortestPathBFS();
82 return 0;
83}

Đánh giá độ phức tạp:

  • Không gian: $O(6 \cdot N^2)$ vì dùng thêm 1 danh sách kề, 1 mảng visitedVal, 1 vector b. Nhưng với giới hạn 1 GB thì các bạn không phải lo nha.
  • Thời gian: $O(nm \log(nm) + 8nm)$: thêm 1 lần sắp xếp vector b tốn $nm \log(nm)$; duyệt vector b tốn tối đa $nm$; và tổng số lần duyệt danh sách kề trong if(!visitedVal[a[i][j]]) { for(ii tmp : adj[a[i][j]]) ... } cũng chỉ tối đa $nm$ (vì mỗi giá trị chỉ duyệt 1 lần). Lưu ý log ở đây là $\log_2$; tính sơ qua ta được khoảng $3 \cdot 10^7$ phép tính, dự đoán khoảng 0.3 giây cho trường hợp xấu nhất. Thực tế khi nộp thì test lâu nhất dao động 0.5 – 0.6 giây, vì nhiều lúc server đang chạy nhiều hoạt động.

Vậy với cách trên thì ta đã AC được bài này rồi.

Bản gốc của subtask 4 chỉ có C++, mình viết thêm bản Python cùng ý tưởng (nén số bằng dict, lưu lưới vào mảng 1 chiều có viền 0 để chạy nhanh hơn). Mình đã kiểm tra tất cả các bản code với 600 bảng ngẫu nhiên so với cách BFS trâu, đều cho cùng kết quả. Với bảng $1000 \times 1000$ toàn số phân biệt, bản C++ subtask 4 chạy khoảng 0.4 giây, còn bản Python khoảng 2.4 giây (bảng có nhiều số trùng thì nhanh hơn nhiều vì dịch chuyển tức thời tới đích sớm).

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 😇

Nhận xét