Cho biến logic, mỗi biến nhận giá trị đúng hoặc sai, và điều kiện, mỗi điều kiện có dạng “ hoặc ”, trong đó , là một biến hoặc phủ định của một biến. Tìm một cách gán giá trị cho các biến thỏa mọi điều kiện, hoặc cho biết không có, trong .
Đây là bài toán 2-SAT. Mỗi điều kiện “ hoặc ” được viết thành hai câu “nếu sai thì đúng” và “nếu sai thì đúng”, và các câu “nếu… thì…” này tạo thành một đồ thị có hướng gọi là đồ thị kéo theo. Tìm các thành phần liên thông mạnh của đồ thị (bài 3.9) là biết ngay có nghiệm hay không, và thứ tự của các thành phần cho luôn một cách gán. Phần lớn công sức khi giải bài là dịch đề thành các điều kiện hai vế như vậy: chọn một trong hai phương án, không chọn cùng lúc hai thứ xung đột, các phép AND, OR, XOR giữa hai biến.
IBài toán 2-SAT và cách thử hết
Một biến logic nhận một trong hai giá trị: đúng hoặc sai. Với biến , ta viết (đọc là “không ”) cho phủ định của nó: đúng khi sai và ngược lại. Mỗi biến và phủ định của nó được gọi là các literal. Một mệnh đề là một điều kiện dạng (đọc là “ hoặc ”), với , là hai literal; mệnh đề được thỏa khi ít nhất một trong hai literal đúng.
Ví dụ với ba biến và ba mệnh đề
cách gán đúng, sai, đúng thỏa cả ba mệnh đề.
Cách dễ nghĩ ra nhất là thử hết cách gán, mỗi cách kiểm mệnh đề, tốn . Với đã cần hơn cách gán, nên chỉ dùng được khi rất nhỏ. Điểm đặc biệt của 2-SAT là mỗi mệnh đề chỉ có hai literal, và nhờ vậy có cách giải tuyến tính bằng đồ thị.
IIĐồ thị kéo theo
Mệnh đề có nghĩa là: nếu sai thì phải đúng, và nếu sai thì phải đúng. Viết bằng ký hiệu kéo theo :
Ta dựng đồ thị kéo theo: mỗi literal là một đỉnh, nên có đỉnh; mỗi mệnh đề thêm hai cung và . Một cung nghĩa là “nếu đúng thì đúng”. Đi theo một đường , các câu “nếu… thì…” nối tiếp nhau, nên nếu đúng thì mọi literal trên đường, kể cả , đều phải đúng.
Trong code, biến (đánh số từ ) có hai đỉnh: literal là đỉnh , literal là đỉnh . Hai đỉnh này chỉ khác nhau ở bit cuối, nên phủ định của đỉnh là đỉnh , viết trong C++ là u ^ 1. Hàm thêm một mệnh đề chỉ có hai dòng:
void clause(ll a, ll b) {
g[a ^ 1].push_back(b);
g[b ^ 1].push_back(a);
}
Độ phức tạp: cho mỗi mệnh đề, cả đồ thị có đỉnh và cung.
Hỏi nhanh
Mệnh đề được thêm vào đồ thị kéo theo.
Hai cung nào được thêm?
Mệnh đề cho hai cung và . Ở đây và , nên hai cung là (nếu sai thì phải sai) và (nếu đúng thì phải đúng).
Lựa chọn và là hai cung của mệnh đề , quên mất dấu phủ định của .
Trong mô phỏng dưới đây, hàng trên là các literal , hàng dưới là . Mỗi bước thêm hai cung của một mệnh đề; bước cuối tô các thành phần liên thông mạnh và đọc ra cách gán, sẽ nói ở mục sau.
IIIKiểm tra có nghiệm và dựng cách gán
Nhắc lại từ bài 3.9: hai đỉnh liên thông mạnh khi mỗi đỉnh đi tới được đỉnh kia, và quan hệ này chia các đỉnh thành các thành phần liên thông mạnh.
Nếu và nằm trong cùng một thành phần liên thông mạnh, thì có đường từ tới và có đường từ tới . Gán đúng thì phải đúng, tức sai; gán sai thì đúng, kéo theo đúng. Cả hai cách đều mâu thuẫn, nên các mệnh đề không có nghiệm. Điều bất ngờ là chiều ngược lại cũng đúng: nếu không biến nào nằm cùng thành phần với phủ định của nó, thì luôn có nghiệm, và dựng được ngay.
Thuật toán Kosaraju ở bài 3.9 đánh số các thành phần theo thứ tự tô pô: nếu có cung từ thành phần mang số tới thành phần mang số khác nó thì . Cách gán là: với mỗi biến , chọn literal nằm ở thành phần có số lớn hơn, tức là đứng sau trong thứ tự tô pô. Nghĩa là đúng khi số thành phần của lớn hơn số thành phần của , và sai trong trường hợp ngược lại.
Chứng minh
Ký hiệu là số thành phần của đỉnh . Mọi cung đều có : cùng thành phần thì bằng nhau, khác thành phần thì theo thứ tự tô pô.
Giả sử có một mệnh đề không được thỏa, tức là và đều được gán sai. Theo cách gán, được chọn nên , và được chọn nên . Mệnh đề này cho hai cung và , nên và . Ghép lại:
vô lý. Vậy mọi mệnh đề đều được thỏa.
Mô phỏng dưới đây là một hệ không có nghiệm: kéo theo cả lẫn , còn kéo theo cả lẫn . Bạn có thể sửa một mệnh đề để hệ có nghiệm:
Ví dụCấu Hình Tùy Chọn
Đề. Có tùy chọn, mỗi tùy chọn bật hoặc tắt, và yêu cầu. Mỗi yêu cầu gồm hai điều kiện dạng + x (tùy chọn bật) hoặc - x (tùy chọn tắt), và cần ít nhất một trong hai điều kiện đúng. In + hoặc - cho từng tùy chọn, hoặc IMPOSSIBLE nếu không có cách nào.
Lời giải. Mỗi yêu cầu đúng là một mệnh đề . Tùy chọn (đánh số từ ) bật là đỉnh , tắt là đỉnh . Ta dựng đồ thị kéo theo cùng đồ thị ngược của nó, chạy Kosaraju như bài 3.9: lần duyệt thứ nhất ghi thứ tự xong, lần thứ hai duyệt đồ thị ngược theo thứ tự xong giảm dần và đánh số thành phần từ . Sau đó kiểm từng biến, rồi đọc cách gán theo quy tắc ở trên. Đồ thị kéo theo có thể có hàng triệu cung, nên danh sách kề lưu kiểu int cho đỡ tốn bộ nhớ.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 2e5 + 5;
ll n, m, k, vis[N], comp[N];
vector<int> g[N], rg[N], ord;
void clause(ll a, ll b) {
g[a ^ 1].push_back(b);
g[b ^ 1].push_back(a);
rg[b].push_back(a ^ 1);
rg[a].push_back(b ^ 1);
}
void dfs1(ll u) {
vis[u] = 1;
for (int v : g[u])
if (!vis[v]) dfs1(v);
ord.push_back(u);
}
void dfs2(ll u) {
comp[u] = k;
for (int v : rg[u])
if (!comp[v]) dfs2(v);
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m;
for (ll i = 1; i <= n; i++) {
char s1, s2;
ll x1, x2;
cin >> s1 >> x1 >> s2 >> x2;
clause(2 * (x1 - 1) + (s1 == '-'), 2 * (x2 - 1) + (s2 == '-'));
}
for (ll u = 0; u < 2 * m; u++)
if (!vis[u]) dfs1(u);
for (ll i = 2 * m - 1; i >= 0; i--)
if (!comp[ord[i]]) {
k++;
dfs2(ord[i]);
}
for (ll x = 0; x < m; x++)
if (comp[2 * x] == comp[2 * x + 1]) {
cout << "IMPOSSIBLE\n";
return 0;
}
for (ll x = 0; x < m; x++) cout << (comp[2 * x] > comp[2 * x + 1] ? '+' : '-') << ' ';
cout << '\n';
}
4 3
+ 1 + 1
- 2 - 2
+ 3 + 3
+ 1 - 2
+ - +
Độ phức tạp: , vì Kosaraju duyệt mỗi đỉnh và mỗi cung hai lần; với cần khoảng phép tính.
Hỏi nhanh
Một bạn viết chương trình như ví dụ trên, nhưng đọc cách gán với dấu so sánh ngược lại: tùy chọn bật khi số thành phần của literal “bật” nhỏ hơn số thành phần của literal “tắt”. Với đúng một yêu cầu + 1 + 1, tức là ép tùy chọn phải bật, chương trình in ra -.
Vì sao chương trình sai?
Mệnh đề cho cung : nếu tùy chọn tắt thì nó phải bật. Kosaraju đánh số thành phần theo thứ tự tô pô, nên thành phần của mang số nhỏ hơn thành phần của . Quy tắc đúng chọn literal ở thành phần số lớn hơn, tức là , và in +; đảo dấu so sánh thì chọn nhầm .
Đồ thị có hai đỉnh nhưng chúng nằm ở hai thành phần khác nhau, vì không có đường từ về . Kosaraju cũng không có gì ngẫu nhiên. Nếu dùng thuật toán Tarjan thay cho Kosaraju thì các thành phần được đánh số theo thứ tự tô pô ngược, và dấu so sánh phải đảo lại; khi đổi thuật toán, luôn kiểm lại quy tắc này trên một ví dụ nhỏ.
IVDịch các ràng buộc khác thành mệnh đề
Nhiều bài không cho sẵn mệnh đề, mà cho các ràng buộc giữa hai biến. Mỗi ràng buộc như vậy thường dịch được thành một hoặc hai mệnh đề:
- Ép đúng: mệnh đề , cho cung (nếu sai thì phải đúng).
- bằng : hai mệnh đề và .
- khác : hai mệnh đề và .
- Không được cùng đúng: mệnh đề .
Hỏi nhanh
Hai biến và phải nhận giá trị khác nhau: một biến đúng thì biến kia sai.
Cặp mệnh đề nào diễn tả đúng ràng buộc này?
Mệnh đề cấm trường hợp cả hai cùng sai, mệnh đề cấm trường hợp cả hai cùng đúng; còn lại đúng hai trường hợp một đúng một sai.
Cặp và là ràng buộc “bằng nhau”. Cặp và ép đúng. Một mình vẫn cho phép cả hai cùng sai.
Ví dụMạng Phép Toán
Đề. Có biến nhận giá trị hoặc và ràng buộc dạng , với phép toán là AND, OR hoặc XOR và là hoặc . Hỏi có cách gán thỏa mọi ràng buộc không.
Lời giải. Coi giá trị là đúng. Mỗi ràng buộc dịch thành một hoặc hai mệnh đề theo bảng trên:
- AND bằng : ép cả hai đúng. AND bằng : không được cùng đúng.
- OR bằng : . OR bằng : ép cả hai sai.
- XOR bằng : khác . XOR bằng : bằng .
Sau đó dựng đồ thị kéo theo trên đỉnh và kiểm như ví dụ trước. Mỗi ràng buộc cho tới hai mệnh đề, nên đồ thị có tới cung, cộng thêm chừng ấy cung ở đồ thị ngược.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 2005;
ll n, m, k, vis[N], comp[N];
vector<int> g[N], rg[N], ord;
void clause(ll a, ll b) {
g[a ^ 1].push_back(b);
g[b ^ 1].push_back(a);
rg[b].push_back(a ^ 1);
rg[a].push_back(b ^ 1);
}
void dfs1(ll u) {
vis[u] = 1;
for (int v : g[u])
if (!vis[v]) dfs1(v);
ord.push_back(u);
}
void dfs2(ll u) {
comp[u] = k;
for (int v : rg[u])
if (!comp[v]) dfs2(v);
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m;
for (ll i = 0; i < m; i++) {
ll a, b, c;
string op;
cin >> a >> b >> c >> op;
ll A = 2 * a, B = 2 * b;
if (op == "AND") {
if (c) clause(A, A), clause(B, B);
else clause(A ^ 1, B ^ 1);
} else if (op == "OR") {
if (c) clause(A, B);
else clause(A ^ 1, A ^ 1), clause(B ^ 1, B ^ 1);
} else {
if (c) clause(A, B), clause(A ^ 1, B ^ 1);
else clause(A, B ^ 1), clause(A ^ 1, B);
}
}
for (ll u = 0; u < 2 * n; u++)
if (!vis[u]) dfs1(u);
for (ll i = 2 * n - 1; i >= 0; i--)
if (!comp[ord[i]]) {
k++;
dfs2(ord[i]);
}
bool ok = 1;
for (ll x = 0; x < n; x++)
if (comp[2 * x] == comp[2 * x + 1]) ok = 0;
cout << (ok ? "YES" : "NO") << '\n';
}
3 3
0 1 1 XOR
1 2 1 AND
0 2 1 OR
YES
Độ phức tạp: ; với cần khoảng phép tính để dựng và duyệt cả hai đồ thị.
Với đồ thị có hàng triệu cung, cách lưu danh sách kề ảnh hưởng nhiều tới tốc độ:
Lưu các cung trong một mảng liền nhau (đếm bậc của từng đỉnh, cộng dồn để biết chỗ bắt đầu, rồi điền cung vào) nhanh hơn vector<int> g[N] khoảng lần, tính cả thời gian dựng đồ thị. Khi đọc được hết mệnh đề trước rồi mới chạy, và giới hạn thời gian chặt, nên cân nhắc cách lưu này.
Sử dụng kiến thức này cũng có thể giải bài có một biến nhận giá trị nguyên từ tới , chẳng hạn mức công suất. Dùng literal “” với , nối thành chuỗi kéo theo “” “”. Một điều kiện như “chọn trạm thì ” khi đó thành hai mệnh đề: chọn trạm kéo theo “”, và chọn trạm kéo theo phủ định của “”.
VChọn một trong hai phương án có xung đột
Dạng hay gặp nhất của 2-SAT: mỗi đối tượng phải chọn đúng một trong hai phương án, phương án hoặc phương án , và một số cặp phương án của hai đối tượng khác nhau xung đột, không được chọn cùng lúc. Ta dùng một biến cho mỗi đối tượng: literal “đối tượng chọn phương án ”. Với mỗi cặp xung đột “đối tượng chọn ” và “đối tượng chọn ”, thêm mệnh đề cấm: không ( chọn ) hoặc không ( chọn ). Duyệt mọi cặp đối tượng và mọi cặp phương án tạo ra mệnh đề, đủ dùng khi cỡ vài nghìn.
Câu hỏi nhanh dành cho tài khoản Pro
Mở khóa bằng tài khoản ProVí dụCờ Trên Trục Số
Đề. Có lá cờ, cờ thứ đặt ở tọa độ hoặc . Mọi cặp cờ phải cách nhau ít nhất . In Yes cùng một cách đặt, hoặc in No nếu không đặt được.
Lời giải. Literal nghĩa là “cờ đặt ở phương án ”, với phương án là và phương án là ; literal đúng là phủ định của literal , đúng như cách đánh số ở trên. Với mỗi cặp cờ và mỗi cặp phương án cách nhau dưới , thêm mệnh đề cấm trên phủ định của cả hai lựa chọn. Có tới mệnh đề, rồi giải 2-SAT như các ví dụ trước; cờ đặt ở khi số thành phần của đỉnh lớn hơn của đỉnh .
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 2005;
ll n, d, k, vis[N], comp[N], pos[N][2];
vector<int> g[N], rg[N], ord;
void clause(ll a, ll b) {
g[a ^ 1].push_back(b);
g[b ^ 1].push_back(a);
rg[b].push_back(a ^ 1);
rg[a].push_back(b ^ 1);
}
void dfs1(ll u) {
vis[u] = 1;
for (int v : g[u])
if (!vis[v]) dfs1(v);
ord.push_back(u);
}
void dfs2(ll u) {
comp[u] = k;
for (int v : rg[u])
if (!comp[v]) dfs2(v);
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> d;
for (ll i = 0; i < n; i++) cin >> pos[i][0] >> pos[i][1];
for (ll i = 0; i < n; i++)
for (ll j = i + 1; j < n; j++)
for (ll p = 0; p < 2; p++)
for (ll q = 0; q < 2; q++)
if (llabs(pos[i][p] - pos[j][q]) < d) clause((2 * i + p) ^ 1, (2 * j + q) ^ 1);
for (ll u = 0; u < 2 * n; u++)
if (!vis[u]) dfs1(u);
for (ll i = 2 * n - 1; i >= 0; i--)
if (!comp[ord[i]]) {
k++;
dfs2(ord[i]);
}
for (ll i = 0; i < n; i++)
if (comp[2 * i] == comp[2 * i + 1]) {
cout << "No\n";
return 0;
}
cout << "Yes\n";
for (ll i = 0; i < n; i++) cout << (comp[2 * i] > comp[2 * i + 1] ? pos[i][0] : pos[i][1]) << '\n';
}
2 8
3 13
5 7
Yes
13
5
Độ phức tạp: cho việc tạo mệnh đề và chạy Kosaraju; với cần khoảng phép tính.
Sử dụng kiến thức này cũng có thể giải bài: mỗi sự kiện chọn tổ chức nghi lễ ở đầu hoặc ở cuối khoảng thời gian của nó, và hai nghi lễ không được chồng lên nhau. Hai phương án xung đột khi hai khoảng thời gian giao nhau. Khi đề hỏi khoảng cách lớn nhất có thể (thay vì cho trước ), ta chặt nhị phân trên (bài 0.6) và mỗi bước kiểm bằng 2-SAT.
Bài nâng cao
Xem 2 bài nâng cao
Ví dụTrung Vị Trên Ba Hàng
Đề. Có biến , mỗi biến nhận giá trị hoặc , và một bảng hàng, cột. Mỗi ô ghi hoặc , nghĩa là ô đó có giá trị hoặc . Hỏi có cách chọn các để trong mọi cột, trung vị của ba giá trị (giá trị đứng giữa sau khi sắp xếp) bằng không. Có nhiều bộ dữ liệu.
Lời giải. Coi là biến đúng. Ô ghi có giá trị khi literal đúng, ô ghi có giá trị khi literal đúng. Như vậy mỗi ô ứng với một literal, và ô có giá trị đúng khi literal của nó đúng.
Ba giá trị chỉ là hoặc , nên trung vị bằng khi và chỉ khi có ít nhất hai giá trị bằng . Điều kiện “ít nhất hai trong ba” nghe như cần ba literal, nhưng nó tương đương với “trong mỗi cặp ô của cột, có ít nhất một ô bằng ”: nếu có ít nhất hai ô bằng thì cặp nào cũng chứa một ô như vậy; nếu có nhiều nhất một ô bằng thì hai ô còn lại tạo thành một cặp toàn . Vậy mỗi cột cho đúng ba mệnh đề hai vế, mỗi cặp ô một mệnh đề, và bài toán thành 2-SAT với mệnh đề.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 1005;
ll t, n, k, vis[N], comp[N], c[3][N];
vector<int> g[N], rg[N], ord;
void clause(ll a, ll b) {
g[a ^ 1].push_back(b);
g[b ^ 1].push_back(a);
rg[b].push_back(a ^ 1);
rg[a].push_back(b ^ 1);
}
void dfs1(ll u) {
vis[u] = 1;
for (int v : g[u])
if (!vis[v]) dfs1(v);
ord.push_back(u);
}
void dfs2(ll u) {
comp[u] = k;
for (int v : rg[u])
if (!comp[v]) dfs2(v);
}
ll lit(ll v) {
return v > 0 ? 2 * (v - 1) : 2 * (-v - 1) + 1;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> t;
while (t--) {
cin >> n;
for (ll u = 0; u < 2 * n; u++) g[u].clear(), rg[u].clear(), vis[u] = comp[u] = 0;
ord.clear();
k = 0;
for (ll r = 0; r < 3; r++)
for (ll j = 0; j < n; j++) cin >> c[r][j];
for (ll j = 0; j < n; j++) {
ll a = lit(c[0][j]), b = lit(c[1][j]), d = lit(c[2][j]);
clause(a, b), clause(a, d), clause(b, d);
}
for (ll u = 0; u < 2 * n; u++)
if (!vis[u]) dfs1(u);
for (ll i = 2 * n - 1; i >= 0; i--)
if (!comp[ord[i]]) {
k++;
dfs2(ord[i]);
}
bool ok = 1;
for (ll x = 0; x < n; x++)
if (comp[2 * x] == comp[2 * x + 1]) ok = 0;
cout << (ok ? "YES" : "NO") << '\n';
}
}
2
2
1 2
1 -2
-1 2
2
1 -1
1 -1
2 2
YES
NO
Độ phức tạp: cho mỗi bộ dữ liệu, vì có đỉnh và cung; với cần khoảng phép tính mỗi bộ.
Ví dụKết Quả Bắt Buộc
Đề. Có đề xuất, mỗi đề xuất nhận kết quả Y hoặc N, và yêu cầu dạng “đề xuất nhận hoặc đề xuất nhận ”. Nếu không có cách nào thỏa mọi yêu cầu, in IMPOSSIBLE. Ngược lại, với mỗi đề xuất in Y nếu mọi cách thỏa đều cho nó Y, in N nếu mọi cách thỏa đều cho nó N, và in ? nếu cả hai kết quả đều có thể.
Lời giải. Mỗi yêu cầu là một mệnh đề. Bài này không chỉ hỏi một cách gán, mà hỏi một literal có bắt buộc đúng hay không. Ta có thể thấy rằng, khi các mệnh đề có nghiệm:
- literal bắt buộc đúng khi và chỉ khi có đường từ tới trong đồ thị kéo theo.
Nếu có đường đó thì gán sai sẽ kéo theo đúng, mâu thuẫn, nên phải đúng. Chiều ngược lại cần một lập luận dài hơn (xem ô Chứng minh). Với mỗi đề xuất , ta kiểm hai đường: từ “ nhận N” tới “ nhận Y” thì in Y; từ “ nhận Y” tới “ nhận N” thì in N; không có đường nào thì in ?. Nếu có cả hai đường thì và phủ định của nó nằm cùng một thành phần liên thông mạnh, các yêu cầu không có nghiệm.
Đồ thị chỉ có đỉnh và cung, nên ta duyệt riêng từ từng đỉnh, không cần tìm thành phần liên thông mạnh.
Chứng minh
Giả sử các mệnh đề có nghiệm và không có đường từ tới . Ta dựng một nghiệm có đúng. Gọi là tập các literal tới được từ , và gán đúng mọi literal trong .
không chứa một literal cùng phủ định của nó. Đồ thị kéo theo có tính đối xứng: mỗi cung đi cùng cung , vì hai cung của một mệnh đề là như vậy. Nếu chứa cả lẫn , thì có đường , nên có đường đối xứng ; nối với đường ta được đường từ tới , trái giả thiết.
Mọi mệnh đề đều thỏa được. Xét mệnh đề . Nếu bị gán sai vì thuộc , thì cung cho thuộc , nên đúng; tương tự nếu bị gán sai. Mệnh đề có một literal được gán đúng thì đã thỏa. Các mệnh đề còn lại chỉ chứa những biến chưa được gán; chúng là một phần của các mệnh đề ban đầu nên được thỏa bởi chính một nghiệm ban đầu, giữ nguyên giá trị của các biến chưa gán đó.
Vậy có một nghiệm với đúng, tức là không bắt buộc đúng. Lật lại: bắt buộc đúng thì phải có đường từ tới .
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 2005;
ll n, m, vis[N];
vector<ll> g[N];
void clause(ll a, ll b) {
g[a ^ 1].push_back(b);
g[b ^ 1].push_back(a);
}
bool reach(ll s, ll t) {
for (ll u = 0; u < 2 * n; u++) vis[u] = 0;
vector<ll> st = {s};
vis[s] = 1;
while (!st.empty()) {
ll u = st.back();
st.pop_back();
if (u == t) return 1;
for (ll v : g[u])
if (!vis[v]) {
vis[v] = 1;
st.push_back(v);
}
}
return 0;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m;
for (ll i = 1; i <= m; i++) {
ll b, c;
char x, y;
cin >> b >> x >> c >> y;
clause(2 * (b - 1) + (x == 'N'), 2 * (c - 1) + (y == 'N'));
}
string res;
for (ll i = 0; i < n; i++) {
bool yes = reach(2 * i + 1, 2 * i), no = reach(2 * i, 2 * i + 1);
if (yes && no) {
cout << "IMPOSSIBLE\n";
return 0;
}
res += yes ? 'Y' : no ? 'N' : '?';
}
cout << res << '\n';
}
3 4
1 Y 2 Y
1 Y 2 N
2 N 3 Y
2 N 3 N
YN?
Độ phức tạp: , vì mỗi đề xuất duyệt đồ thị hai lần; với và cần khoảng phép tính.
Khi đi thi
- Gặp “mỗi thứ chọn một trong hai”, “ít nhất một trong hai điều kiện”, hay ràng buộc giữa từng cặp đối tượng thì nghĩ tới 2-SAT. Viết sẵn hàm thêm mệnh đề, với phủ định là
^ 1. - Ép một literal đúng bằng mệnh đề ; bằng nhau, khác nhau, AND, OR, XOR đều là một hoặc hai mệnh đề. Nên thử từng cách dịch trên ví dụ nhỏ trước khi chạy cả bài.
- Đọc cách gán đúng chiều với thuật toán đang dùng: với Kosaraju, đúng khi số thành phần của lớn hơn của ; với Tarjan thì ngược lại. Khi số biến tới , DFS đệ quy có thể tràn bộ nhớ ngăn xếp, nên cân nhắc viết DFS bằng ngăn xếp tự quản lý.
Tóm tắt
- Mệnh đề thành hai cung và trên đồ thị đỉnh.
- Không có nghiệm khi và chỉ khi có biến nằm cùng thành phần liên thông mạnh với .
- Cách gán: chọn literal ở thành phần đứng sau theo thứ tự tô pô.
- Một literal bắt buộc đúng khi có đường từ tới .
- Xung đột giữa các cặp phương án thành mệnh đề cấm; cần tối ưu một ngưỡng thì chặt nhị phân rồi kiểm bằng 2-SAT.
Bài tập
Bài tập ngoài
Dịch ràng buộc thành mệnh đề
IVThe Road to Berland is Paved With Good IntentionsCodeforcesIV2+ doorsCodeforcesIVUnusual MatrixCodeforcesChọn một trong hai phương án
VInnokenty and a Football LeagueCodeforces