Пов'яжемо із задачею орієнтований граф. Вершинами графа є гноми, а кожне твердження визначає ребро. Якщо гном s_1 нижчий за гнома s_2 (тобто s_1 < s_2), додаємо орієнтоване ребро s_1 \to s_2. Твердження s_1 > s_2 еквівалентне s_2 < s_1 і визначає ребро s_2 \to s_1. Таким чином, кожне ребро спрямоване від нижчого гнома до вищого.
Імена гномів — це рядки, тому спочатку присвоюємо кожному імені унікальний цілочисельний індекс вершини.
Коли твердження суперечливі. Припустимо, граф містить цикл v_1 \to v_2 \to \ldots \to v_k \to v_1. Тоді v_1 нижчий за v_2, v_2 нижчий за v_3, , v_k нижчий за v_1. За транзитивністю гном v_1 нижчий за самого себе, що неможливо. Отже, хоча б один із гномів збрехав.
І навпаки, якщо граф ациклічний, то він має топологічне сортування — впорядкування вершин, в якому кожне ребро йде від попередньої вершини до наступної. Присвоїмо гномам зрости 1, 2, 3, \ldots у порядку топологічного сортування. Тоді s_1 < s_2 виконується для кожного ребра s_1 \to s_2, тобто всі твердження істинні одночасно.
Отже, відповідь "impossible" тоді і тільки тоді, коли побудований граф містить цикл.
Пошук циклу. Щоб знайти цикл в орієнтованому графі, використовуємо пошук у глибину із трьома кольорами вершин:
біла вершина (used[v] = 0) ще не відвідана;
сіра вершина (used[v] = 1) обробляється: пошук зайшов до неї, але ще не вийшов. Сірі вершини утворюють шлях від кореня пошуку до поточної вершини;
чорна вершина (used[v] = 2) повністю оброблена: всі вершини, досяжні з неї, вже відвідані.
Якщо під час пошуку ребро v \to to з поточної вершини веде до сірої вершини , то лежить на поточному шляху від кореня до . Разом із ребром v \to to цей шлях утворює цикл. Ребро до чорної вершини не створює цикл: із чорної вершини неможливо повернутися до , інакше було б відвідано раніше.
Граф може бути незв'язним, тому запускаємо пошук у глибину з кожної вершини, яка ще не була відвідана.
Приклад
Графи, що відповідають прикладам з умови задачі, виглядають так (зліва — граф першого прикладу, справа — другого):
Перший граф містить цикл Dori Kili Balin Dori. Це означає, що Dori нижчий за Kili, Kili нижчий за Balin, а Balin нижчий за Dori. Тоді Dori нижчий за самого себе, тому твердження суперечливі і відповідь "impossible".
Другий граф ациклічний. Його топологічне сортування — Kili, Balin, Dori. Присвоївши гномам зрости в цьому порядку (Kili Balin Dori), отримуємо, що всі три твердження виконуються одночасно. Відповідь "possible".
Розглянемо, як будується граф для першого тесту. Кожне ім'я отримує індекс у порядку своєї першої появи у вхідних даних. Перше ім'я — Dori, воно отримує індекс . Далі йде Balin — індекс . У другому твердженні ім'я Balin вже має індекс, тому новий індекс присвоюється лише Kili — індекс . Таким чином, name\_to\_id[Dori] = 0, name\_to\_id[Balin] = 1, name\_to\_id[Kili] = 2, і граф містить ребра 1 \to 0, 2 \to 1 та 0 \to 2.
Просимулюємо пошук у глибину. Викликаємо : вершина (Dori) стає сірою. По ребру 0 \to 2 переходимо до білої вершини (Kili), яка стає сірою. По ребру 2 \to 1 переходимо до білої вершини (Balin), яка також стає сірою. Ребро 1 \to 0 виходить із вершини і веде до сірої вершини . Знайдено цикл, тому встановлюємо flag = 1.
Реалізація алгоритму
Зберігаємо орієнтований граф у вигляді списку суміжності . Масив зберігає кольори вершин під час пошуку в глибину: — білий, — сірий, — чорний. Словник name\_to\_id зіставляє ім'я гнома з індексом вершини: гному з іменем відповідає вершина name\_to\_id[s].
vector<vector<int> > g;
vector<int> used;
unordered_map<string, int> name_to_id;Функція get_id повертає індекс вершини, що відповідає гному з іменем . Індекси присвоюються в порядку першої появи імен: від до id - 1, де — кількість різних гномів, зустрінутих на даний момент.
int get_id(string name)
{Якщо гном з іменем вже зустрічався, повертаємо присвоєний йому раніше індекс.
if (name_to_id.count(name)) return name_to_id[name];Інакше присвоюємо гному з іменем наступний індекс і збільшуємо на .
return name_to_id[name] = id++;
}Функція dfs виконує пошук у глибину, починаючи з вершини . При вході у вершину вона стає сірою.
void dfs(int v)
{
used[v] = 1;
for (int to : g[v])
{Розглянемо ребро (v, to). Якщо воно веде до сірої вершини, знайдено цикл. Встановлюємо flag = 1 і припиняємо пошук.
if (used[to] == 1)
{
flag = 1;
return;
}Якщо вершина ще не відвідана (вона біла), продовжуємо пошук у глибину з неї.
if (used[to] == 0) dfs(to);
}Після того, як усі вершини, досяжні з , оброблені, вершина стає чорною.
used[v] = 2;
}Основна частина програми. Зчитуємо кількість тверджень . Кількість різних гномів не перевищує 10^4, тому граф має не більше стільки вершин. Виділяємо пам'ять для списку суміжності та масиву кольорів; спочатку всі вершини білі.
cin >> n;
g.resize(10001); used.resize(10001,0);Нумерація гномів починається з .
id = 0;Обробляємо тверджень. Кожне з них визначає одне орієнтоване ребро графа.
for (i = 0; i < n; i++)
{Зчитуємо твердження вигляду s_1 < s_2 або s_1 > s_2: ім'я , знак та ім'я .
cin >> l >> op >> r;Знаходимо індекси та вершин, що відповідають гномам та .
u = get_id(l);
v = get_id(r);Ребро спрямоване від нижчого гнома до вищого. Якщо l < r, додаємо ребро u \to v, інакше додаємо ребро v \to u.
if (op == "<")
g[u].push_back(v);
else
g[v].push_back(u);
}Змінна стає рівною , якщо в графі знайдено цикл.
flag = 0;Граф може бути незв'язним, тому запускаємо пошук у глибину з кожної вершини, яка ще не була відвідана. Вершини нумеруються від до id - 1. Як тільки знайдено цикл, припиняємо пошук.
for (i = 0; i < id; i++)
{
if (!used[i]) dfs(i);
if (flag == 1) break;
}Якщо знайдено цикл, твердження суперечливі — виводимо "impossible", інакше виводимо "possible".
if (flag == 1) printf("impossible\n");
else printf("possible\n");Реалізація на Java
import java.util.*;
public class Main {
static List<List<Integer>> g = new ArrayList<>();
static int[] used;
static Map<String, Integer> nameToId = new HashMap<>();
static int id = 0;
static boolean hasCycle = false;
static int getId(String name) {
if (!nameToId.containsKey(name))
nameToId.put(name, id++);
return nameToId.get(name);
}
static void dfs(int v) {
used[v] = 1;
for (int to : g.get(v)) {
if (used[to] == 1) {
hasCycle = true;
return;
}
if (used[to] == 0) dfs(to);
}
used[v] = 2;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
used = new int[10001];
for (int i = 0; i < 10001; i++)
g.add(new ArrayList<>());
for (int i = 0; i < n; i++) {
String l = sc.next();
String op = sc.next();
String r = sc.next();
int u = getId(l);
int v = getId(r);
if (op.equals("<"))
g.get(u).add(v);
else
g.get(v).add(u);
}
for (int i = 0; i < id; i++) {
if (used[i] == 0) {
dfs(i);
if (hasCycle) break;
}
}
System.out.println(hasCycle ? "impossible" : "possible");
}
}