Có một đống 10 viên sỏi. Hai người lần lượt lấy 1, 2 hoặc 3 viên; ai không còn gì để lấy thì thua. Đi trước nên lấy mấy viên?
Trả lời được là nhờ nhìn ra vị trí thắng, vị trí thua. Nhiều bài trò chơi không cần lý thuyết nào nặng hơn: tính các trạng thái nhỏ để tìm quy luật, đếm chẵn lẻ số lượt, hoặc dựng chiến lược đối xứng để luôn có nước đáp trả. Khi hai người tranh điểm, DP minimax làm nốt phần còn lại.
Thắng và thua
Một trạng thái là thua (cho người sắp đi) nếu mọi nước đi đều dẫn tới trạng thái thắng. Nó là thắng nếu có ít nhất một nước tới trạng thái thua. Trạng thái không còn nước đi là thua. Đồ thị trạng thái không có chu trình thì tính theo thứ tự ngược của nước đi, từ trạng thái gần kết thúc.
Hình dưới tính bảng này cho trò lấy sỏi. Đổi tập số sỏi được lấy mỗi lượt để thấy quy luật.
Tham số tới thì không tính hết được. Ta tính brute force cho nhỏ, nhìn ra chu kỳ, rồi chứng minh bằng quy nạp: mỗi trạng thái thua chỉ dẫn tới trạng thái thắng, mỗi trạng thái thắng có nước tới trạng thái thua.
Hỏi nhanh
Trò lấy sỏi: mỗi lượt bỏ 1, 2 hoặc 3 viên, người không lấy được thua. Những nào là vị trí thua cho người đi trước?
Từ bội của 4, lấy bao nhiêu cũng để lại số không chia hết cho 4; từ số không chia hết cho 4, lấy đúng phần dư là về bội của 4. Vậy bội của 4 là thua, còn lại thắng.
Ví dụBước Nhảy Trên Hoán Vị
Đề. Hoán vị dài . Từ ô được nhảy tới ô nếu là bội dương của và . Với mỗi ô xuất phát, in A nếu người đi trước thắng, B nếu thua.
Lời giải. Mọi nước đi tăng giá trị, nên xét ô theo giảm dần: khi tới ô , mọi ô đích đã có kết quả. Ô thắng nếu có một ô đích thua.
Bước 1: duyệt theo giá trị giảm dần. at[v] là vị trí của giá trị .
Bước 2: các ô đích. Vòng j bắt đầu từ i % v, tăng theo bước : đi qua mọi ô cách một bội của , cả bên trái lẫn bên phải, trong một vòng. j >= 1 loại ô 0; a[j] > v giữ đúng điều kiện nước đi và tự loại chính ô .
for (ll v = n; v >= 1; v--) {
ll i = at[v];
for (ll j = i % v; j <= n; j += v)
if (j >= 1 && a[j] > v && !win[j]) win[i] = 1;
}
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 5005;
ll n, a[N], at[N];
bool win[N];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
for (ll i = 1; i <= n; i++) {
cin >> a[i];
at[a[i]] = i;
}
for (ll v = n; v >= 1; v--) {
ll i = at[v];
for (ll j = i % v; j <= n; j += v)
if (j >= 1 && a[j] > v && !win[j]) win[i] = 1;
}
for (ll i = 1; i <= n; i++) cout << (win[i] ? 'A' : 'B');
cout << '\n';
}
1
1
B
Độ phức tạp: tổng số ô xét là .
Ví dụBước Nhảy 1-2-K
Đề. Quân ở ô trên dải , mỗi lượt lùi 1, 2 hoặc ô; không đi được thì thua. , , tối đa 100 test.
Lời giải. Không có bước , thua đúng khi chia hết cho 3. Nếu không chia hết cho 3, bước từ bội của 3 vẫn rơi vào số không chia hết cho 3, nên quy luật giữ nguyên. Nếu chia hết cho 3, brute force cho thấy chu kỳ : lấy , ô thắng (nhảy về 0), các khác thắng khi không chia hết cho 3.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll t, n, k;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> t;
while (t--) {
cin >> n >> k;
bool alice;
if (k % 3) alice = n % 3 != 0;
else {
ll r = n % (k + 1);
alice = r == k || r % 3 != 0;
}
cout << (alice ? "Alice" : "Bob") << '\n';
}
}
1
0 3
Bob
Chỉ cần chẵn lẻ
Có những trò mà dù chơi thế nào, tổng số lượt vẫn như nhau. Khi đó chiến lược không còn ý nghĩa: người đi trước thắng khi và chỉ khi tổng số lượt lẻ. Dấu hiệu: mỗi lượt làm một đại lượng thay đổi đúng 1, và trò chỉ dừng khi đại lượng đó về một giá trị cố định.
Cần chứng minh tổng số lượt không phụ thuộc cách chơi, không chỉ đoán. Ví dụ: xóa cặp ký tự kề bằng nhau thì xâu cuối cùng là duy nhất, như rút gọn ngoặc, nên số lần xóa luôn như nhau.
Ví dụNhững Khúc Gỗ
Đề. khúc gỗ dài . Mỗi lượt chặt một khúc dài ít nhất 2 thành hai khúc nguyên dương. Ai không chặt được thì thua; errorgorn đi trước.
Lời giải. Mỗi lượt tăng số khúc đúng 1, trò dừng khi mọi khúc dài 1, tức có khúc. Tổng số lượt là ; lẻ thì errorgorn thắng.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll t, n;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> t;
while (t--) {
cin >> n;
ll s = 0;
for (ll i = 1; i <= n; i++) {
ll x;
cin >> x;
s += x - 1;
}
cout << (s % 2 ? "errorgorn" : "maomao90") << '\n';
}
}
1
1
1
maomao90
Đối xứng
Nếu người thứ hai luôn đi được nước gương của nước vừa rồi thì không bao giờ hết nước trước: người thứ nhất đi được thì nước gương cũng hợp lệ. Muốn người đi trước hưởng chiến lược này, nước đầu phải đưa trạng thái về dạng đối xứng, ví dụ chiếm đúng tâm.
Tổng quát hơn là ghép cặp: chia các nước đi thành cặp sao cho đối thủ dùng một nước thì nước còn lại của cặp vẫn hợp lệ. Chỉ cần chỉ ra phép ghép, không cần duyệt cây trò chơi.
Ví dụĐặt Đĩa Tròn
Đề. Bàn , hai người luân phiên đặt đĩa bán kính nằm trọn trong bàn và không chồng lên nhau. Ai không đặt được thì thua. .
Lời giải. Không đặt nổi một đĩa ( hoặc ) thì người đầu thua. Ngược lại người đầu đặt đĩa đầu vào tâm, rồi đáp mỗi đĩa của đối thủ bằng đĩa đối xứng qua tâm: chỗ đó luôn trống, vì nếu nó chồng lên đĩa nào thì đĩa đối thủ vừa đặt cũng chồng lên ảnh gương của đĩa ấy.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll a, b, r;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> a >> b >> r;
cout << (2 * r <= min(a, b) ? "First" : "Second") << '\n';
}
3 3 1
First
Tranh điểm
Hai người cùng tối đa điểm của mình trên một tổng cố định. Đặt là hiệu “điểm người sắp đi trừ điểm người kia” khi cả hai chơi tối ưu. Người sắp đi chọn nước được điểm, rồi đổi vai: . Biết hiệu và tổng thì điểm người đầu là .
Một số bài cho phép tham lam (sắp giảm dần rồi chọn lần lượt). Khi các lượt ảnh hưởng nhau qua hình dạng trạng thái thì cần DP.
Trong hình, mỗi ô lấy từ hai ô kề màu xanh: lấy số trái hay số phải, rồi đổi vai.
Hỏi nhanh
Dãy ; mỗi lượt lấy một số ở đầu trái hoặc phải. Người đầu chơi tối ưu được bao nhiêu điểm?
Lấy 5 thì còn : đối thủ lấy 2, người đầu lấy nốt 1, được 6. Lấy 2 thì đối thủ lấy 5, người đầu chỉ còn 1, được 3. Tham lam “lấy số to hơn ở hai đầu” đúng ở đây nhưng sai với : lấy 2 thì mất 100 vào tay đối thủ, còn lấy 1 bên phải thì người đầu chắc chắn được 100.
Ví dụLấy Số Hai Đầu
Đề. Dãy số nguyên (có thể âm, trị tuyệt đối tới ). Hai người lần lượt lấy một số ở đầu trái hoặc đầu phải và cộng vào điểm mình, cùng tối đa điểm mình. In điểm người đầu.
Lời giải. Gọi là điểm người đi lượt này hơn đối thủ khi còn đoạn . Lấy đầu trái được , rồi đối thủ hơn mình ; lấy đầu phải tương tự. Vậy , . Biết tổng và hiệu thì điểm người đầu là .
Bước 1: một mảng cho độ dài hiện tại. dp[l] ở vòng độ dài len là . Lúc tính, dp[l + 1] vẫn là giá trị độ dài cũ, tức ; dp[l] cũ là . Duyệt tăng dần nên đọc dp[l + 1] trước khi nó bị ghi. Bộ nhớ thay vì số long long.
for (ll len = 2; len <= n; len++)
for (ll l = 1; l + len - 1 <= n; l++) {
ll r = l + len - 1;
dp[l] = max(a[l] - dp[l + 1], a[r] - dp[l]);
}
cout << (sum + dp[1]) / 2 << '\n';
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 5005;
ll n, a[N], dp[N];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
ll sum = 0;
for (ll i = 1; i <= n; i++) cin >> a[i], sum += a[i], dp[i] = a[i];
for (ll len = 2; len <= n; len++)
for (ll l = 1; l + len - 1 <= n; l++) {
ll r = l + len - 1;
dp[l] = max(a[l] - dp[l + 1], a[r] - dp[l]);
}
cout << (sum + dp[1]) / 2 << '\n';
}
1
1
1
thời gian, bộ nhớ. Cuốn mảng không chỉ tiết kiệm bộ nhớ, nó còn nhanh hơn nhiều:
Cùng ô cần tính, mảng một chiều nhanh hơn bảng hai chiều duyệt theo độ dài khoảng 15 lần, và duyệt theo hàng cũng chậm hơn khoảng 6 lần. Quy tắc: DP chỉ cần lớp trước thì cuốn mảng, cả bộ nhớ lẫn tốc độ đều lợi.
Khi đi thi
- Viết brute force cho tham số nhỏ trước tiên, in bảng ra, tìm chu kỳ hay điều kiện, rồi chứng minh bằng quy nạp.
- Hỏi ngay: tổng số lượt có cố định không? Có thì đáp án là chẵn lẻ. Trạng thái có đối xứng thì thử chiến lược gương, có thể sau một nước mở đầu.
- Tranh điểm thì DP hiệu số ; kiểm bộ nhớ, thường cuốn mảng theo độ dài.
Tóm tắt
- Trạng thái thua khi mọi nước dẫn tới trạng thái thắng; tính theo thứ tự ngược nước đi.
- Quy luật cho tham số lớn đến từ bảng trạng thái nhỏ, kèm chứng minh.
- Số lượt cố định thì chỉ chẵn lẻ quyết định; đối xứng hay ghép cặp cho người thứ hai, hoặc người thứ nhất sau một nước mở đầu, luôn có nước đáp.
- Trò tranh điểm: DP trên hiệu số, điểm người đầu là .
Bài tập
Đọc thêm
- Game Theorycp-algorithms
- Introduction to Game TheoryUSACO Guide
- Combinatorial Game TheoryWikipedia