Gnomi#7945

Gnomi (Editoriale)

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:

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");
  }
}