Associamo al problema un grafo orientato. I vertici del grafo sono gli gnomi, e ogni affermazione definisce un arco. Se lo gnomo s_1 è più basso dello gnomo s_2 (cioè s_1 < s_2), aggiungiamo un arco orientato s_1 \to s_2. L'affermazione s_1 > s_2 equivale a s_2 < s_1 e definisce l'arco s_2 \to s_1. Quindi, ogni arco è orientato da uno gnomo più basso verso uno più alto.
I nomi degli gnomi sono stringhe, quindi per prima cosa assegniamo a ciascun nome un indice intero univoco di vertice.
Quando le affermazioni sono contraddittorie. Supponiamo che il grafo contenga un ciclo v_1 \to v_2 \to \ldots \to v_k \to v_1. Allora v_1 è più basso di v_2, v_2 è più basso di v_3, , v_k è più basso di v_1. Per transitività, lo gnomo v_1 è più basso di se stesso, il che è impossibile. Quindi, almeno uno degli gnomi ha mentito.
Viceversa, se il grafo è aciclico, esso possiede un ordinamento topologico — un ordinamento dei vertici in cui ogni arco va da un vertice precedente a uno successivo. Assegniamo agli gnomi le altezze 1, 2, 3, \ldots secondo l'ordinamento topologico. Allora s_1 < s_2 vale per ogni arco s_1 \to s_2, cioè tutte le affermazioni sono vere simultaneamente.
Quindi, la risposta è "impossible" se e solo se il grafo costruito contiene un ciclo.
Individuazione del ciclo. Per trovare un ciclo in un grafo orientato, utilizziamo la ricerca in profondità (depth-first search) con tre colori per i vertici:
un vertice bianco (used[v] = 0) non è ancora stato visitato;
un vertice grigio (used[v] = 1) è in corso di elaborazione: la ricerca vi è entrata ma non ne è ancora uscita. I vertici grigi formano il percorso dalla radice della ricerca fino al vertice corrente;
un vertice nero (used[v] = 2) è completamente elaborato: tutti i vertici raggiungibili da esso sono già stati visitati.
Se durante la ricerca un arco v \to to dal vertice corrente porta a un vertice grigio , allora si trova sul percorso corrente dalla radice a . Insieme all'arco v \to to, questo percorso forma un ciclo. Un arco verso un vertice nero non crea un ciclo: da un vertice nero è impossibile tornare a , altrimenti sarebbe stato visitato prima.
Il grafo può essere disconnesso, quindi avviamo una ricerca in profondità a partire da ogni vertice non ancora visitato.
Esempio
I grafi corrispondenti agli esempi dell'enunciato del problema sono i seguenti (a sinistra — il grafo del primo esempio, a destra — del secondo):
Il primo grafo contiene il ciclo Dori Kili Balin Dori. Ciò significa che Dori è più basso di Kili, Kili è più basso di Balin, e Balin è più basso di Dori. Allora Dori è più basso di se stesso, quindi le affermazioni sono contraddittorie e la risposta è "impossible".
Il secondo grafo è aciclico. Il suo ordinamento topologico è Kili, Balin, Dori. Assegnando agli gnomi le altezze in questo ordine (Kili Balin Dori), otteniamo che tutte e tre le affermazioni valgono simultaneamente. La risposta è "possible".
Consideriamo come viene costruito il grafo per il primo test. Ogni nome riceve un indice secondo l'ordine della sua prima comparsa nell'input. Il primo nome è Dori, che riceve l'indice . Poi viene Balin — indice . Nella seconda affermazione, il nome Balin ha già un indice, quindi un nuovo indice viene assegnato solo a Kili — indice . Quindi, name\_to\_id[Dori] = 0, name\_to\_id[Balin] = 1, name\_to\_id[Kili] = 2, e il grafo contiene gli archi 1 \to 0, 2 \to 1 e 0 \to 2.
Simuliamo la ricerca in profondità. Chiamiamo : il vertice (Dori) diventa grigio. Lungo l'arco 0 \to 2 ci spostiamo al vertice bianco (Kili), che diventa grigio. Lungo l'arco 2 \to 1 ci spostiamo al vertice bianco (Balin), che diventa anch'esso grigio. L'arco 1 \to 0 esce dal vertice e porta al vertice grigio . Viene trovato un ciclo, quindi impostiamo flag = 1.
Implementazione dell'algoritmo
Memorizziamo il grafo orientato come lista di adiacenza . L'array memorizza i colori dei vertici durante la ricerca in profondità: — bianco, — grigio, — nero. La mappa name\_to\_id associa il nome di uno gnomo a un indice di vertice: lo gnomo di nome corrisponde al vertice name\_to\_id[s].
vector<vector<int> > g;
vector<int> used;
unordered_map<string, int> name_to_id;La funzione get_id restituisce l'indice del vertice corrispondente allo gnomo di nome . Gli indici vengono assegnati nell'ordine in cui i nomi compaiono per la prima volta: da a id - 1, dove è il numero di gnomi distinti incontrati finora.
int get_id(string name)
{Se lo gnomo di nome è già stato incontrato, restituisce l'indice che gli è stato assegnato in precedenza.
if (name_to_id.count(name)) return name_to_id[name];Altrimenti, assegna il successivo indice allo gnomo di nome e incrementa di .
return name_to_id[name] = id++;
}La funzione dfs esegue una ricerca in profondità a partire dal vertice . All'ingresso nel vertice , esso diventa grigio.
void dfs(int v)
{
used[v] = 1;
for (int to : g[v])
{Consideriamo l'arco (v, to). Se porta a un vertice grigio, è stato trovato un ciclo. Impostiamo flag = 1 e interrompiamo la ricerca.
if (used[to] == 1)
{
flag = 1;
return;
}Se il vertice non è ancora stato visitato (è bianco), proseguiamo la ricerca in profondità a partire da esso.
if (used[to] == 0) dfs(to);
}Dopo che tutti i vertici raggiungibili da sono stati elaborati, il vertice diventa nero.
used[v] = 2;
}La parte principale del programma. Leggiamo il numero di affermazioni . Il numero di gnomi distinti non supera 10^4, quindi il grafo ha al massimo questo numero di vertici. Allochiamo memoria per la lista di adiacenza e l'array dei colori; inizialmente tutti i vertici sono bianchi.
cin >> n;
g.resize(10001); used.resize(10001,0);La numerazione degli gnomi inizia da .
id = 0;Elaboriamo le affermazioni. Ognuna di esse definisce un arco orientato del grafo.
for (i = 0; i < n; i++)
{Leggiamo un'affermazione della forma s_1 < s_2 o s_1 > s_2: il nome , il segno e il nome .
cin >> l >> op >> r;Troviamo gli indici e dei vertici corrispondenti agli gnomi e .
u = get_id(l);
v = get_id(r);Un arco è orientato da uno gnomo più basso verso uno più alto. Se l < r, aggiungiamo l'arco u \to v, altrimenti aggiungiamo l'arco v \to u.
if (op == "<")
g[u].push_back(v);
else
g[v].push_back(u);
}La variabile diventa se viene trovato un ciclo nel grafo.
flag = 0;Il grafo può essere disconnesso, quindi avviamo una ricerca in profondità a partire da ogni vertice non ancora visitato. I vertici sono numerati da a id - 1. Non appena viene trovato un ciclo, interrompiamo la ricerca.
for (i = 0; i < id; i++)
{
if (!used[i]) dfs(i);
if (flag == 1) break;
}Se viene trovato un ciclo, le affermazioni sono contraddittorie — stampiamo "impossible", altrimenti stampiamo "possible".
if (flag == 1) printf("impossible\n");
else printf("possible\n");Implementazione in 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");
}
}