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 →

Trò chơi 4: Game 2048 trong C++ và Python: mảng 2 chiều, trượt và gộp một hàng, xoay ma trận cho 4 hướng, máy tự chơi bằng tham lam

Trò chơi 4: Game 2048 trong C++ và Python: mảng 2 chiều, trượt và gộp một hàng, xoay ma trận cho 4 hướng, máy tự chơi bằng tham lam
Series: Thuật toán qua trò chơi Độ khó: Trung bình Trò chơi 4 / Thuật toán qua trò chơi Mảng 2 chiều Mô phỏng
// Thuật toán qua trò chơi · Trò chơi 4
Game 2048: trượt, gộp và để máy tự chơi
Bốn phím mũi tên, một bàn 4 × 4 và những con số nhân đôi: 2048 là trò chơi “gây nghiện” nổi tiếng nhất thế giới di động. Hôm nay em sẽ tự tay lập trình nó bằng mảng 2 chiều, rồi dạy máy tính tự chơi!

Lời mở đầu

Năm 2014, một bạn sinh viên 19 tuổi người Ý tên Gabriele Cirulli viết trò 2048 trong một dịp cuối tuần, đăng lên mạng cho vui… và chỉ sau một tuần đã có hàng triệu người chơi. Luật chơi gói gọn trong ba câu:

  • Bàn 4 × 4, mỗi ô trống hoặc chứa một lũy thừa của 2. Mỗi lượt em bấm một trong 4 hướng, mọi ô trượt hết cỡ về hướng đó.
  • Hai ô cùng số va vào nhau thì gộp thành một ô gấp đôi, và em được cộng điểm bằng đúng số mới. Mỗi ô chỉ được gộp một lần trong một lượt.
  • Sau mỗi lượt có thay đổi, một ô mới (thường là 2, đôi khi là 4) hiện ra ở một ô trống ngẫu nhiên. Tạo được ô 2048 là thắng; bàn đầy mà không gộp được nữa là thua.
224444882244841622trướcsau khi bấm ←+28 điểmViền xanh: ô vừa được gộp. Hàng 2: 4 4 4 → 8 4 (chỉ gộp cặp sát mép trái).

Hình 1. Bấm ← trên bàn bên trái: hàng 1 có 2 2 gộp thành 4; hàng 2 có ba số 4 thì chỉ cặp sát mép trái gộp thành 8, số 4 còn lại đứng cạnh; hàng 3 có hai số 8 cách một ô trống vẫn gộp được. Tổng cộng +28 điểm.

Trông thì đơn giản, nhưng khi tự viết code, rất nhiều bạn ra kết quả sai ở đúng những hàng như 2 2 4 hay 4 4 4. Bài này dạy em làm chuẩn từng bước: nếu cần ôn mảng 2 chiều, em xem lại bài 7: Mảng 2 chiều; kỹ thuật dồn phần tử bằng hai chỉ số ở bài 17: Hai con trỏ; các phép xoay, lật, chuyển vị ma trận ở chuyên đề 4: Mô phỏng trên lưới; còn chiến thuật cho máy chơi dựa trên bài 18: Tham lam.

Trong bài này bạn sẽ học: biểu diễn bàn 2048 bằng mảng 2 chiều; thuật toán trượt và gộp một hàng bằng hai con trỏ trong $O(n)$ và lỗi kinh điển gộp hai lần (2 2 4 thành 8); chỉ viết một hàm trượt trái rồi dùng chuyển vị, đảo hàng để làm đủ 4 hướng; kiểm tra nước đi hợp lệ, sinh ô mới bằng hạt giống để ván chơi tái lập được, kiểm tra hết nước đi; viết máy tự chơi bằng hàm đánh giá bàn (số ô trống, ô lớn ở góc) và tham lam nhìn trước 2 nước. Có khung CHƠI THỬ game 2048 thật, 3 mô phỏng bấm từng bước, 2 khung chạy tay và 8 bài tập kèm gợi ý.

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

Phần 1Chơi thử trước đã!

Học lập trình một trò chơi thì phải chơi nó trước đã. Khung dưới đây là game 2048 thật, viết đúng bằng các hàm em sẽ học trong bài. Trên máy tính, em bấm vào bàn rồi dùng phím mũi tên (hoặc W, A, S, D); trên điện thoại thì vuốt trên bàn hoặc bấm 4 nút mũi tên. Bí quá thì bấm “💡 Máy gợi ý hướng đi”, hoặc để máy đi giúp một nước.

Thanh điều khiển bên dưới bàn dùng để xem máy tự chơi cả ván: bấm Tiếp ▶ để máy đi từng nước kèm lời giải thích, Tự chạy để xem liền một mạch, ⏭ để nhảy tới cuối ván. Dừng ở bước nào cũng được, em có thể tự chơi tiếp từ chính bàn đó. Ô Hạt giống quyết định vị trí các ô mới: cùng hạt giống thì cùng một ván, nên em có thể rủ bạn thi đấu trên cùng một ván xem ai điểm cao hơn.

MÔ PHỎNGCHƠI THỬ: game 2048 ngay trên trangPhím mũi tên / W A S D, vuốt trên bàn, hoặc bấm nút. Bấm Tiếp ▶ để xem máy tự chơi từng nước.
viền xanh: ô mới xuất hiệnnút vàng: hướng máy gợi ý

Chơi vài ván em sẽ để ý: muốn đi xa thì phải giữ nhiều ô trống và dồn ô lớn nhất vào một góc, rồi xếp các ô nhỏ dần ra xa góc đó. Đây chính là hai ý mà phần 5 dùng để dạy máy chơi. Bây giờ ta “mổ xẻ” xem chương trình phải làm gì sau mỗi lần em bấm phím:

BướcViệc cần làmKỹ thuậtỞ phần
1Trượt và gộp từng hàng (hoặc từng cột) theo hướng bấmhai con trỏ trên mảng 1 chiều2
2Làm được cả 4 hướng mà không viết 4 lần codechuyển vị, đảo hàng ma trận3
3Bàn không đổi thì bỏ qua; có đổi thì cộng điểm, sinh ô mớiso sánh 2 mảng, số ngẫu nhiên4
4Kiểm tra còn nước đi hay đã thuaduyệt ô kề trên lưới4
5(Máy chơi) thử mọi hướng, chấm điểm bàn, chọn hướng tốt nhấttham lam, nhìn trước5

Bảng 1. Một lượt chơi 2048 gồm những việc gì. Cả trò chơi chỉ là mảng 2 chiều cộng với vài vòng lặp!

Biểu diễn bàn chơi
Bàn 4 × 4 là một mảng 2 chiều a[4][4] (C++) hoặc danh sách các danh sách (Python). Ô trống lưu số 0, ô có số lưu đúng giá trị. Ta đánh số hàng $i$ từ trên xuống, cột $j$ từ trái sang, bắt đầu từ 0, giống bài 7. Ô lớn nhất trên bàn 4 × 4 về lý thuyết có thể tới $2^{17} = 131072$, nên kiểu int là đủ cho bàn chơi; nhưng tổng điểm và các bài tập với bàn lớn thì nên dùng long long.

Phần 2Trượt và gộp một hàng

Ta bắt đầu từ bài toán nhỏ nhất: một hàng bấm sang trái. Nhìn kỹ luật chơi, mỗi lượt gồm hai việc xen kẽ nhau:

  • Trượt: các ô có số dồn sát về bên trái, giữ nguyên thứ tự. Nói cách khác là bỏ hết các số 0.
  • Gộp: đi từ trái sang phải, hai số đứng liền nhau sau khi bỏ số 0 mà bằng nhau thì gộp thành một số gấp đôi. Ô vừa được tạo ra không được gộp tiếp trong lượt đó.

Ta dùng hai con trỏ giống bài toán “dồn các số khác 0 về đầu mảng” ở bài 17: con trỏ i đọc từng ô của hàng cũ a, con trỏ k là vị trí sẽ ghi tiếp theo trong hàng mới b. Thêm một biến cờ canMerge cho biết ô cuối cùng b[k − 1] có còn được gộp không. Với mỗi a[i] khác 0:

  • nếu canMerge đúng và b[k − 1] == a[i]: gộp, b[k − 1] nhân đôi, cộng điểm, rồi khoá lại (canMerge = false);
  • ngược lại: đặt a[i] vào b[k], tăng k, và mở cờ (canMerge = true) vì ô mới này còn chờ được gộp.

Em bấm Tiếp để xem hai con trỏ làm việc. Sau đó đổi sang chế độ “Cài sai: quên khoá” với hàng 2 2 4 để thấy lỗi kinh điển tận mắt:

MÔ PHỎNGTrượt và gộp một hàng bằng hai con trỏNhập một hàng (0 là ô trống), chọn cài đúng hoặc cài sai rồi bấm Tiếp / Tự chạy.
ô đang đọc (i)ô vừa gộpđã đặt vào bô bị gộp saiđã đọc / còn trống
Hàng trước khi bấm ←Cài đúng (mỗi ô gộp tối đa 1 lần)Cài sai (quên khoá ô vừa gộp)
2 2 4 04 4 0 0 (+4)8 0 0 0 (+12)
2 2 2 24 4 0 0 (+8)4 4 0 0 (+8)
4 4 4 08 4 0 0 (+8)8 4 0 0 (+8)
4 4 8 88 16 0 0 (+24)16 8 0 0 (+24)
8 0 0 816 0 0 0 (+16)16 0 0 0 (+16)
2 4 8 162 4 8 16 (+0)2 4 8 16 (+0)
2 2 4 84 4 8 0 (+4)16 0 0 0 (+28)

Bảng 2. Cài sai cho kết quả khác ở mọi hàng mà một ô vừa gộp lại bằng ô kế tiếp. Hàng 2 2 4 8 thậm chí bị “gộp dây chuyền” thành một ô 16!

Lỗi kinh điển: gộp hai lần trong một lượt
Với hàng 2 2 4, cặp 2 2 gộp thành 4. Nếu không khoá ô 4 vừa tạo, nó lại thấy số 4 kế tiếp bằng mình và gộp tiếp thành 8. Game thật cho ra 4 4 0, vì luật nói mỗi ô chỉ gộp tối đa 1 lần mỗi lượt. Lỗi thứ hai hay gặp là gộp sai thứ tự: hàng 4 4 4 bấm ← phải ra 8 4 (cặp sát mép đích gộp trước), không phải 4 8. Duyệt từ mép đích đi ra như code dưới đây thì tự động đúng cả hai.
Xem code C++ / Python: trượt và gộp một hàng sang trái (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

// Trượt và gộp một hàng sang TRÁI theo luật 2048, trả về số điểm được cộng
long long slideLeft(vector<long long> &a){
    int n = a.size(), k = 0;              // k: ô tiếp theo sẽ ghi trong hàng mới b
    vector<long long> b(n, 0);
    bool canMerge = false;                // ô b[k - 1] còn được gộp nữa không?
    long long gain = 0;
    for(int i = 0; i < n; i++){
        if(a[i] == 0) continue;           // bỏ qua ô trống = trượt
        if(canMerge && b[k - 1] == a[i]){
            b[k - 1] *= 2;                // gộp hai ô bằng nhau
            gain += b[k - 1];
            canMerge = false;             // ô vừa gộp bị khoá trong lượt này
        } else {
            b[k++] = a[i];                // đặt sát bên trái
            canMerge = true;
        }
    }
    a = b;
    return gain;
}

int main(){
    int n;
    cin >> n;
    vector<long long> a(n);
    for(auto &x : a) cin >> x;
    long long g = slideLeft(a);
    for(int i = 0; i < n; i++) cout << a[i] << " ";
    cout << "\n" << g;
    return 0;
}
import sys

def slide_left(a):
    """Trượt và gộp một hàng sang TRÁI, trả về (hàng mới, điểm cộng)."""
    b, gain, can_merge = [], 0, False
    for v in a:
        if v == 0:                       # bỏ qua ô trống = trượt
            continue
        if can_merge and b[-1] == v:
            b[-1] *= 2                   # gộp hai ô bằng nhau
            gain += b[-1]
            can_merge = False            # ô vừa gộp bị khoá trong lượt này
        else:
            b.append(v)                  # đặt sát bên trái
            can_merge = True
    return b + [0] * (len(a) - len(b)), gain

data = sys.stdin.read().split()
n = int(data[0])
row, g = slide_left(list(map(int, data[1:n + 1])))
print(*row)
print(g)

Bây giờ chạy tay từng dòng code C++. Em thử cả dữ liệu “bẫy” 2 2 4 để thấy dòng canMerge = false cứu ta như thế nào:

C++▶ Chạy tay từng dòng codeHai con trỏ: i đọc hàng cũ a, k là chỗ ghi tiếp theo của hàng mới b; canMerge cho biết b[k − 1] còn được gộp không.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4int main(){
5 int n;
6 cin >> n;
7 vector<long long> a(n), b(n, 0);
8 for(int i = 0; i < n; i++) cin >> a[i];
9 int k = 0;
10 bool canMerge = false;
11 long long gain = 0;
12 for(int i = 0; i < n; i++){
13 if(a[i] == 0) continue;
14 if(canMerge && b[k - 1] == a[i]){
15 b[k - 1] *= 2; gain += b[k - 1];
16 canMerge = false;
17 } else {
18 b[k] = a[i]; k++;
19 canMerge = true;
20 }
21 }
22 for(int i = 0; i < n; i++) cout << b[i] << " ";
23 cout << "\n" << gain;
24 return 0;
25}

Độ phức tạp. Mỗi ô của hàng cũ được đọc đúng một lần và mỗi ô của hàng mới được ghi tối đa hai lần, nên trượt một hàng dài $n$ tốn $O(n)$. Cả bàn $n \times n$ có $n$ hàng, một lượt chơi tốn $O(n^2)$, nghĩa là với bàn 4 × 4 chỉ khoảng vài chục phép tính. Máy tính làm được hàng triệu lượt mỗi giây, đủ để ở phần 5 cho máy “thử trước” rất nhiều nước.

Phần 3Một hàm trượt trái cho cả 4 hướng

Có hàm trượt trái rồi, các hướng còn lại thì sao? Cách “cục súc” là viết thêm 3 hàm gần giống hệt nhau, chỉ khác chiều vòng lặp. Cách đó dài và rất dễ sai một dấu cộng trừ. Mẹo hay hơn: biến đổi bàn sao cho hướng cần đi trở thành hướng trái, trượt trái, rồi biến đổi ngược lại. Hai phép biến đổi ta cần đều đã gặp ở chuyên đề 4 (phần xoay, lật ma trận):

  • Đảo từng hàng (lật ngang): mép phải thành mép trái. Đi phải = đảo hàng → trượt trái → đảo lại.
  • Chuyển vị $b[j][i] = a[i][j]$: cột thành hàng, đỉnh cột thành đầu hàng. Đi lên = chuyển vị → trượt trái → chuyển vị lại.
  • Đi xuống = chuyển vị, rồi đảo hàng (đáy cột thành đầu hàng) → trượt trái → làm ngược lại theo thứ tự ngược: đảo hàng, rồi chuyển vị.
Hướng bấmBiến đổi trướcTrượtBiến đổi sauMép đích thành
← Trái(không)trái từng hàng(không)đầu hàng
→ Phảiđảo từng hàngtrái từng hàngđảo từng hàngđầu hàng (sau khi đảo)
↑ Lênchuyển vịtrái từng hàngchuyển vịđầu hàng = đỉnh cột
↓ Xuốngchuyển vị, rồi đảo hàngtrái từng hàngđảo hàng, rồi chuyển vịđầu hàng = đáy cột

Bảng 3. Mỗi phép biến đổi đều tự là nghịch đảo của chính nó (làm hai lần thì về như cũ), nên “biến đổi sau” chỉ là làm lại các bước “biến đổi trước” theo thứ tự ngược.

Vì sao phải đảo hàng chứ không trượt sang phải luôn cho xong? Vì luật gộp ưu tiên các ô gần mép đích trước: hàng 2 2 2 bấm → phải ra 0 2 4. Sau khi đảo, mép đích nằm ở đầu hàng, hàm trượt trái duyệt từ đầu hàng nên tự động gộp đúng cặp. Em xem từng bước với hướng Lên (mặc định), rồi thử Xuống:

MÔ PHỎNGMột hàm trượt trái cho cả 4 hướngNhập bàn n × n (theo từng hàng), chọn hướng rồi bấm Tiếp để xem biến đổi → trượt → biến đổi ngược.
hàng đang trượthàng đã trượtô trống
HướngBàn sau khi đi (từ bàn mặc định của mô phỏng)Điểm
← Trái4 4 0 0
2 8 4 0
4 16 0 0
4 4 0 0
+36
↑ Lên4 8 2 8
2 2 4 8
0 0 8 2
0 0 2 0
+20
→ Phải0 0 4 4
0 2 4 8
0 0 4 16
0 0 4 4
+36
↓ Xuống0 0 2 0
0 0 4 8
2 8 8 8
4 2 2 2
+20

Bảng 4. Bốn nước đi khác nhau từ cùng một bàn 2 0 2 4 / 2 4 4 4 / 0 4 8 8 / 2 2 2 2. Em tự kiểm tra lại vài cột bằng tay nhé.

Xem code C++ / Python: một nước đi 2048 theo 4 hướng bằng chuyển vị và đảo hàng (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

int n;
vector<vector<long long>> a;

long long slideLeft(vector<long long> &row){      // như phần 2
    int k = 0;
    vector<long long> b(n, 0);
    bool canMerge = false;
    long long gain = 0;
    for(int i = 0; i < n; i++){
        if(row[i] == 0) continue;
        if(canMerge && b[k - 1] == row[i]){ b[k - 1] *= 2; gain += b[k - 1]; canMerge = false; }
        else { b[k++] = row[i]; canMerge = true; }
    }
    row = b;
    return gain;
}

void reverseRows(){                                // lật ngang: đảo từng hàng
    for(auto &r : a) reverse(r.begin(), r.end());
}

void transpose(){                                  // chuyển vị: a[i][j] <-> a[j][i]
    for(int i = 0; i < n; i++)
        for(int j = i + 1; j < n; j++) swap(a[i][j], a[j][i]);
}

long long moveLeft(){
    long long g = 0;
    for(auto &r : a) g += slideLeft(r);
    return g;
}

long long doMove(char d){
    long long g;
    if(d == 'L') g = moveLeft();
    else if(d == 'R'){ reverseRows(); g = moveLeft(); reverseRows(); }
    else if(d == 'U'){ transpose(); g = moveLeft(); transpose(); }
    else { transpose(); reverseRows(); g = moveLeft(); reverseRows(); transpose(); }   // 'D'
    return g;
}

int main(){
    cin >> n;
    a.assign(n, vector<long long>(n));
    for(auto &r : a) for(auto &x : r) cin >> x;
    char d;
    cin >> d;
    long long g = doMove(d);
    for(auto &r : a){
        for(auto x : r) cout << x << " ";
        cout << "\n";
    }
    cout << g;
    return 0;
}
import sys

def slide_left(a):                                # như phần 2
    b, gain, can_merge = [], 0, False
    for v in a:
        if v == 0:
            continue
        if can_merge and b[-1] == v:
            b[-1] *= 2; gain += b[-1]; can_merge = False
        else:
            b.append(v); can_merge = True
    return b + [0] * (len(a) - len(b)), gain

def transpose(b):
    return [list(col) for col in zip(*b)]

def reverse_rows(b):
    return [r[::-1] for r in b]

def move_left(b):
    res, g = [], 0
    for r in b:
        x, y = slide_left(r)
        res.append(x); g += y
    return res, g

def do_move(b, d):
    if d == 'L':
        return move_left(b)
    if d == 'R':
        x, g = move_left(reverse_rows(b)); return reverse_rows(x), g
    if d == 'U':
        x, g = move_left(transpose(b)); return transpose(x), g
    x, g = move_left(reverse_rows(transpose(b)))  # 'D'
    return transpose(reverse_rows(x)), g

data = sys.stdin.read().split()
n = int(data[0])
b = [list(map(int, data[1 + i * n:1 + (i + 1) * n])) for i in range(n)]
b, g = do_move(b, data[1 + n * n])
for r in b:
    print(*r)
print(g)
Khuôn mẫu “biến đổi – làm – biến đổi ngược”
long long doMove(char d){
    if(d == 'U' || d == 'D') transpose();     // cột thành hàng
    if(d == 'R' || d == 'D') reverseRows();   // mép đích về đầu hàng
    long long g = moveLeft();                  // chỉ một hàm duy nhất!
    if(d == 'R' || d == 'D') reverseRows();   // làm ngược lại...
    if(d == 'U' || d == 'D') transpose();     // ...theo thứ tự ngược
    return g;
}
Mẹo này dùng được cho rất nhiều bài lưới khác: chỉ giải cho một hướng (hay một góc, một cạnh), rồi xoay, lật dữ liệu để quy các trường hợp còn lại về trường hợp đã giải. Code ngắn hơn, ít chỗ sai hơn.
Chuyển vị tại chỗ chỉ đúng với ma trận vuông
Hàm transpose() đổi chỗ a[i][j] với a[j][i] cho mọi j > i. Nếu em cho j chạy từ 0 thì mỗi cặp bị đổi hai lần và ma trận… y nguyên như cũ! Bàn 2048 luôn vuông nên chuyển vị tại chỗ được; với lưới $n \times m$ không vuông thì phải tạo mảng mới kích thước $m \times n$ như chuyên đề 4.

Phần 4Ô mới, điểm số và kiểm tra hết nước đi

Có hàm đi 4 hướng rồi, ghép thành trò chơi hoàn chỉnh chỉ còn ba việc nhỏ.

1. Nước đi hợp lệ. Nếu bấm một hướng mà bàn không thay đổi (chẳng hạn mọi ô đã dồn sát trái mà em vẫn bấm ←), lượt đó không được tính và không sinh ô mới. Cách kiểm tra đơn giản nhất: lưu bản sao bàn trước khi đi rồi so sánh với bàn sau khi đi. Trong C++, hai vector so sánh được bằng ==, mảng tĩnh thì dùng memcmp; trong Python, hai danh sách so sánh bằng ==.

2. Sinh ô mới. Gom chỉ số các ô trống vào một danh sách, chọn ngẫu nhiên một phần tử, đặt vào đó số 2 với xác suất 90% hoặc số 4 với xác suất 10%. Máy tính không có “ngẫu nhiên thật”: nó dùng công thức sinh ra dãy số trông như ngẫu nhiên, bắt đầu từ một số gọi là hạt giống (seed). Bài này dùng công thức rất gọn $s \leftarrow s \cdot 48271 \bmod (2^{31} - 1)$. Cùng hạt giống thì cùng dãy số, nên ván chơi tái lập được: rất quý khi gỡ lỗi hay khi chấm bài tự động.

3. Hết nước đi. Bàn luôn có ít nhất một ô có số, nên còn đi được khi và chỉ khi bàn còn ô trống (khi đó luôn có một ô có số đứng cạnh ô trống và trượt vào được), hoặc có hai ô kề nhau bằng nhau (kề ngang thì bấm trái/phải sẽ gộp, kề dọc thì bấm lên/xuống sẽ gộp). Mẹo nhỏ: mỗi cặp ô kề chỉ cần xét một lần, nên với mỗi ô ta chỉ so với ô bên phải và ô bên dưới, khỏi dùng đủ 4 hướng như ở chuyên đề 4.

C++▶ Chạy tay từng dòng codeDuyệt từng ô: ô trống, hoặc bằng ô bên phải, hoặc bằng ô bên dưới thì còn nước đi.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4int a[105][105];
5
6int main(){
7 int n;
8 cin >> n;
9 for(int i = 0; i < n; i++)
10 for(int j = 0; j < n; j++) cin >> a[i][j];
11 for(int i = 0; i < n; i++){
12 for(int j = 0; j < n; j++){
13 if(a[i][j] == 0){ cout << "YES"; return 0; }
14 if(j + 1 < n && a[i][j] == a[i][j + 1]){ cout << "YES"; return 0; }
15 if(i + 1 < n && a[i][j] == a[i + 1][j]){ cout << "YES"; return 0; }
16 }
17 }
18 cout << "NO";
19 return 0;
20}
Xem code C++ / Python: kiểm tra còn nước đi hay không (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

int a[505][505];

int main(){
    int n;
    cin >> n;
    for(int i = 0; i < n; i++)
        for(int j = 0; j < n; j++) cin >> a[i][j];
    for(int i = 0; i < n; i++)
        for(int j = 0; j < n; j++){
            if(a[i][j] == 0){ cout << "YES"; return 0; }                            // còn ô trống
            if(j + 1 < n && a[i][j] == a[i][j + 1]){ cout << "YES"; return 0; }     // gộp được với ô bên phải
            if(i + 1 < n && a[i][j] == a[i + 1][j]){ cout << "YES"; return 0; }     // gộp được với ô bên dưới
        }
    cout << "NO";
    return 0;
}
import sys

def can_move(b):
    n = len(b)
    for i in range(n):
        for j in range(n):
            if b[i][j] == 0:                                  # còn ô trống
                return True
            if j + 1 < n and b[i][j] == b[i][j + 1]:          # gộp được với ô bên phải
                return True
            if i + 1 < n and b[i][j] == b[i + 1][j]:          # gộp được với ô bên dưới
                return True
    return False

data = sys.stdin.read().split()
n = int(data[0])
b = [list(map(int, data[1 + i * n:1 + (i + 1) * n])) for i in range(n)]
print("YES" if can_move(b) else "NO")

Ghép tất cả lại, ta được game 2048 chạy trên màn hình console. Chương trình đọc hạt giống, rồi đọc từng ký tự L, U, R, D như những lần bấm phím. Cuối cùng in bàn, điểm, số nước đã đi và trạng thái. Vì dùng cùng công thức sinh số và cùng cách chọn ô trống (đánh số ô theo hàng, từ trái sang phải) với khung CHƠI THỬ, nên em nhập hạt giống 2048 rồi bấm ← ↑ ← ↑ trong khung CHƠI THỬ sẽ thấy đúng bàn mà chương trình in ra với dữ liệu mẫu đầu tiên!

Xem code C++ / Python: game 2048 hoàn chỉnh chạy trên console (hạt giống + chuỗi phím) (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

const int N = 4;
long long a[N][N], seed;

long long rnd(){                          // sinh số giả ngẫu nhiên: cùng seed thì cùng dãy số
    seed = seed * 48271 % 2147483647;
    return seed;
}

void spawn(){                             // ô mới: 2 (90%) hoặc 4 (10%) ở một ô trống ngẫu nhiên
    vector<int> e;
    for(int p = 0; p < N * N; p++) if(a[p / N][p % N] == 0) e.push_back(p);
    if(e.empty()) return;
    int p = e[rnd() % e.size()];
    a[p / N][p % N] = (rnd() % 10 == 0) ? 4 : 2;
}

long long slideLeft(long long r[]){
    long long b[N] = {0}, gain = 0;
    int k = 0;
    bool canMerge = false;
    for(int i = 0; i < N; i++){
        if(r[i] == 0) continue;
        if(canMerge && b[k - 1] == r[i]){ b[k - 1] *= 2; gain += b[k - 1]; canMerge = false; }
        else { b[k++] = r[i]; canMerge = true; }
    }
    for(int i = 0; i < N; i++) r[i] = b[i];
    return gain;
}
void reverseRows(){ for(int i = 0; i < N; i++) reverse(a[i], a[i] + N); }
void transpose(){ for(int i = 0; i < N; i++) for(int j = i + 1; j < N; j++) swap(a[i][j], a[j][i]); }
long long moveLeft(){ long long g = 0; for(int i = 0; i < N; i++) g += slideLeft(a[i]); return g; }

long long doMove(char d){
    long long g;
    if(d == 'L') g = moveLeft();
    else if(d == 'R'){ reverseRows(); g = moveLeft(); reverseRows(); }
    else if(d == 'U'){ transpose(); g = moveLeft(); transpose(); }
    else { transpose(); reverseRows(); g = moveLeft(); reverseRows(); transpose(); }
    return g;
}

bool canMove(){
    for(int i = 0; i < N; i++)
        for(int j = 0; j < N; j++)
            if(a[i][j] == 0 || (j + 1 < N && a[i][j] == a[i][j + 1]) || (i + 1 < N && a[i][j] == a[i + 1][j])) return true;
    return false;
}

int main(){
    cin >> seed;
    spawn(); spawn();                     // bàn mở đầu có 2 ô
    long long score = 0, moves = 0;
    char c;
    while(cin >> c){                      // mỗi ký tự L, U, R, D là một lần bấm phím
        if(!canMove()) break;
        long long old[N][N];
        memcpy(old, a, sizeof a);
        long long g = doMove(c);
        if(memcmp(old, a, sizeof a) == 0) continue;   // bàn không đổi: không tính nước, không sinh ô
        score += g; moves++;
        spawn();
    }
    for(int i = 0; i < N; i++){
        for(int j = 0; j < N; j++) cout << a[i][j] << " ";
        cout << "\n";
    }
    cout << score << " " << moves << "\n" << (canMove() ? "CONTINUE" : "GAME OVER");
    return 0;
}
import sys

N = 4
seed = 0

def rnd():                                # sinh số giả ngẫu nhiên: cùng seed thì cùng dãy số
    global seed
    seed = seed * 48271 % 2147483647
    return seed

def spawn(b):                             # ô mới: 2 (90%) hoặc 4 (10%) ở một ô trống ngẫu nhiên
    e = [p for p in range(N * N) if b[p // N][p % N] == 0]
    if e:
        p = e[rnd() % len(e)]
        b[p // N][p % N] = 4 if rnd() % 10 == 0 else 2

def slide_left(a):
    b, gain, can_merge = [], 0, False
    for v in a:
        if v == 0:
            continue
        if can_merge and b[-1] == v:
            b[-1] *= 2; gain += b[-1]; can_merge = False
        else:
            b.append(v); can_merge = True
    return b + [0] * (len(a) - len(b)), gain

def transpose(b): return [list(c) for c in zip(*b)]
def reverse_rows(b): return [r[::-1] for r in b]

def move_left(b):
    res, g = [], 0
    for r in b:
        x, y = slide_left(r); res.append(x); g += y
    return res, g

def do_move(b, d):
    if d == 'L': return move_left(b)
    if d == 'R':
        x, g = move_left(reverse_rows(b)); return reverse_rows(x), g
    if d == 'U':
        x, g = move_left(transpose(b)); return transpose(x), g
    x, g = move_left(reverse_rows(transpose(b)))
    return transpose(reverse_rows(x)), g

def can_move(b):
    for i in range(N):
        for j in range(N):
            if b[i][j] == 0 or (j + 1 < N and b[i][j] == b[i][j + 1]) or (i + 1 < N and b[i][j] == b[i + 1][j]):
                return True
    return False

data = sys.stdin.read().split()
seed = int(data[0])
b = [[0] * N for _ in range(N)]
spawn(b); spawn(b)                        # bàn mở đầu có 2 ô
score = moves = 0
for c in ''.join(data[1:]):               # mỗi ký tự L, U, R, D là một lần bấm phím
    if not can_move(b):
        break
    nb, g = do_move(b, c)
    if nb == b:                           # bàn không đổi: không tính nước, không sinh ô
        continue
    b = nb; score += g; moves += 1
    spawn(b)
for r in b:
    print(*r)
print(score, moves)
print("CONTINUE" if can_move(b) else "GAME OVER")
Muốn chơi thật trên console?
Chỉ cần in bàn ra sau mỗi lần bấm phím (đưa đoạn in bàn vào trong vòng lặp), và thay hạt giống cố định bằng thời gian hiện tại: seed = time(0) % 2147483646 + 1; (C++) hoặc seed = int(time.time()) % 2147483646 + 1 (Python). Hạt giống phải khác 0, vì $0 \cdot 48271$ mãi mãi bằng 0. Còn khi đi thi, đề thường cho sẵn vị trí ô mới (bài tập 4), để kết quả là duy nhất.

Phần 5Dạy máy tự chơi: hàm đánh giá và tham lam nhìn trước

Máy tính không “nhìn” được bàn như người. Ta phải biến cảm giác “bàn này đẹp” thành một con số, gọi là hàm đánh giá. Từ kinh nghiệm chơi ở phần 1, bàn tốt là bàn:

  • có nhiều ô trống: còn chỗ cho ô mới, khó bị thua. Mỗi ô trống cho 10 điểm.
  • có ô lớn nhất nằm ở góc (ở đây chọn góc trái trên): các ô lớn không chắn đường nhau. Thưởng thêm đúng bằng giá trị ô lớn nhất nếu nó ở góc.

$$\text{điểm bàn} = 10 \times (\text{số ô trống}) + (\text{ô lớn nhất, nếu nó ở góc trái trên; ngược lại } 0)$$

Tham lam nhìn trước 1 nước (như bài 18): thử cả 4 hướng; hướng nào làm bàn không đổi thì bỏ; với các hướng còn lại, điểm của hướng = điểm gộp được + điểm bàn thu được. Chọn hướng điểm cao nhất, bằng nhau thì ưu tiên theo thứ tự ← ↑ → ↓ (để ô lớn dần dồn về góc trái trên). Máy chỉ cần 4 lần thử cho mỗi nước!

Nhìn trước 2 nước: với mỗi hướng thứ nhất, máy thử tiếp cả 4 hướng thứ hai và lấy kết quả tốt nhất. Tổng cộng $4 \times 4 = 16$ lần thử, vẫn nhanh như chớp. Ở đây ta bỏ qua ô mới sinh ra giữa hai nước cho đơn giản. Em xem máy chấm điểm từng hướng trên một bàn cụ thể:

MÔ PHỎNGMáy chấm điểm từng hướng rồi chọn hướng tốt nhấtNhập bàn (theo từng hàng), chọn nhìn trước 1 hay 2 nước rồi bấm Tiếp.
bàn sau nước thứ nhấtbàn sau nước thứ hai / hướng được chọnhướng không đi được
HướngGộpÔ trống sau đóThưởng gócĐiểm (nhìn 1 nước)Điểm (nhìn 2 nước)
← Trái+8664132334
↑ Lên+1286128316316
→ Phải+8664132172
↓ Xuống+12860188316

Bảng 5. Bàn mặc định của mô phỏng: 64 16 8 2 / 64 8 4 0 / 4 4 2 0 / 2 0 0 0. Nhìn 1 nước, máy chọn ↑ Lên vì gộp ngay hai ô 64 vào góc. Nhìn 2 nước, máy phát hiện đi ← Trái trước (gộp 4 4 ở hàng thứ ba) rồi mới đi lên vẫn gộp được hai ô 64, lại còn thêm ô trống, nên chọn ← Trái.

Xem code C++ / Python: máy chọn hướng đi bằng tham lam nhìn trước 2 nước (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

typedef vector<vector<long long>> Board;
int n;
const string DIRS = "LURD";

long long slideLeft(vector<long long> &row){
    int k = 0;
    vector<long long> b(n, 0);
    bool canMerge = false;
    long long gain = 0;
    for(int i = 0; i < n; i++){
        if(row[i] == 0) continue;
        if(canMerge && b[k - 1] == row[i]){ b[k - 1] *= 2; gain += b[k - 1]; canMerge = false; }
        else { b[k++] = row[i]; canMerge = true; }
    }
    row = b;
    return gain;
}
void reverseRows(Board &a){ for(auto &r : a) reverse(r.begin(), r.end()); }
void transpose(Board &a){ for(int i = 0; i < n; i++) for(int j = i + 1; j < n; j++) swap(a[i][j], a[j][i]); }

long long doMove(Board &a, char d){       // đi theo hướng d trên bản sao a, trả về điểm gộp
    if(d == 'U' || d == 'D') transpose(a);
    if(d == 'R' || d == 'D') reverseRows(a);
    long long g = 0;
    for(auto &r : a) g += slideLeft(r);
    if(d == 'R' || d == 'D') reverseRows(a);
    if(d == 'U' || d == 'D') transpose(a);
    return g;
}

long long evalBoard(const Board &a){      // 10 x số ô trống + thưởng nếu ô lớn nhất ở góc trái trên
    long long e = 0, mx = 0;
    for(auto &r : a) for(auto x : r){ if(x == 0) e++; mx = max(mx, x); }
    return 10 * e + (a[0][0] == mx ? mx : 0);
}

int main(){
    cin >> n;
    Board a(n, vector<long long>(n));
    for(auto &r : a) for(auto &x : r) cin >> x;
    long long bestScore = -1;
    char best = '?';
    for(char d : DIRS){
        Board b1 = a;
        long long g1 = doMove(b1, d);
        if(b1 == a){ cout << -1 << " "; continue; }        // hướng không hợp lệ
        long long s2 = -1;                                  // nhìn trước thêm 1 nước
        for(char d2 : DIRS){
            Board b2 = b1;
            long long g2 = doMove(b2, d2);
            if(b2 != b1) s2 = max(s2, g2 + evalBoard(b2));
        }
        long long s = g1 + (s2 >= 0 ? s2 : evalBoard(b1));
        cout << s << " ";
        if(s > bestScore){ bestScore = s; best = d; }       // bằng điểm: giữ hướng đứng trước
    }
    cout << "\n";
    if(best == '?') cout << "NONE";
    else cout << best;
    return 0;
}
import sys

def slide_left(a):
    b, gain, can_merge = [], 0, False
    for v in a:
        if v == 0:
            continue
        if can_merge and b[-1] == v:
            b[-1] *= 2; gain += b[-1]; can_merge = False
        else:
            b.append(v); can_merge = True
    return b + [0] * (len(a) - len(b)), gain

def do_move(b, d):                        # trả về (bàn mới, điểm gộp), không sửa b
    t = b
    if d in 'UD': t = [list(c) for c in zip(*t)]
    if d in 'RD': t = [r[::-1] for r in t]
    res, g = [], 0
    for r in t:
        x, y = slide_left(r); res.append(x); g += y
    if d in 'RD': res = [r[::-1] for r in res]
    if d in 'UD': res = [list(c) for c in zip(*res)]
    return res, g

def eval_board(b):                        # 10 x số ô trống + thưởng nếu ô lớn nhất ở góc trái trên
    mx = max(max(r) for r in b)
    return 10 * sum(r.count(0) for r in b) + (mx if b[0][0] == mx else 0)

data = sys.stdin.read().split()
n = int(data[0])
a = [list(map(int, data[1 + i * n:1 + (i + 1) * n])) for i in range(n)]
best, best_score, out = 'NONE', -1, []
for d in 'LURD':
    b1, g1 = do_move(a, d)
    if b1 == a:                           # hướng không hợp lệ
        out.append(-1); continue
    s2 = -1                               # nhìn trước thêm 1 nước
    for d2 in 'LURD':
        b2, g2 = do_move(b1, d2)
        if b2 != b1:
            s2 = max(s2, g2 + eval_board(b2))
    s = g1 + (s2 if s2 >= 0 else eval_board(b1))
    out.append(s)
    if s > best_score:                    # bằng điểm: giữ hướng đứng trước
        best, best_score = d, s
print(*out)
print(best)

Chính đoạn code này là “bộ não” của nút 💡 Máy gợi ý và phần phát lại máy chơi trong khung CHƠI THỬ. Thử 40 hạt giống khác nhau, máy nhìn 1 nước thường dừng ở ô 256, còn nhìn 2 nước thường đạt 512, có ván lên tới 1024. Không tệ cho vài chục dòng code! Muốn mạnh hơn nữa, người ta làm theo các hướng sau:

  • Hàm đánh giá tốt hơn: thưởng cho các hàng, cột đơn điệu (giảm dần ra xa góc), phạt các ô kề nhau chênh lệch quá lớn.
  • Tính cả ô mới: sau mỗi nước của máy, xét mọi vị trí ô mới có thể hiện ra (cùng xác suất 2 hay 4) rồi lấy trung bình điểm. Thuật toán này tên là expectimax, họ hàng gần của thuật toán minimax mà ta sẽ học ở trò chơi tiếp theo.
  • Nhìn sâu hơn bằng đệ quy: nhìn trước $k$ nước tốn $4^k$ lần thử, nên phải cân nhắc giữa độ sâu và tốc độ.
Tham lam không phải lúc nào cũng đúng
Tham lam chỉ chọn cái trước mắt tốt nhất, không đảm bảo thắng. Ở 2048 lại càng không có chiến thuật chắc thắng, vì ô mới xuất hiện ngẫu nhiên. Nhưng với các trò chơi mà máy phải “sống sót” lâu, một hàm đánh giá hợp lý cộng với nhìn trước vài nước thường đã chơi giỏi hơn đa số người chơi mới. Đó cũng là ý tưởng nền của các chương trình chơi cờ: đánh giá thế cờ + tìm kiếm vài nước phía trước.
Luyện tập trên web
Các bài dưới đây trên blog đều có lời giải chi tiết bằng C++ và Python, xếp từ dễ tới khó. Em hãy tự làm trước rồi mới xem lời giải nhé.Xem toàn bộ bài cùng chủ đề: nhãn Mảng 2 chiều.

Bài tậpLuyện tập với game 2048

Bài 1Trượt sang phảiDễ

Cho một hàng gồm $n$ ô ($1 \le n \le 2 \cdot 10^5$), mỗi ô là 0 (trống) hoặc một lũy thừa của 2 không quá $2^{30}$. Người chơi bấm phím →. In ra hàng sau khi trượt và gộp theo luật 2048, và ở dòng thứ hai là số điểm được cộng (tổng các số mới tạo thành do gộp).

Input
8
2 2 2 0 4 4 8 0
Output
0 0 0 0 2 4 8 8
12

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Đọc hàng từ phải sang trái và ghi hàng mới cũng từ mép phải vào, hoặc gọn hơn: đảo hàng, trượt trái, đảo lại như phần 3. Trong ví dụ, từ phải sang trái các số khác 0 là 8, 4, 4, 2, 2, 2: số 8 đứng sát mép, hai số 4 gộp thành 8 và bị khoá nên không gộp tiếp với số 8 cũ, hai số 2 gần mép gộp thành 4, số 2 cuối đứng riêng. Điểm cộng là 8 + 4 = 12. Giá trị có thể tới $2^{31}$ nên dùng long long.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n;
    cin >> n;
    vector<long long> a(n), b(n, 0);
    for(auto &x : a) cin >> x;
    int k = n - 1;                        // ghi từ mép phải sang trái
    bool canMerge = false;
    long long gain = 0;
    for(int i = n - 1; i >= 0; i--){      // đọc từ phải sang trái
        if(a[i] == 0) continue;
        if(canMerge && b[k + 1] == a[i]){ b[k + 1] *= 2; gain += b[k + 1]; canMerge = false; }
        else { b[k--] = a[i]; canMerge = true; }
    }
    for(int i = 0; i < n; i++) cout << b[i] << " ";
    cout << "\n" << gain;
    return 0;
}
import sys

def slide_left(a):
    b, gain, can_merge = [], 0, False
    for v in a:
        if v == 0:
            continue
        if can_merge and b[-1] == v:
            b[-1] *= 2; gain += b[-1]; can_merge = False
        else:
            b.append(v); can_merge = True
    return b + [0] * (len(a) - len(b)), gain

data = sys.stdin.buffer.read().split()
n = int(data[0])
a = list(map(int, data[1:n + 1]))
row, g = slide_left(a[::-1])              # đảo hàng, trượt trái, đảo lại
print(*row[::-1])
print(g)
Bài 2Những hướng đi đượcDễ

Cho bàn 2048 kích thước $n \times n$ ($2 \le n \le 500$). Hướng bấm được gọi là đi được nếu bấm hướng đó làm bàn thay đổi. In ra các hướng đi được theo thứ tự L U R D (trái, lên, phải, xuống), cách nhau một dấu cách; nếu không có hướng nào thì in NONE.

Input
3
2 4 8
2 16 8
4 4 32
Output
L U R D

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Không cần mô phỏng cả nước đi. Xét từng hàng (hoặc cột) theo thứ tự từ mép đích đi ra: bàn thay đổi khi và chỉ khi có hai ô liền nhau $x$ (gần mép đích) và $y$ (xa hơn) với $y \ne 0$ và $x = 0$ (y trượt vào được) hoặc $x = y$ (gộp được). Ví dụ: hàng cuối 4 4 32 gộp được nên đi trái, phải được; cột đầu 2 2 4 gộp được nên đi lên, xuống được. Cả bàn tốn $O(n^2)$ cho mỗi hướng.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int n;
int a[505][505];

int get(char d, int line, int t){         // ô thứ t (tính từ mép đích) của hàng/cột thứ line
    if(d == 'L') return a[line][t];
    if(d == 'R') return a[line][n - 1 - t];
    if(d == 'U') return a[t][line];
    return a[n - 1 - t][line];            // 'D'
}

bool valid(char d){
    for(int line = 0; line < n; line++)
        for(int t = 0; t + 1 < n; t++){
            int x = get(d, line, t), y = get(d, line, t + 1);   // x gần mép đích hơn y
            if(y != 0 && (x == 0 || x == y)) return true;       // y trượt vào chỗ trống, hoặc gộp với x
        }
    return false;
}

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n;
    for(int i = 0; i < n; i++) for(int j = 0; j < n; j++) cin >> a[i][j];
    string res;
    for(char d : string("LURD")) if(valid(d)){ if(!res.empty()) res += ' '; res += d; }
    cout << (res.empty() ? "NONE" : res);
    return 0;
}
import sys

def valid(b, d):
    n = len(b)
    for line in range(n):
        if d == 'L': seq = b[line]
        elif d == 'R': seq = b[line][::-1]
        elif d == 'U': seq = [b[t][line] for t in range(n)]
        else: seq = [b[t][line] for t in range(n - 1, -1, -1)]
        for t in range(n - 1):                    # seq[t] gần mép đích hơn seq[t + 1]
            x, y = seq[t], seq[t + 1]
            if y != 0 and (x == 0 or x == y):     # y trượt vào chỗ trống, hoặc gộp với x
                return True
    return False

data = sys.stdin.buffer.read().split()
n = int(data[0])
b = [list(map(int, data[1 + i * n:1 + (i + 1) * n])) for i in range(n)]
res = [d for d in 'LURD' if valid(b, d)]
print(' '.join(res) if res else 'NONE')
Bài 3Chuỗi phím bấmDễ

Cho bàn $n \times n$ ($2 \le n \le 50$) và xâu $s$ gồm các ký tự L U R D ($|s| \le 1000$), mỗi ký tự là một lần bấm phím. Trong bài này không sinh ô mới. In ra bàn sau khi bấm hết xâu và tổng số điểm.

Input
4
2 0 2 4
2 4 4 4
0 4 8 8
2 2 2 2
LLU
Output
8 8 4 0
2 16 0 0
4 0 0 0
8 0 0 0
52

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Gọi hàm đi một nước ở phần 3 lần lượt cho từng ký tự và cộng dồn điểm. Nước không làm bàn thay đổi thì điểm gộp bằng 0, không cần xử lý riêng. Trong ví dụ, lần ← thứ nhất được +36 điểm (4 + 8 + 16 + 8), lần ← thứ hai gộp tiếp hai cặp 4 4 ở hàng 1 và hàng 4 (+16), lần ↑ các ô chỉ trượt. Tổng 52. Độ phức tạp $O(|s| \cdot n^2)$.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int n;
vector<vector<long long>> a;

long long slideLeft(vector<long long> &row){      // như phần 2
    int k = 0;
    vector<long long> b(n, 0);
    bool canMerge = false;
    long long gain = 0;
    for(int i = 0; i < n; i++){
        if(row[i] == 0) continue;
        if(canMerge && b[k - 1] == row[i]){ b[k - 1] *= 2; gain += b[k - 1]; canMerge = false; }
        else { b[k++] = row[i]; canMerge = true; }
    }
    row = b;
    return gain;
}

void reverseRows(){                                // lật ngang: đảo từng hàng
    for(auto &r : a) reverse(r.begin(), r.end());
}

void transpose(){                                  // chuyển vị: a[i][j] <-> a[j][i]
    for(int i = 0; i < n; i++)
        for(int j = i + 1; j < n; j++) swap(a[i][j], a[j][i]);
}

long long moveLeft(){
    long long g = 0;
    for(auto &r : a) g += slideLeft(r);
    return g;
}

long long doMove(char d){
    long long g;
    if(d == 'L') g = moveLeft();
    else if(d == 'R'){ reverseRows(); g = moveLeft(); reverseRows(); }
    else if(d == 'U'){ transpose(); g = moveLeft(); transpose(); }
    else { transpose(); reverseRows(); g = moveLeft(); reverseRows(); transpose(); }   // 'D'
    return g;
}

int main(){
    cin >> n;
    a.assign(n, vector<long long>(n));
    for(auto &r : a) for(auto &x : r) cin >> x;
    string s;
    cin >> s;
    long long g = 0;
    for(char d : s) g += doMove(d);       // bàn không đổi thì doMove trả về 0, không sao
    for(auto &r : a){
        for(auto x : r) cout << x << " ";
        cout << "\n";
    }
    cout << g;
    return 0;
}
import sys

def slide_left(a):                                # như phần 2
    b, gain, can_merge = [], 0, False
    for v in a:
        if v == 0:
            continue
        if can_merge and b[-1] == v:
            b[-1] *= 2; gain += b[-1]; can_merge = False
        else:
            b.append(v); can_merge = True
    return b + [0] * (len(a) - len(b)), gain

def transpose(b):
    return [list(col) for col in zip(*b)]

def reverse_rows(b):
    return [r[::-1] for r in b]

def move_left(b):
    res, g = [], 0
    for r in b:
        x, y = slide_left(r)
        res.append(x); g += y
    return res, g

def do_move(b, d):
    if d == 'L':
        return move_left(b)
    if d == 'R':
        x, g = move_left(reverse_rows(b)); return reverse_rows(x), g
    if d == 'U':
        x, g = move_left(transpose(b)); return transpose(x), g
    x, g = move_left(reverse_rows(transpose(b)))  # 'D'
    return transpose(reverse_rows(x)), g

data = sys.stdin.read().split()
n = int(data[0])
b = [list(map(int, data[1 + i * n:1 + (i + 1) * n])) for i in range(n)]
g = 0
for d in data[1 + n * n]:                       # bàn không đổi thì điểm cộng bằng 0, không sao
    b, x = do_move(b, d)
    g += x
for r in b:
    print(*r)
print(g)
Bài 4Ô mới theo kịch bảnTrung bình

Trên bàn 4 × 4 người ta bấm $q$ lần ($1 \le q \le 10^5$). Mỗi lần cho một dòng d r c v: hướng bấm $d$, và vị trí ô mới (hàng $r$, cột $c$, đánh số từ 1) với giá trị $v \in \{2, 4\}$. Nếu nước đi làm bàn thay đổi thì cộng điểm và đặt ô mới (đề đảm bảo ô đó trống sau nước đi); nếu bàn không đổi thì bỏ qua cả dòng. In ra tổng điểm, số nước hợp lệ, ô lớn nhất; dòng sau in YES nếu bàn cuối cùng còn nước đi, ngược lại NO.

Input
2 2 0 0
0 0 0 0
0 0 0 0
0 0 0 0
4
L 1 4 2
L 4 4 4
U 2 1 2
U 3 3 2
Output
4 3 4
YES

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Ghép đúng những gì ở phần 4: lưu bản sao, đi, so sánh; có đổi thì cộng điểm, tăng số nước và đặt ô mới. Ví dụ: nước 1 gộp 2 2 thành 4 (+4) rồi đặt 2 ở ô (1, 4); nước 2 bấm ← nhưng hàng 1 là 4 0 0 2 nên vẫn đổi thành 4 2 0 0, đặt 4 ở (4, 4); nước 3 ↑ đưa ô 4 ở góc dưới lên; nước 4 ↑ không đổi gì nên bỏ qua. Cuối cùng kiểm tra hết nước đi như khung chạy tay ở phần 4. Mỗi nước chỉ tốn khoảng 50 phép tính trên bàn 4 × 4.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

typedef vector<vector<long long>> Board;
const int N = 4;

long long slideLeft(vector<long long> &row){
    int k = 0;
    vector<long long> b(N, 0);
    bool canMerge = false;
    long long gain = 0;
    for(int i = 0; i < N; i++){
        if(row[i] == 0) continue;
        if(canMerge && b[k - 1] == row[i]){ b[k - 1] *= 2; gain += b[k - 1]; canMerge = false; }
        else { b[k++] = row[i]; canMerge = true; }
    }
    row = b;
    return gain;
}
void reverseRows(Board &a){ for(auto &r : a) reverse(r.begin(), r.end()); }
void transpose(Board &a){ for(int i = 0; i < N; i++) for(int j = i + 1; j < N; j++) swap(a[i][j], a[j][i]); }
long long doMove(Board &a, char d){
    if(d == 'U' || d == 'D') transpose(a);
    if(d == 'R' || d == 'D') reverseRows(a);
    long long g = 0;
    for(auto &r : a) g += slideLeft(r);
    if(d == 'R' || d == 'D') reverseRows(a);
    if(d == 'U' || d == 'D') transpose(a);
    return g;
}
bool canMove(const Board &a){
    for(int i = 0; i < N; i++)
        for(int j = 0; j < N; j++)
            if(a[i][j] == 0 || (j + 1 < N && a[i][j] == a[i][j + 1]) || (i + 1 < N && a[i][j] == a[i + 1][j])) return true;
    return false;
}

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    Board a(N, vector<long long>(N));
    for(auto &r : a) for(auto &x : r) cin >> x;
    int q;
    cin >> q;
    long long score = 0, moves = 0;
    while(q--){
        char d; int r, c; long long v;
        cin >> d >> r >> c >> v;
        Board old = a;
        long long g = doMove(a, d);
        if(a == old) continue;            // không đổi: bỏ qua cả ô mới
        score += g; moves++;
        a[r - 1][c - 1] = v;
    }
    long long mx = 0;
    for(auto &r : a) for(auto x : r) mx = max(mx, x);
    cout << score << " " << moves << " " << mx << "\n" << (canMove(a) ? "YES" : "NO");
    return 0;
}
import sys

N = 4

def slide_left(a):
    b, gain, can_merge = [], 0, False
    for v in a:
        if v == 0:
            continue
        if can_merge and b[-1] == v:
            b[-1] *= 2; gain += b[-1]; can_merge = False
        else:
            b.append(v); can_merge = True
    return b + [0] * (len(a) - len(b)), gain

def do_move(b, d):
    t = b
    if d in 'UD': t = [list(c) for c in zip(*t)]
    if d in 'RD': t = [r[::-1] for r in t]
    res, g = [], 0
    for r in t:
        x, y = slide_left(r); res.append(x); g += y
    if d in 'RD': res = [r[::-1] for r in res]
    if d in 'UD': res = [list(c) for c in zip(*res)]
    return res, g

def can_move(b):
    for i in range(N):
        for j in range(N):
            if b[i][j] == 0 or (j + 1 < N and b[i][j] == b[i][j + 1]) or (i + 1 < N and b[i][j] == b[i + 1][j]):
                return True
    return False

data = sys.stdin.buffer.read().split()
b = [list(map(int, data[i * N:(i + 1) * N])) for i in range(N)]
q = int(data[16])
score = moves = 0
p = 17
for _ in range(q):
    d = data[p].decode(); r = int(data[p + 1]); c = int(data[p + 2]); v = int(data[p + 3])
    p += 4
    nb, g = do_move(b, d)
    if nb == b:                           # không đổi: bỏ qua cả ô mới
        continue
    b = nb; score += g; moves += 1
    b[r - 1][c - 1] = v
print(score, moves, max(max(r) for r in b))
print("YES" if can_move(b) else "NO")
Bài 5Bấm trái đến khi đứng yênTrung bình

Cho một hàng $n$ ô ($1 \le n \le 2000$), mỗi ô là 0 hoặc lũy thừa của 2 không quá $2^{20}$. Người chơi bấm ← liên tục cho tới khi bấm thêm cũng không làm hàng thay đổi (không sinh ô mới). In ra số lần bấm có làm thay đổi và tổng điểm, rồi in hàng cuối cùng.

Input
6
2 2 4 8 0 16
Output
4 60
32 0 0 0 0 0

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Mô phỏng: gọi hàm trượt trái, nếu hàng không đổi thì dừng. Vì sao không bị lặp mãi? Sau lần bấm đầu tiên, mọi số đã dồn sát trái; từ đó mỗi lần hàng thay đổi đều phải có ít nhất một phép gộp, làm số ô có số giảm đi ít nhất 1. Nên số lần bấm có thay đổi không quá $n + 1$, tổng thời gian $O(n^2) = 4 \cdot 10^6$. Ví dụ: 2 2 4 8 16 → 4 4 8 16 → 8 8 16 → 16 16 → 32, tức là 4 lần, điểm 4 + 8 + 16 + 32 = 60.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

long long slideLeft(vector<long long> &a){
    int n = a.size(), k = 0;
    vector<long long> b(n, 0);
    bool canMerge = false;
    long long gain = 0;
    for(int i = 0; i < n; i++){
        if(a[i] == 0) continue;
        if(canMerge && b[k - 1] == a[i]){ b[k - 1] *= 2; gain += b[k - 1]; canMerge = false; }
        else { b[k++] = a[i]; canMerge = true; }
    }
    a = b;
    return gain;
}

int main(){
    int n;
    cin >> n;
    vector<long long> a(n);
    for(auto &x : a) cin >> x;
    long long presses = 0, total = 0;
    while(true){
        vector<long long> old = a;
        long long g = slideLeft(a);
        if(a == old) break;               // hàng đứng yên: dừng
        presses++; total += g;
    }
    cout << presses << " " << total << "\n";
    for(auto x : a) cout << x << " ";
    return 0;
}
import sys

def slide_left(a):
    b, gain, can_merge = [], 0, False
    for v in a:
        if v == 0:
            continue
        if can_merge and b[-1] == v:
            b[-1] *= 2; gain += b[-1]; can_merge = False
        else:
            b.append(v); can_merge = True
    return b + [0] * (len(a) - len(b)), gain

data = sys.stdin.read().split()
n = int(data[0])
a = list(map(int, data[1:n + 1]))
presses = total = 0
while True:
    b, g = slide_left(a)
    if b == a:                            # hàng đứng yên: dừng
        break
    a = b; presses += 1; total += g
print(presses, total)
print(*a)
Bài 6Hướng nhiều ô trống nhấtTrung bình

Cho bàn $n \times n$ ($2 \le n \le 200$). Với mỗi hướng đi được (làm bàn thay đổi), đếm số ô trống của bàn sau khi đi (không sinh ô mới). In ra hướng cho nhiều ô trống nhất và số ô trống đó; nếu bằng nhau thì chọn hướng đứng trước trong thứ tự L U R D. Nếu không có hướng nào đi được, in NONE.

Input
4
64 16 8 2
64 8 4 0
4 4 2 0
2 0 0 0
Output
L 6

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Đây là máy chơi tham lam nhìn 1 nước với hàm đánh giá chỉ gồm số ô trống. Với mỗi hướng, đi trên bản sao của bàn, bỏ qua nếu bàn không đổi, đếm số 0. Nhớ phải so sánh với bàn gốc trước khi đếm, nếu không em có thể chọn nhầm một hướng không hợp lệ. Ví dụ: bàn có 5 ô trống, hướng nào cũng gộp được đúng một cặp (← và → gộp 4 4 ở hàng thứ ba, ↑ và ↓ gộp hai ô 64 ở cột đầu), nên cả 4 hướng đều cho 6 ô trống; ← đứng đầu thứ tự ưu tiên nên được chọn. Cũng bàn này, máy ở phần 5 lại chọn khác vì còn tính thưởng góc và nhìn trước.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

typedef vector<vector<long long>> Board;
int n;

long long slideLeft(vector<long long> &row){
    int k = 0;
    vector<long long> b(n, 0);
    bool canMerge = false;
    long long gain = 0;
    for(int i = 0; i < n; i++){
        if(row[i] == 0) continue;
        if(canMerge && b[k - 1] == row[i]){ b[k - 1] *= 2; gain += b[k - 1]; canMerge = false; }
        else { b[k++] = row[i]; canMerge = true; }
    }
    row = b;
    return gain;
}
void reverseRows(Board &a){ for(auto &r : a) reverse(r.begin(), r.end()); }
void transpose(Board &a){ for(int i = 0; i < n; i++) for(int j = i + 1; j < n; j++) swap(a[i][j], a[j][i]); }
long long doMove(Board &a, char d){
    if(d == 'U' || d == 'D') transpose(a);
    if(d == 'R' || d == 'D') reverseRows(a);
    long long g = 0;
    for(auto &r : a) g += slideLeft(r);
    if(d == 'R' || d == 'D') reverseRows(a);
    if(d == 'U' || d == 'D') transpose(a);
    return g;
}

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n;
    Board a(n, vector<long long>(n));
    for(auto &r : a) for(auto &x : r) cin >> x;
    int best = -1;
    char bd = '?';
    for(char d : string("LURD")){
        Board b = a;
        doMove(b, d);
        if(b == a) continue;                       // hướng không hợp lệ
        int e = 0;
        for(auto &r : b) for(auto x : r) if(x == 0) e++;
        if(e > best){ best = e; bd = d; }          // bằng nhau: giữ hướng đứng trước
    }
    if(bd == '?') cout << "NONE";
    else cout << bd << " " << best;
    return 0;
}
import sys

def slide_left(a):
    b, can_merge = [], False
    for v in a:
        if v == 0:
            continue
        if can_merge and b[-1] == v:
            b[-1] *= 2; can_merge = False
        else:
            b.append(v); can_merge = True
    return b + [0] * (len(a) - len(b))

def do_move(b, d):
    t = b
    if d in 'UD': t = [list(c) for c in zip(*t)]
    if d in 'RD': t = [r[::-1] for r in t]
    res = [slide_left(r) for r in t]
    if d in 'RD': res = [r[::-1] for r in res]
    if d in 'UD': res = [list(c) for c in zip(*res)]
    return res

data = sys.stdin.buffer.read().split()
n = int(data[0])
a = [list(map(int, data[1 + i * n:1 + (i + 1) * n])) for i in range(n)]
best, bd = -1, None
for d in 'LURD':
    b = do_move(a, d)
    if b == a:                            # hướng không hợp lệ
        continue
    e = sum(r.count(0) for r in b)
    if e > best:                          # bằng nhau: giữ hướng đứng trước
        best, bd = e, d
print(f"{bd} {best}" if bd else "NONE")
Bài 7Nhìn trước 2 nướcKhá

Cho bàn $n \times n$ ($2 \le n \le 100$), không sinh ô mới. Người chơi muốn đi hai nước để tổng điểm gộp lớn nhất (cả hai nước đều phải làm bàn thay đổi; nếu sau nước thứ nhất không còn nước nào đi được thì chỉ tính nước thứ nhất). In ra hướng của nước thứ nhất và tổng điểm lớn nhất; bằng nhau thì chọn hướng đứng trước trong thứ tự L U R D. Không có nước nào thì in NONE.

Input
4
2 2 4 0
0 0 0 8
0 0 0 0
0 0 0 0
Output
L 12

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Hai vòng lặp lồng nhau trên 4 hướng như phần 5, chỉ khác hàm đánh giá: ở đây chỉ tính điểm gộp. Ví dụ: đi ← được +4 (hàng 1 thành 4 4 0 0), rồi ← tiếp gộp 4 4 thành 8, tổng 12. Đi → trước cũng được 12 nhưng ← đứng trước. Đi ↑ hay ↓ trước thì nước đầu không gộp được gì, cả hai nước cộng lại chỉ được 4. Mỗi lần đi tốn $O(n^2)$, tổng $16 \cdot n^2$.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

typedef vector<vector<long long>> Board;
int n;

long long slideLeft(vector<long long> &row){
    int k = 0;
    vector<long long> b(n, 0);
    bool canMerge = false;
    long long gain = 0;
    for(int i = 0; i < n; i++){
        if(row[i] == 0) continue;
        if(canMerge && b[k - 1] == row[i]){ b[k - 1] *= 2; gain += b[k - 1]; canMerge = false; }
        else { b[k++] = row[i]; canMerge = true; }
    }
    row = b;
    return gain;
}
void reverseRows(Board &a){ for(auto &r : a) reverse(r.begin(), r.end()); }
void transpose(Board &a){ for(int i = 0; i < n; i++) for(int j = i + 1; j < n; j++) swap(a[i][j], a[j][i]); }
long long doMove(Board &a, char d){
    if(d == 'U' || d == 'D') transpose(a);
    if(d == 'R' || d == 'D') reverseRows(a);
    long long g = 0;
    for(auto &r : a) g += slideLeft(r);
    if(d == 'R' || d == 'D') reverseRows(a);
    if(d == 'U' || d == 'D') transpose(a);
    return g;
}

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n;
    Board a(n, vector<long long>(n));
    for(auto &r : a) for(auto &x : r) cin >> x;
    long long best = -1;
    char bd = '?';
    for(char d : string("LURD")){
        Board b1 = a;
        long long g1 = doMove(b1, d);
        if(b1 == a) continue;                      // nước 1 không hợp lệ
        long long s = g1;                          // nếu không có nước 2 thì chỉ tính nước 1
        for(char d2 : string("LURD")){
            Board b2 = b1;
            long long g2 = doMove(b2, d2);
            if(b2 != b1) s = max(s, g1 + g2);
        }
        if(s > best){ best = s; bd = d; }
    }
    if(bd == '?') cout << "NONE";
    else cout << bd << " " << best;
    return 0;
}
import sys

def slide_left(a):
    b, gain, can_merge = [], 0, False
    for v in a:
        if v == 0:
            continue
        if can_merge and b[-1] == v:
            b[-1] *= 2; gain += b[-1]; can_merge = False
        else:
            b.append(v); can_merge = True
    return b + [0] * (len(a) - len(b)), gain

def do_move(b, d):
    t = b
    if d in 'UD': t = [list(c) for c in zip(*t)]
    if d in 'RD': t = [r[::-1] for r in t]
    res, g = [], 0
    for r in t:
        x, y = slide_left(r); res.append(x); g += y
    if d in 'RD': res = [r[::-1] for r in res]
    if d in 'UD': res = [list(c) for c in zip(*res)]
    return res, g

data = sys.stdin.buffer.read().split()
n = int(data[0])
a = [list(map(int, data[1 + i * n:1 + (i + 1) * n])) for i in range(n)]
best, bd = -1, None
for d in 'LURD':
    b1, g1 = do_move(a, d)
    if b1 == a:                           # nước 1 không hợp lệ
        continue
    s = g1                                # nếu không có nước 2 thì chỉ tính nước 1
    for d2 in 'LURD':
        b2, g2 = do_move(b1, d2)
        if b2 != b1:
            s = max(s, g1 + g2)
    if s > best:
        best, bd = s, d
print(f"{bd} {best}" if bd else "NONE")
Bài 8Bấm lặp K lầnKhá

Cho bàn $n \times n$ ($2 \le n \le 4$), các ô là 0 hoặc lũy thừa của 2 không quá $2^{16}$, và xâu phím $s$ ($1 \le |s| \le 100$). Người chơi bấm hết xâu $s$, rồi lại bấm lại từ đầu, tổng cộng lặp $K$ lần ($1 \le K \le 10^{18}$). Không sinh ô mới. In ra bàn cuối cùng và tổng điểm.

Input
4 1000000000000
8 0 2 2
8 2 4 4
4 2 2 4
8 0 4 8
LDRUR
Output
0 16 4 8
0 4 2 4
0 0 8 16
0 0 0 0
56

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Không thể mô phỏng $10^{18}$ lần. Nhưng số bàn khác nhau có hạn, nên sau một số lượt bàn sẽ lặp lại, giống mẹo tìm chu kỳ ở chuyên đề 4. Lưu bàn sau mỗi lượt vào map; khi bàn sau lượt $t$ trùng bàn sau lượt $f$, chu kỳ dài $t - f$. Số lượt còn lại $K - t$ chia cho độ dài chu kỳ: phần nguyên là số vòng trọn, phần dư $r$ cho biết bàn cuối là bàn sau lượt $f + r$. Điểm cũng tính như vậy (thật ra trong chu kỳ không thể có phép gộp nào, vì mỗi phép gộp làm giảm số ô có số). Ví dụ: lượt đầu tiên gộp được 56 điểm; từ đó các ô chỉ “chạy vòng” quanh bàn: bàn sau lượt 8 trùng bàn sau lượt 1, chu kỳ dài 7. $10^{12} - 8$ chia 7 dư 3, nên bàn cuối là bàn sau lượt $1 + 3 = 4$, và tổng điểm vẫn là 56.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

typedef vector<vector<long long>> Board;
int n;

long long slideLeft(vector<long long> &row){
    int k = 0;
    vector<long long> b(n, 0);
    bool canMerge = false;
    long long gain = 0;
    for(int i = 0; i < n; i++){
        if(row[i] == 0) continue;
        if(canMerge && b[k - 1] == row[i]){ b[k - 1] *= 2; gain += b[k - 1]; canMerge = false; }
        else { b[k++] = row[i]; canMerge = true; }
    }
    row = b;
    return gain;
}
void reverseRows(Board &a){ for(auto &r : a) reverse(r.begin(), r.end()); }
void transpose(Board &a){ for(int i = 0; i < n; i++) for(int j = i + 1; j < n; j++) swap(a[i][j], a[j][i]); }
long long doMove(Board &a, char d){
    if(d == 'U' || d == 'D') transpose(a);
    if(d == 'R' || d == 'D') reverseRows(a);
    long long g = 0;
    for(auto &r : a) g += slideLeft(r);
    if(d == 'R' || d == 'D') reverseRows(a);
    if(d == 'U' || d == 'D') transpose(a);
    return g;
}

int main(){
    long long K;
    string s;
    cin >> n >> K;
    Board a(n, vector<long long>(n));
    for(auto &r : a) for(auto &x : r) cin >> x;
    cin >> s;
    map<Board, long long> seen;           // bàn -> sau bao nhiêu lượt thì gặp
    vector<Board> boards;                 // boards[t]: bàn sau t lượt
    vector<long long> sc;                 // sc[t]: tổng điểm sau t lượt
    boards.push_back(a); sc.push_back(0); seen[a] = 0;
    long long t = 0;
    while(t < K){
        long long g = 0;
        for(char d : s) g += doMove(a, d);
        t++;
        boards.push_back(a); sc.push_back(sc.back() + g);
        if(seen.count(a)){                // gặp lại bàn cũ: có chu kỳ
            long long f = seen[a], len = t - f, rest = K - t;
            long long full = rest / len, r = rest % len;
            long long total = sc[t] + full * (sc[t] - sc[f]) + (sc[f + r] - sc[f]);
            a = boards[f + r];
            for(auto &row : a){ for(auto x : row) cout << x << " "; cout << "\n"; }
            cout << total;
            return 0;
        }
        seen[a] = t;
    }
    for(auto &row : a){ for(auto x : row) cout << x << " "; cout << "\n"; }
    cout << sc[t];
    return 0;
}
import sys

def slide_left(a):
    b, gain, can_merge = [], 0, False
    for v in a:
        if v == 0:
            continue
        if can_merge and b[-1] == v:
            b[-1] *= 2; gain += b[-1]; can_merge = False
        else:
            b.append(v); can_merge = True
    return b + [0] * (len(a) - len(b)), gain

def do_move(b, d):
    t = b
    if d in 'UD': t = [list(c) for c in zip(*t)]
    if d in 'RD': t = [r[::-1] for r in t]
    res, g = [], 0
    for r in t:
        x, y = slide_left(r); res.append(x); g += y
    if d in 'RD': res = [r[::-1] for r in res]
    if d in 'UD': res = [list(c) for c in zip(*res)]
    return res, g

data = sys.stdin.read().split()
n, K = int(data[0]), int(data[1])
a = [list(map(int, data[2 + i * n:2 + (i + 1) * n])) for i in range(n)]
s = data[2 + n * n]
key = lambda b: tuple(map(tuple, b))
seen = {key(a): 0}                        # bàn -> sau bao nhiêu lượt thì gặp
boards, sc = [a], [0]                     # boards[t], sc[t]: bàn và tổng điểm sau t lượt
t = 0
ans = None
while t < K:
    g = 0
    for d in s:
        a, x = do_move(a, d)
        g += x
    t += 1
    boards.append(a); sc.append(sc[-1] + g)
    if key(a) in seen:                    # gặp lại bàn cũ: có chu kỳ
        f = seen[key(a)]
        ln, rest = t - f, K - t
        full, r = divmod(rest, ln)
        ans = (boards[f + r], sc[t] + full * (sc[t] - sc[f]) + (sc[f + r] - sc[f]))
        break
    seen[key(a)] = t
if ans is None:
    ans = (a, sc[t])
for row in ans[0]:
    print(*row)
print(ans[1])
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 😇
Bản quyền thuộc về Thầy Nguyễn Việt Nam Sơn. Bài viết, lời giải, hình minh hoạ và mã nguồn trên trang là công sức biên soạn của thầy qua nhiều năm giảng dạy. Vui lòng không sao chép, đăng lại hay sử dụng dưới mọi hình thức khi chưa được sự đồng ý của tác giả. Nếu thấy hữu ích, hãy chia sẻ bằng đường link bài gốc nhé. Cảm ơn bạn!
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