Lát bảng bằng domino, chọn ô không kề nhau, đặt trạm không nhiễu: lưới có một cạnh ngắn (tới 10 đến 12 ô) và một cạnh dài. Duyệt lưới theo một thứ tự cố định thì phần đã quyết định và phần chưa quyết định chỉ chạm nhau trên một đường biên; mọi thứ cần nhớ về quá khứ nằm trên biên đó, đủ ngắn để mã hóa bằng một mặt nạ bit (bài 15). Biên có thể là cả một hàng (quy hoạch động theo hàng) hoặc một đường bậc thang “gãy” tại ô đang xét (quy hoạch động theo từng ô), cho chuyển tiếp rẻ hơn hẳn.
Biên gãy theo từng ô
Lát bảng bằng domino, nhỏ. Duyệt từng cột từ trái sang, trong mỗi cột từ trên xuống. Khi đang ở ô , biên đi dọc cột từ hàng xuống, rồi gãy sang cột cho các hàng phía trên . Bit của mặt nạ cho biết ô trên biên ở hàng đã bị phủ chưa: với đó là ô , với đó là ô , bị phủ bởi domino ngang đặt từ cột .
Tại ô với bit :
- Bit đã bật: ô đã phủ, không làm gì; bit chuyển sang nói về ô , chưa phủ, nên tắt.
- Bit tắt: đặt domino ngang sang , bật bit ; hoặc đặt domino dọc xuống nếu còn hàng và bit tắt, khi đó bật bit còn bit tắt.
Mỗi ô có trạng thái và tối đa hai chuyển tiếp: . Làm theo cả cột thì mỗi cặp mặt nạ (cột trước, cột sau) phải kiểm riêng, .
Hỏi nhanh
Đang ở ô với bit tắt và bit bật. Đặt được gì ở ô này?
Bit bật nghĩa là ô đã bị domino ngang từ cột trước chiếm, nên không đặt dọc được. Đặt ngang sang vẫn được nếu còn cột; ở cột cuối thì trạng thái này không hoàn thành được.
Ví dụLát Kín Bảng Chữ Nhật
Đề. Bảng hàng, cột. Đếm số cách lát kín bằng domino, modulo .
Lời giải. Duyệt ô theo từng cột, trong cột theo từng hàng. Mặt nạ s có bit: bit cho biết ô ở hàng của cột đang xét đã bị phủ sẵn chưa, bởi domino ngang từ cột trước hay domino dọc từ ô ngay trên.
Bước 1: ô đã bị phủ. Không đặt gì; xóa bit , vì từ giờ bit đó nói về ô hàng của cột .
if (s >> r & 1) nd[s ^ (1 << r)] = (nd[s ^ (1 << r)] + v) % MOD;
Bước 2: ô còn trống. Đặt domino ngang sang cột sau thì bật bit (cho cột ), chỉ khi còn cột sau. Đặt domino dọc xuống ô thì bật bit , chỉ khi ô đó cũng còn trống.
else {
if (c + 1 < m) nd[s | (1 << r)] = (nd[s | (1 << r)] + v) % MOD;
if (r + 1 < n && !(s >> (r + 1) & 1)) nd[s | (1 << (r + 1))] = (nd[s | (1 << (r + 1))] + v) % MOD;
}
Bước 3: chuyển bảng. Mỗi ô xóa nd, chuyển từ dp, rồi chép lại vào dp. Sau ô cuối cùng, dp[0] là số cách không ô nào của cột ảo bị phủ, tức lát vừa khít.
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll MOD = 1e9 + 7;
ll n, m, dp[1 << 10], nd[1 << 10];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m;
ll full = 1 << n;
dp[0] = 1;
for (ll c = 0; c < m; c++)
for (ll r = 0; r < n; r++) {
for (ll s = 0; s < full; s++) nd[s] = 0;
for (ll s = 0; s < full; s++) {
if (!dp[s]) continue;
ll v = dp[s];
if (s >> r & 1) nd[s ^ (1 << r)] = (nd[s ^ (1 << r)] + v) % MOD;
else {
if (c + 1 < m) nd[s | (1 << r)] = (nd[s | (1 << r)] + v) % MOD;
if (r + 1 < n && !(s >> (r + 1) & 1)) nd[s | (1 << (r + 1))] = (nd[s | (1 << (r + 1))] + v) % MOD;
}
}
for (ll s = 0; s < full; s++) dp[s] = nd[s];
}
cout << dp[0] << '\n';
}
2 3
3
bước.
Cả hàng làm trạng thái
Khi ràng buộc chỉ nối hai hàng liền nhau (không hai ô chọn chung cạnh, không ngồi chéo), lấy cả hàng làm mặt nạ: là số cách cho hàng đầu với hàng chọn tập . Chuyển tiếp duyệt mọi cặp của hai hàng liền nhau. Trước hết lọc các mặt nạ tự hợp lệ trong một hàng: không hai bit kề nhau có mặt nạ, với chỉ 377 trong 4096. Điều kiện giữa hai hàng thường là một phép bit: không chung cạnh dọc là s & s2 == 0, không chéo là (s << 1) & s2 == 0 và (s >> 1) & s2 == 0.
Hỏi nhanh
Với cột, những mặt nạ nào là hợp lệ trong một hàng (không hai ô chọn chung cạnh)?
Một mặt nạ hợp lệ khi s & (s << 1) bằng 0, tức không có hai bit 1 kề nhau. Có 5 mặt nạ như vậy, trong đó 101 chọn hai ô ở hai đầu và 000 là không chọn gì; đó cũng là đáp án của ví dụ với hàng .
Ví dụChọn Ô Không Chung Cạnh
Đề. Bảng , mỗi ô được phép chọn hoặc không. Đếm số tập ô được phép, không hai ô nào chung cạnh, modulo .
Lời giải. Quy hoạch động theo hàng, trạng thái là mặt nạ các ô chọn ở hàng vừa xét. Hai ô trong một hàng không được kề nhau; hai hàng liền nhau không được chọn cùng cột.
Bước 1: mặt nạ ô được phép. fr[i] bật bit khi ô được chọn.
if (x) fr[i] |= 1 << j;
Bước 2: lọc mặt nạ một hàng. Mặt nạ không có hai bit kề khi s & (s << 1) bằng 0. Với chỉ còn 377 mặt nạ; lọc một lần, dùng cho mọi hàng.
for (ll s = 0; s < (1 << m); s++)
if (!(s & (s << 1))) ok.push_back(s);
Bước 3: chuyển hàng. Mặt nạ phải nằm gọn trong fr[i]; cộng dp của mọi mặt nạ ở hàng trước không giao . dp[0] = 1 trước hàng đầu, coi như có một hàng rỗng phía trên.
for (ll s : ok) {
if ((s & fr[i]) != s) continue;
for (ll t : ok)
if (!(s & t) && dp[t]) nd[s] = (nd[s] + dp[t]) % MOD;
}
Đáp án cộng mọi trạng thái của hàng cuối, kể cả mặt nạ rỗng (tập không chọn ô nào cũng được đếm).
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll MOD = 1e8;
ll n, m, fr[15], dp[1 << 12], nd[1 << 12];
vector<ll> ok;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m;
for (ll i = 0; i < n; i++)
for (ll j = 0; j < m; j++) {
ll x;
cin >> x;
if (x) fr[i] |= 1 << j;
}
for (ll s = 0; s < (1 << m); s++)
if (!(s & (s << 1))) ok.push_back(s);
dp[0] = 1;
for (ll i = 0; i < n; i++) {
for (ll s : ok) nd[s] = 0;
for (ll s : ok) {
if ((s & fr[i]) != s) continue;
for (ll t : ok)
if (!(s & t) && dp[t]) nd[s] = (nd[s] + dp[t]) % MOD;
}
for (ll s : ok) dp[s] = nd[s];
}
ll ans = 0;
for (ll s : ok) ans = (ans + dp[s]) % MOD;
cout << ans << '\n';
}
1 3
1 1 1
5
.
Ràng buộc kéo dài hai hàng
Nếu ràng buộc nối hàng với cả hàng và (trạm cách nhau tới 2 ô theo cột), trạng thái phải giữ hai hàng gần nhất: với là mặt nạ hàng , hàng . Chuyển tiếp thử mặt nạ cho hàng cũ: điều kiện là đôi một không giao. Số mặt nạ tự hợp lệ rất nhỏ (không hai bit trong khoảng cách 2: với chỉ 60), nên vẫn nhanh. Lọc trước cũng giúp bỏ các mặt nạ đặt trạm lên ô cấm.
Ví dụTrạm Tín Hiệu Trên Lưới
Đề. Lưới hàng, cột, ô P đặt được trạm. Hai trạm cùng hàng hay cùng cột cách nhau 1 hoặc 2 ô thì nhiễu nhau. Đặt nhiều trạm nhất.
Lời giải. Trạm ở hàng nhiễu với trạm cùng cột ở hàng và , nên trạng thái phải nhớ mặt nạ của cả hai hàng gần nhất.
Bước 1: mặt nạ một hàng. Không có hai trạm cách nhau 1 hay 2 ô: s & (s << 1) và s & (s << 2) đều bằng 0. Với chỉ còn 60 mặt nạ, nên trạng thái dùng chỉ số trong danh sách ok thay cho chính mặt nạ.
for (ll s = 0; s < (1 << m); s++)
if (!(s & (s << 1)) && !(s & (s << 2))) ok.push_back(s);
Bước 2: trạng thái. dp[a][b] là số trạm lớn nhất khi hàng hiện tại dùng ok[a], hàng trước dùng ok[b]; là trạng thái không tồn tại. Bắt đầu với hai hàng rỗng: dp[0][0] = 0.
Bước 3: thêm hàng mới . Phải nằm trong ô P, không giao hàng ngay trên () và hàng trên nữa (). Trạng thái mới là , cộng số trạm của .
for (ll x = 0; x < k; x++) {
if ((ok[x] & fr[i]) != ok[x]) continue;
ll c = __builtin_popcountll(ok[x]);
for (ll a = 0; a < k; a++) {
if (ok[x] & ok[a]) continue;
for (ll b = 0; b < k; b++) {
if (dp[a][b] < 0 || (ok[x] & ok[b])) continue;
nd[x][a] = max(nd[x][a], dp[a][b] + c);
}
}
}
Đáp án là lớn nhất trên mọi trạng thái sau hàng cuối.
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
ll n, m, fr[105], dp[65][65], nd[65][65];
vector<ll> ok;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m;
for (ll i = 0; i < n; i++) {
string s;
cin >> s;
for (ll j = 0; j < m; j++)
if (s[j] == 'P') fr[i] |= 1 << j;
}
for (ll s = 0; s < (1 << m); s++)
if (!(s & (s << 1)) && !(s & (s << 2))) ok.push_back(s);
ll k = ok.size();
for (ll a = 0; a < k; a++)
for (ll b = 0; b < k; b++) dp[a][b] = -1;
dp[0][0] = 0;
for (ll i = 0; i < n; i++) {
for (ll a = 0; a < k; a++)
for (ll b = 0; b < k; b++) nd[a][b] = -1;
for (ll x = 0; x < k; x++) {
if ((ok[x] & fr[i]) != ok[x]) continue;
ll c = __builtin_popcountll(ok[x]);
for (ll a = 0; a < k; a++) {
if (ok[x] & ok[a]) continue;
for (ll b = 0; b < k; b++) {
if (dp[a][b] < 0 || (ok[x] & ok[b])) continue;
nd[x][a] = max(nd[x][a], dp[a][b] + c);
}
}
}
for (ll a = 0; a < k; a++)
for (ll b = 0; b < k; b++) dp[a][b] = nd[a][b];
}
ll ans = 0;
for (ll a = 0; a < k; a++)
for (ll b = 0; b < k; b++) ans = max(ans, dp[a][b]);
cout << ans << '\n';
}
3 5
PHPHP
PPPPP
HPHPH
4
bước.
Thêm thông tin vào bit cho bài tối ưu
Bit trên biên không chỉ nói “đã phủ hay chưa”; nó có thể mang bất kỳ thông tin nhị phân nào cần cho ô kế tiếp. Phủ sàn bằng ít thanh thẳng nhất: mỗi ô thuộc một thanh ngang hoặc một thanh dọc, và một thanh mới tốn 1 khi không nối tiếp được thanh cùng hướng ở ô bên cạnh. Duyệt theo hàng, bit là “ô gần nhất đã xét ở cột có thuộc thanh dọc không”. Ô đi dọc thì miễn phí nếu ô trên là sàn và bit bật; đi ngang thì miễn phí nếu ô trái là sàn và bit (đã cập nhật ở hàng này) tắt. Ô chắn đặt bit về 0 và không tốn gì.
Ví dụPhủ Sàn Bằng Các Thanh Thẳng
Đề. Bảng có ô sàn . và cột chắn #. Phủ mọi ô sàn bằng các thanh thẳng hoặc không chồng nhau, dùng ít thanh nhất.
Lời giải. Mỗi ô sàn thuộc một thanh ngang hoặc một thanh dọc. Số thanh bằng số ô “mở đầu” một thanh: ô dọc mà ô trên không đi dọc, ô ngang mà ô trái không đi ngang. Duyệt ô theo hàng, mặt nạ s nhớ hướng của ô gần nhất: bit bật khi ô gần nhất đã xét ở cột đi dọc (với cột là ô ở hàng hiện tại, với cột là ô ở hàng trên). là số thanh ít nhất.
Bước 1: ô chắn. Không thanh nào đi qua: xóa bit , không tốn gì.
if (g[r][c] == '#') {
ll t = s & ~(1LL << c);
nd[t] = min(nd[t], v);
continue;
}
Bước 2: ô sàn đi dọc. Miễn phí khi ô trên là sàn và cũng đi dọc (bit đang bật), ngược lại mở thanh mới tốn 1. Bật bit .
ll up = r > 0 && g[r - 1][c] == '.' && (s >> c & 1);
ll t = s | (1LL << c);
nd[t] = min(nd[t], v + !up);
Bước 3: ô sàn đi ngang. Miễn phí khi ô trái là sàn và đi ngang (bit , đã cập nhật ở hàng này, đang tắt). Xóa bit .
ll left = c > 0 && g[r][c - 1] == '.' && !(s >> (c - 1) & 1);
t = s & ~(1LL << c);
nd[t] = min(nd[t], v + !left);
Đáp án là nhỏ nhất trên mọi mặt nạ sau ô cuối; bảng không có ô sàn cho 0.
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll INF = 1e18;
ll n, m, dp[1 << 10], nd[1 << 10];
string g[12];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m;
for (ll i = 0; i < n; i++) cin >> g[i];
ll full = 1 << m;
for (ll s = 0; s < full; s++) dp[s] = INF;
dp[0] = 0;
for (ll r = 0; r < n; r++)
for (ll c = 0; c < m; c++) {
for (ll s = 0; s < full; s++) nd[s] = INF;
for (ll s = 0; s < full; s++) {
if (dp[s] == INF) continue;
ll v = dp[s];
if (g[r][c] == '#') {
ll t = s & ~(1LL << c);
nd[t] = min(nd[t], v);
continue;
}
ll up = r > 0 && g[r - 1][c] == '.' && (s >> c & 1);
ll t = s | (1LL << c);
nd[t] = min(nd[t], v + !up);
ll left = c > 0 && g[r][c - 1] == '.' && !(s >> (c - 1) & 1);
t = s & ~(1LL << c);
nd[t] = min(nd[t], v + !left);
}
for (ll s = 0; s < full; s++) dp[s] = nd[s];
}
ll ans = INF;
for (ll s = 0; s < full; s++) ans = min(ans, dp[s]);
cout << ans << '\n';
}
2 2
..
..
2
trạng thái nhân 100 ô, khoảng bước.
Bảng rất dài: nhân ma trận chuyển
Khi chiều dài tới , chuyển tiếp giữa hai cột liên tiếp giống hệt nhau ở mọi cột, nên là một ma trận kích thước : là số cách điền một cột khi các ô trong đã bị domino ngang từ cột trước chiếm và các ô trong bắt đầu domino ngang sang cột sau. Đáp án là , tính bằng lũy thừa nhị phân ma trận (bài 18) trong . Mỗi ô đếm cách phủ các ô còn trống trong cột bằng những miếng nằm gọn trong cột.
Ví dụLát Bảng Rất Dài
Đề. Bảng hàng, cột, lát bằng ô , domino ngang và domino dọc. Đếm số cách, modulo .
Lời giải. tới nên không thể đi từng cột; nhưng mọi cột chuyển như nhau, nên dựng ma trận chuyển một lần rồi lũy thừa. Trạng thái giữa hai cột là mặt nạ các ô bị domino ngang nối qua.
Bước 1: ma trận chuyển. là các ô của cột bị domino ngang từ cột trước chiếm, là các ô bắt đầu domino ngang sang cột sau. và giao nhau thì . Các ô còn lại chia thành các đoạn liền; đoạn dài phủ bằng ô đơn và domino dọc theo cách, với , . Vòng r chạy tới (dư một ô) để chốt đoạn cuối.
for (ll a = 0; a < S; a++)
for (ll b = 0; b < S; b++) {
if (a & b) continue;
ll ways = 1, run = 0;
for (ll r = 0; r <= h; r++) {
if (r < h && !((a | b) >> r & 1)) run++;
else {
ways *= f[run];
run = 0;
}
}
T[a][b] = ways % MOD;
}
Bước 2: nhân ma trận. Ma trận theo mô-đun; bỏ qua phần tử 0 của a để nhanh hơn.
vector<vector<ll>> mul(vector<vector<ll>> a, vector<vector<ll>> b) {
vector<vector<ll>> c(S, vector<ll>(S, 0));
for (ll i = 0; i < S; i++)
for (ll k = 0; k < S; k++) {
if (!a[i][k]) continue;
for (ll j = 0; j < S; j++) c[i][j] = (c[i][j] + a[i][k] * b[k][j]) % MOD;
}
return c;
}
Bước 3: lũy thừa nhị phân. R bắt đầu là ma trận đơn vị. là số cách đi từ “không ô nào bị chiếm” trước cột đầu tới “không ô nào sang cột ảo” sau cột cuối.
for (; w; w >>= 1) {
if (w & 1) R = mul(R, T);
T = mul(T, T);
}
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll MOD = 998244353;
ll h, w, S, f[10];
vector<vector<ll>> T;
vector<vector<ll>> mul(vector<vector<ll>> a, vector<vector<ll>> b) {
vector<vector<ll>> c(S, vector<ll>(S, 0));
for (ll i = 0; i < S; i++)
for (ll k = 0; k < S; k++) {
if (!a[i][k]) continue;
for (ll j = 0; j < S; j++) c[i][j] = (c[i][j] + a[i][k] * b[k][j]) % MOD;
}
return c;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> h >> w;
S = 1 << h;
f[0] = f[1] = 1;
for (ll k = 2; k < 10; k++) f[k] = f[k - 1] + f[k - 2];
T.assign(S, vector<ll>(S, 0));
for (ll a = 0; a < S; a++)
for (ll b = 0; b < S; b++) {
if (a & b) continue;
ll ways = 1, run = 0;
for (ll r = 0; r <= h; r++) {
if (r < h && !((a | b) >> r & 1)) run++;
else {
ways *= f[run];
run = 0;
}
}
T[a][b] = ways % MOD;
}
vector<vector<ll>> R(S, vector<ll>(S, 0));
for (ll i = 0; i < S; i++) R[i][i] = 1;
for (; w; w >>= 1) {
if (w & 1) R = mul(R, T);
T = mul(T, T);
}
cout << R[0][0] << '\n';
}
2 2
7
phép nhân.
Khi đi thi
- Lưới có một cạnh không quá 10 đến 12: mặt nạ trên cạnh ngắn. Theo từng ô khi chuyển tiếp phụ thuộc ô kề trước đó; theo hàng khi ràng buộc chỉ giữa hai hàng và có thể lọc trước các mặt nạ hợp lệ.
- Viết rõ nghĩa của từng bit trước khi viết chuyển tiếp: bit nói về ô nào của biên, ở cột nào, ý nghĩa gì; phần lớn lỗi nằm ở chỗ bit đổi nghĩa khi biên dời.
- Cạnh dài tới : ma trận chuyển giữa hai cột và lũy thừa nhị phân; trạng thái là thoải mái, đã nặng.
Tóm tắt
- Biên giữa phần đã xử lý và chưa xử lý mã hóa bằng mặt nạ; theo từng ô, biên gãy tại ô đang xét, cho lát domino.
- Theo hàng: lọc mặt nạ tự hợp lệ, kiểm hai hàng bằng phép bit.
- Ràng buộc hai hàng: giữ hai mặt nạ trong trạng thái.
- Bit có thể mang thông tin khác như hướng của ô phía trên, cho bài tối ưu.
- Bảng rất dài: ma trận chuyển giữa các cột, lũy thừa nhị phân.
Bài tập
(s << 1) & t và (s >> 1) & t đều bằng 0.* chưa dọn trong cửa sổ 4 cột tới; tại mỗi cột thử đặt các hình vuông bắt đầu ở cột đó.Đọc thêm
- Quy hoạch động bitmaskVNOI WikiMặt nạ bit trong quy hoạch động, viết bằng tiếng Việt.
- Dynamic Programming on Broken Profilecp-algorithmsLát domino bằng biên gãy.
- 插头 DPOI WikiQuy hoạch động trên biên gãy cho chu trình và đường đi.
- DP on Broken ProfileUSACO GuideBài luyện biên gãy.