#include <bits/stdc++.h>
using namespace std;
#define __Yang_Yang__() int main()
int test = 1;
using ll = long long;
using ull = unsigned long long;
using ldb = long double;
using db = double;
// Các hàm hỗ trợ (không dùng class)
const int MAXN = 1005;
const int MAXV = 2005;
// Mảng toàn cục lưu trữ Đồ thị Suy luận (Implication Graph)
// Chiều đầu tiên [2] dùng để phân biệt: 0 là đồ thị f1, 1 là đồ thị f2
bitset<MAXN> good_bs[2][MAXV], bad_bs[2][MAXV];
bool bv[2][MAXV];
int val[2][MAXN]; // Lưu giá trị gán cho các biến: -1 (chưa gán), 0 (False), 1 (True)
int sof[MAXN]; // Mảng lưu tạm các đỉnh đã thay đổi trạng thái (để rollback nếu sai)
// Hàm chuyển đổi đỉnh:
// Đỉnh dương 1..n -> index 0..(n-1)
// Đỉnh âm -1..-n -> index n..(2n-1)
int get_num(int n_total, int a) {
if (a > 0) return a - 1;
return n_total + (-a) - 1;
}
// Hàm lấy đỉnh phủ định (negation) của một đỉnh
int get_neg(int n_total, int a) {
if (a < n_total) return a + n_total;
return a - n_total;
}
// Thêm cung suy luận a -> b vào đồ thị g_idx
void add_implication(int g_idx, int n_total, int a, int b) {
a = get_num(n_total, a);
b = get_num(n_total, b);
if (b < n_total) {
good_bs[g_idx][a].set(b);
} else {
bad_bs[g_idx][a].set(b - n_total);
}
}
// Thêm một mệnh đề (a OR b) vào đồ thị g_idx
// Tương đương với 2 cung suy luận: (!a -> b) và (!b -> a)
void add_clause(int g_idx, int n_total, int a, int b) {
add_implication(g_idx, n_total, -a, b);
add_implication(g_idx, n_total, -b, a);
}
// Tính bao đóng chuyển tiếp (Transitive Closure) để tìm tất cả các liên kết gián tiếp
void build_transitive_closure(int g_idx, int n_total) {
// Khởi tạo: Mỗi đỉnh tự suy ra chính nó
for (int i = 0; i < 2 * n_total; ++i) {
if (i < n_total) good_bs[g_idx][i].set(i);
else bad_bs[g_idx][i].set(i - n_total);
}
// Thuật toán tương tự Floyd-Warshall nhưng tối ưu cực nhanh bằng std::bitset O(N^3 / 64)
for (int mid = 0; mid < 2 * n_total; ++mid) {
for (int i = 0; i < 2 * n_total; ++i) {
bool ok = false;
if (mid < n_total) {
if (good_bs[g_idx][i].test(mid)) ok = true;
} else {
if (bad_bs[g_idx][i].test(mid - n_total)) ok = true;
}
if (ok) {
good_bs[g_idx][i] |= good_bs[g_idx][mid];
bad_bs[g_idx][i] |= bad_bs[g_idx][mid];
}
}
}
// Đánh dấu các đỉnh gây ra mâu thuẫn (Ví dụ: A -> !A nghĩa là A buộc phải sai)
for (int i = 0; i < 2 * n_total; ++i) {
if (i < n_total) {
if (bad_bs[g_idx][i].test(i)) bv[g_idx][i] = true;
} else {
if (good_bs[g_idx][i].test(i - n_total)) bv[g_idx][i] = true;
}
}
// Lan truyền trạng thái mâu thuẫn
for (int i = 0; i < 2 * n_total; ++i) {
for (int j = 0; j < 2 * n_total; ++j) {
if (!bv[g_idx][j]) continue;
if (j < n_total) {
if (good_bs[g_idx][i].test(j)) bv[g_idx][i] = true;
} else {
if (bad_bs[g_idx][i].test(j - n_total)) bv[g_idx][i] = true;
}
}
}
}
// Kiểm tra xem đồ thị có tồn tại nghiệm hợp lệ hay không
bool has_solution(int g_idx, int n_total) {
for (int i = 0; i < n_total; ++i) {
// Nếu A buộc phải là False VÀ !A buộc phải là False -> Vô nghiệm
if (bad_bs[g_idx][i].test(i) && good_bs[g_idx][i + n_total].test(i)) {
return false;
}
}
return true;
}
// Thử gán giá trị cho một đỉnh, trả về true nếu gán thành công không gây mâu thuẫn
bool select_var(int g_idx, int n_total, int v) {
int t = v;
if (val[g_idx][v] == 0) t += n_total;
if (bv[g_idx][t]) return false;
int k = 0;
sof[k++] = v;
bool ok = true;
for (int i = 0; i < n_total; ++i) {
if (good_bs[g_idx][t].test(i)) {
if (val[g_idx][i] == 0) { ok = false; break; }
if (val[g_idx][i] == -1) {
val[g_idx][i] = 1;
sof[k++] = i;
}
}
if (bad_bs[g_idx][t].test(i)) {
if (val[g_idx][i] == 1) { ok = false; break; }
if (val[g_idx][i] == -1) {
val[g_idx][i] = 0;
sof[k++] = i;
}
}
}
// Nếu mâu thuẫn, rollback (trả lại trạng thái chưa gán) cho toàn bộ biến vừa thử
if (!ok) {
for (int i = 0; i < k; ++i) val[g_idx][sof[i]] = -1;
return false;
}
return true;
}
// Hàm in nghiệm và thoát chương trình
void print_solution_and_die(int g_idx, int n_total, int f1, int f2) {
memset(val[g_idx], -1, sizeof(val[g_idx]));
// Ép gán các giá trị vi phạm để tạo ra nghiệm khác biệt
if (f1 != -1) {
if (f1 < n_total) val[g_idx][f1] = 1;
else {
f1 -= n_total;
val[g_idx][f1] = 0;
}
select_var(g_idx, n_total, f1);
}
if (f2 != -1) {
int base_f2 = (f2 < n_total) ? f2 : f2 - n_total;
if (val[g_idx][base_f2] == -1) {
if (f2 < n_total) val[g_idx][f2] = 1;
else {
f2 -= n_total;
val[g_idx][f2] = 0;
}
select_var(g_idx, n_total, f2);
}
}
// Gán các biến còn lại một cách tham lam
for (int i = 0; i < n_total; ++i) {
if (val[g_idx][i] != -1) continue;
val[g_idx][i] = 0;
if (!select_var(g_idx, n_total, i)) {
val[g_idx][i] = 1;
select_var(g_idx, n_total, i);
}
}
// In kết quả
for (int i = 0; i < n_total; ++i) {
if (i > 0) cout << " ";
cout << val[g_idx][i];
}
cout << "\n";
exit(0);
}
// Kiểm tra xem việc gán v1 và v2 có sinh ra mâu thuẫn ngay lập tức không
bool check_bad(int g_idx, int v1, int v2) {
auto t = (good_bs[g_idx][v1] | good_bs[g_idx][v2]) & (bad_bs[g_idx][v1] | bad_bs[g_idx][v2]);
return t.any();
}
bool used_pair[MAXV][MAXV];
// Hàm đối chiếu: Dò xem các mệnh đề của đồ thị `from` có bị đồ thị `to` vi phạm không
void check_conflict(int g_idx_from, int g_idx_to, int n_total, const vector<pair<int, int>>& clauses) {
memset(used_pair, 0, sizeof(used_pair));
for (const auto& cl : clauses) {
int v1 = get_num(n_total, cl.first);
int v2 = get_num(n_total, cl.second);
// Phủ định lại mệnh đề để ép tạo lỗi (A OR B sai nghĩa là !A và !B phải đúng)
v1 = get_neg(n_total, v1);
v2 = get_neg(n_total, v2);
if (v1 > v2) swap(v1, v2);
if (used_pair[v1][v2]) continue;
used_pair[v1][v2] = true;
if (bv[g_idx_to][v1] || bv[g_idx_to][v2]) continue;
if (check_bad(g_idx_to, v1, v2)) continue;
// Nếu đồ thị to có thể chấp nhận cấu hình phủ định này -> 2 đồ thị khác biệt
print_solution_and_die(g_idx_to, n_total, v1, v2);
}
}
void solve() {
int n, m1, m2;
if (!(cin >> n >> m1 >> m2)) return;
// Clear dữ liệu toàn cục nếu có nhiều test (mặc dù bài này test=1)
for (int i = 0; i < 2; ++i) {
for (int j = 0; j < 2 * n; ++j) {
good_bs[i][j].reset();
bad_bs[i][j].reset();
bv[i][j] = false;
}
}
vector<pair<int, int>> cl1(m1), cl2(m2);
for (int i = 0; i < m1; ++i) {
cin >> cl1[i].first >> cl1[i].second;
add_clause(0, n, cl1[i].first, cl1[i].second);
}
for (int i = 0; i < m2; ++i) {
cin >> cl2[i].first >> cl2[i].second;
add_clause(1, n, cl2[i].first, cl2[i].second);
}
// Khởi tạo Transitive Closure cho cả 2 đồ thị
build_transitive_closure(0, n);
build_transitive_closure(1, n);
bool r1 = has_solution(0, n);
bool r2 = has_solution(1, n);
// Nếu cả 2 đều vô nghiệm -> Cùng tập nghiệm (Tập rỗng)
if (!r1 && !r2) {
cout << "SIMILAR\n";
return;
}
// Nếu 1 cái có nghiệm, 1 cái vô nghiệm -> In ngay nghiệm của cái hợp lệ
if (!r1) print_solution_and_die(1, n, -1, -1);
if (!r2) print_solution_and_die(0, n, -1, -1);
// Đối chiếu xem 2 đồ thị có chứa nghiệm vi phạm lẫn nhau không
check_conflict(0, 1, n, cl1);
check_conflict(1, 0, n, cl2);
// Nếu mọi đối chiếu đều không tìm được lỗ hổng -> Hoàn toàn tương đồng
cout << "SIMILAR\n";
}
__Yang_Yang__()
{
ios_base::sync_with_stdio(0);
cin.tie(nullptr); cout.tie(nullptr);
// Nếu bài toán không có test case, hãy cmt dòng cin>>test; lại
// cin >> test;
while(test--) solve();
return 0;
}
/*
TRẢ LỜI CÁC CÂU HỎI:
Cấu trúc dữ liệu nào dùng trong code, và nhiệm vụ của chúng?
=> std::bitset: Được sử dụng để lưu trữ các đồ thị kề/cung suy luận (good_bs, bad_bs). Bitset giúp cực kỳ tối ưu hóa các phép toán logic trên bit, tăng tốc độ xử lý mảng bool gấp 64 lần so với duyệt thông thường.
=> Mảng đa chiều toàn cục: Giúp lưu riêng biệt trạng thái của 2 công thức `f1` (index 0) và `f2` (index 1) mà không cần tạo class hay struct có phương thức, đáp ứng đúng giới hạn của template.
Thuật toán nào dùng trong code, và nhiệm vụ của chúng?
=> Transitive Closure (Bao đóng chuyển tiếp) qua thuật toán Floyd-Warshall: Dùng để phát hiện tất cả các hậu quả suy luận lô-gic gián tiếp. Ví dụ A suy ra B, B suy ra C thì thuật toán đảm bảo tìm được cung nối trực tiếp A -> C.
=> Backtracking Tham lam (Gán biến): Để giải quyết và dò nghiệm cho bài toán 2-SAT (hàm `select_var`).
Công thức toán học nào hoặc công thức truy hồi nào dùng để làm gì?
=> Logic Mệnh đề: Công thức phân rã của 2-SAT là `(A OR B) <=> (!A -> B) AND (!B -> A)`. Dựa vào đây để vẽ đồ thị kề có hướng suy luận.
Độ phức tạp thuật toán là bao nhiêu?
=> Thời gian: Nặng nhất nằm ở bước chạy Transitive Closure với độ phức tạp $O(N^3 / 64)$. Với N tối đa = 1000, số vòng lặp tính ra chỉ tốn chưa tới $1.25 \times 10^8$ phép tính bit, dễ dàng pass giới hạn thời gian 3.0 giây.
=> Không gian bộ nhớ: Các mảng bitset tiêu tốn $O(N^2)$ bit bộ nhớ. Tính ra chưa tốn tới 5MB, hoàn toàn thỏa mãn 256MB.
Dùng đến kỹ thuật thiết kế bài toán nào?
=> Đồ thị kề có hướng (Directed Implication Graph): Biểu diễn quan hệ đúng/sai của 2-SAT qua đồ thị.
=> Tối ưu hóa Bitmask: Nén ma trận kề boolean về bitset để đạt Time Complexity tối ưu thực tế.
Độ khó của bài toán khoảng bao nhiều trên thang 10?
=> 8.5/10. Kiến thức 2-SAT không phải là quá hiếm ở CP, nhưng ở bài này yêu cầu so sánh sự bất đồng của tận 2 đồ thị nên việc tạo ra trạng thái đối chiếu (phủ định các clauses của `f1` để ép vào `f2`) khá phức tạp và hack não.
*/