#include <iostream>
#include <fstream>
#include <bits/stdc++.h>
using namespace std;
class graf {
int noduri, muchii;
vector < vector < int >> adiacenta;
public:
// Constructor cu matrice de adiacenta data
graf(vector < vector < int >> listaAdiacenta, int noduri, int muchii): adiacenta(listaAdiacenta), noduri(noduri), muchii(muchii) {};
// Constructor fara matrice de adiacenta, se cu initalizeaza una goala
graf(int noduri, int muchii): adiacenta(noduri + 1), noduri(noduri), muchii(muchii) {};
void citireNeorientat(istream & in ) {
for (int i = 0; i < muchii; i++) {
int aux1, aux2; in >> aux1 >> aux2;
adiacenta[aux1].push_back(aux2);
adiacenta[aux2].push_back(aux1);
}
}
void citireOrientat(istream & in ) {
for (int i = 0; i < muchii; i++) {
int aux1, aux2; in >> aux1 >> aux2;
adiacenta[aux1].push_back(aux2);
}
}
vector < int > bfs(int start) {
vector < int > rasp(noduri + 1, -1);
vector < int > vis(noduri + 1);
queue < int > q;
vis[start] = 1;
q.push(start);
rasp[start] = 0;
// Se baga pe rand in queue toate nodurile nevizitate ale nodului din capul queue-ului, si se noteaza timpul la care se ajunge la ele
while (!q.empty()) {
int curr = q.front();
q.pop();
for (int i: adiacenta[curr])
if (vis[i] == 0) {
vis[i] = 1;
q.push(i);
rasp[i] = rasp[curr] + 1;
};
}
return rasp;
}
int dfs() {
vector < int > vis(noduri + 1);
int res = 0;
stack < int > s;
// Se baga pe rand in stack toate nodurile nevizitate ale nodului din capul queue-ului, si se noteaza timpul la care se ajunge la ele. Se simuleaza call stack-ul de la recursivitate de dfs
for (int i = 1; i <= noduri; i++) {
if (vis[i] == 0) {
res++;
vis[i] = 1;
s.push(i);
while (!s.empty()) {
int curr = s.top();
s.pop();
for (int i = adiacenta[curr].size() - 1; i >= 0; i--) {
int nod = adiacenta[curr][i];
if (vis[nod] == 0) {
vis[nod] = 1;
s.push(nod);
}
}
}
}
}
return res;
}
void dfsbiconex(int tata, vector < int > & timp, vector < int > & rev, stack < pair < int, int >> & s, int & timpcurent, vector < string > & rasp) {
// Tarjan modificat
timpcurent++;
timp[tata] = timpcurent;
rev[tata] = timpcurent;
int fiuRadacina = 0;
// Se tin minte timpii de intoarcere respectiv timpul de intalnire al fiecarui nod in arborele de dfs call
for (int fiu: adiacenta[tata]) {
if (timp[fiu] == -1) {
// Se retine cati fii are fiecare nod in arborele de call dfs
fiuRadacina++;
// Se parcurge prin dfs fiecare nod, si cand se gaseste un nod nevizitat se adauga muchia intr-un stack
s.push(make_pair(tata, fiu));
dfsbiconex(fiu, timp, rev, s, timpcurent, rasp);
// Timpul de revenire al unei muchii este minimul dintre timpul de revenire curent si timpul de revenire al fiului,
// pt ca daca fiul are timp de revenire mai mic si tata il va putea accesa
rev[tata] = min(rev[tata], rev[fiu]);
if ((timp[tata] != 1 && rev[fiu] >= timp[tata]) || (timp[tata] == 1 && fiuRadacina > 1)) {
// Se verifica daca un nod e critic, si sunt 2 cazuri. Primul e cand nodul nu e chiar nodul de incepere al dfs, iar timpul de revenire al fiului este mai mare ca tatal.
// Practic cand nu suntem intr-un ciclu. Al doilea caz este cel discutat la laborator, cand nodul este chiar radacina arborelui, daca are 2 fii atunci e clar nod critic
pair < int, int > stop = make_pair(tata, fiu);
unordered_set < int > aux;
string str = "";
// Se face afisare cu string
aux.insert(stop.first);
aux.insert(stop.second);
str += to_string(stop.first);
str += " ";
str += to_string(stop.second);
str += " ";
// Se parcurge stackul de muchii pana cand dam de muchia care leaga fiul de tata.
// Se face asta pt ca mai intai se va adauga o muchie intre fiu si tata in stack, apoi se va face dfs
// si se vor detecta componentele biconexe ale descendentilor si se vor adauga in stack muchiile descendentilor, iar cad se va iesi din dfs-ul descendentilor unui nod tata
// ce vor ramane in stack vor fi chiar muchiile corespunzatoare componentei biconexe, pana la muchia care leaga tatal de fiu
while (s.top() != stop) {
int nodcur = s.top().first;
if (aux.find(nodcur) == aux.end()) {
str += to_string(nodcur);
str += " ";
aux.insert(s.top().first);
}
s.pop();
}
str += '\n';
rasp.push_back(str);
s.pop();
}
}
else {
// Daca nodul e deja vizitat, atunci am detectat o muchie de intoarcere,
// si timpul de revenire al tatalui devine minimul dintre timpul actual de revenire (ca poate fi mai mic, cu o alta muchie gasita deja)
// si timpul de gasire al fiului
rev[tata] = min(rev[tata], timp[fiu]);
// Verificam sa nu fie o muchie care sa duca in fata (in arbore) de fapt, pt ca deja a fost analizata mai devreme in dfs
if (timp[fiu] < timp[tata]) {
s.push(make_pair(tata, fiu));
}
}
}
}
vector < string > biconex() {
vector < int > timp(noduri + 1, -1), rev(noduri + 1);
stack < pair < int, int >> s;
vector < string > rasp;
int timpo = 0;
// Se porneste dfs-ul din toate componentele conexe, si apoi se adauga la raspuns si ce a mai ramas in stack, pt ca si ce ramane va forma o componenta biconexa
for (int i = 0; i < noduri; i++) {
if (timp[i] == -1) {
dfsbiconex(i, timp, rev, s, timpo, rasp);
}
if (!s.empty()) {
unordered_set < int > aux;
string str;
while (!s.empty()) {
if (aux.find(s.top().first) == aux.end()) {
aux.insert(s.top().first);
str += to_string(s.top().first);
str += " ";
}
if (aux.find(s.top().second) == aux.end()) {
aux.insert(s.top().second);
str += to_string(s.top().second);
str += " ";
}
s.pop();
}
str += '\n';
rasp.push_back(str);
}
}
return rasp;
}
void dfsCtc(int tata, vector < int > & timp, vector < int > & rev, stack < int > & s, int & timpcurent, vector < string > & rasp, unordered_set < int > & check) {
// Tarjan
timpcurent++;
timp[tata] = timpcurent;
rev[tata] = timpcurent;
s.push(tata);
check.insert(tata);
// Se tin minte timpii de intoarcere respectiv timpul de intalnire al fiecarui nod in arborele de dfs call
for (int fiu: adiacenta[tata]) {
if (timp[fiu] == -1) {
// Cand descoperim un fiu nevizitat facem dfs
dfsCtc(fiu, timp, rev, s, timpcurent, rasp, check);
// Timpul de revenire al unei muchii este minimul dintre timpul de revenire curent si timpul de revenire al fiului,
// pt ca daca fiul are timp de revenire mai mic si tata il va putea accesa
rev[tata] = min(rev[tata], rev[fiu]);
}
// Daca nodul e deja vizitat, atunci am detectat o muchie de intoarcere,
// si timpul de revenire al tatalui devine minimul dintre timpul actual de revenire (ca poate fi mai mic, cu o alta muchie gasita deja)
// si timpul de gasire al fiului
else if (check.find(fiu) != check.end()) {
rev[tata] = min(rev[tata], timp[fiu]);
}
}
// Dupa ce se parcurg toti descendentii tatalui, verificam daca timpul de revenire al tatalui este egal cu timpul de intalnire al sau.
// Daca sunt egale, exista doua cazuri, ori tatal face parte dintr-o componenta tare conexa cu descendenti de-ai sai bagati deja in stack,
// ori el este o componenta tare conexa singur.
// Se vor salva nodurile rezultate in stringuri. Check tine minte daca un element e in stack sau nu, caci daca nu e poate forma o componenta tare conexa cu altcineva
if (timp[tata] == rev[tata]) {
string aux = "";
while (s.top() != tata) {
aux += to_string(s.top());
aux += " ";
check.erase(s.top());
s.pop();
}
aux += to_string(s.top());
aux += " ";
check.erase(s.top());
s.pop();
aux += '\n';
rasp.push_back(aux);
}
}
vector < string > ctc() {
vector < int > timp(noduri + 1, -1), rev(noduri + 1);
unordered_set < int > check;
stack < int > s;
vector < string > rasp;
int timpo = 0;
// Se porneste dfs-ul din toate componentele conexe, si apoi se adauga la raspuns si ce a mai ramas in stack, pt ca si ce ramane va forma o componenta biconexa
for (int i = 1; i <= noduri; i++) {
if (timp[i] == -1) {
dfsCtc(i, timp, rev, s, timpo, rasp, check);
}
}
return rasp;
}
void dfsSortaret(int tata, vector < int > & vis, vector < int > & res) {
// Se face dfs cu recursivitate
for (int fiu: adiacenta[tata]) {
if (!vis[fiu]) {
vis[fiu] = 1;
dfsSortaret(fiu, vis, res);
}
}
// Cand se termina de cautat in fii unui nod, se adauga la raspuns, pt ca inseamna ca nu mai depinde nimeni de el
res.push_back(tata);
}
vector < int > sortaret() {
vector < int > vis(this -> noduri + 1);
vector < int > res;
// Se face dfs pt fiecare componenta conexa
for (int i = 1; i <= this -> noduri; i++) {
if (vis[i] == 0) {
vis[i] = 1;
dfsSortaret(i, vis, res);
}
}
return res;
}
void dfsCriticalConnections(int tata, vector < int > & timp, vector < int > & rev, int & timpcurent, vector < vector < int >> & rasp, vector < int > & parinte) {
// Tarjan modificat
timpcurent++;
timp[tata] = timpcurent;
rev[tata] = timpcurent;
// Se tin minte timpii de intoarcere respectiv timpul de intalnire al fiecarui nod in arborele de dfs call
for (int fiu: adiacenta[tata]) {
// Tinem minte parintele direct din arborele dfs
parinte[fiu] = tata;
if (timp[fiu] == -1) {
// Cand descoperim un fiu nevizitat facem dfs
dfsCriticalConnections(fiu, timp, rev, timpcurent, rasp, parinte);
// Timpul de revenire al unei muchii este minimul dintre timpul de revenire curent si timpul de revenire al fiului,
// pt ca daca fiul are timp de revenire mai mic si tata il va putea accesa
rev[tata] = min(rev[tata], rev[fiu]);
if (rev[fiu] > timp[tata]) {
vector < int > aux;
aux.push_back(tata);
aux.push_back(fiu);
rasp.push_back(aux);
}
}
// Daca nodul e deja vizitat, atunci am detectat o muchie de intoarcere,
// si timpul de revenire al tatalui devine minimul dintre timpul actual de revenire (ca poate fi mai mic, cu o alta muchie gasita deja)
// si timpul de gasire al fiului
// Se verifica ca muchia sa nu fie una prezenta deja, sa nu fie una care sa mearga in fata in arborele dfs
else if (parinte[tata] != fiu) {
rev[tata] = min(rev[tata], timp[fiu]);
}
}
}
vector < vector < int >> criticalConnections() {
vector < int > timp(noduri + 1, -1), rev(noduri + 1), parinte(noduri + 1);
vector < vector < int >> rasp;
int timpo = 0;
// Se face dfs pt fiecare componenta conexa
for (int i = 1; i <= noduri; i++) {
if (timp[i] == -1) {
dfsCriticalConnections(i, timp, rev, timpo, rasp, parinte);
}
}
return rasp;
}
};
bool havelHakimi(vector < int > & a) {
// Mai intai se sorteaza vectorul ca sa avem mereu nodul cu cel mai mare grad primul
sort(a.begin(), a.end(), [](int m, int n) {
return m > n;
});
int x = a[0];
// Daca mai e un singur nod in vector cu grad nenul, evident, nu se va putea face graf cu el
if (a.size() == 1 && x >= 1) {
return false;
}
// Daca primul nod e 0, tinand cont ca vectorul e sortat crescator, atunci toate sunt 0 si e bine, se poate forma graf
if (x == 0) {
return true;
}
// Se sterge primul element din vector, ii facem legaturile
a.erase(a.begin() + 0);
// Daca nodul are grad mai mare decat sunt legaturi posibile atunci e clar ca nu e posibil sa facem graf
if (x > a.size()) {
return false;
}
// 'Conectam' primul nod la urmatoarele noduri din vector, cate ne spune gradul lui
for (int i = 0; i < x; i++) {
a[i]--;
// Daca un nod ajunge cu gradul pe minus, e clar ca nu se paote face graf
if (a[i] < 0) {
return false;
}
}
// Apelam recursiv cu noul vector format
return havelHakimi(a);
}
void bfsDriver() {
// Citire
ifstream in ("bfs.in");
ofstream out("bfs.out");
int n, m, s; in >> n >> m >> s;
graf x(n, m);
x.citireOrientat( in );
// Afisare
vector < int > rasp = x.bfs(s);
for (int i = 1; i <= n; i++) {
out << rasp[i] << " ";
}
in.close();
out.close();
}
void dfsDriver() {
// Citire
ifstream in ("dfs.in");
ofstream out("dfs.out");
int n, m; in >> n >> m;
graf x(n, m);
x.citireNeorientat( in );
// Afisare
out << x.dfs();
in.close();
out.close();
}
void biconexDriver() {
// Citire
ifstream in ("biconex.in");
ofstream out("biconex.out");
int n, m; in >> n >> m;
graf x(n, m);
x.citireNeorientat( in );
// Afisare
vector < string > fin = x.biconex();
out << fin.size() << '\n';
for (string i: fin) {
out << i;
}
in.close();
out.close();
}
void ctcDriver() {
// Citire
ifstream in ("ctc.in");
ofstream out("ctc.out");
int n, m; in >> n >> m;
graf x(n, m);
x.citireOrientat( in );
// Afisare
vector < string > fin = x.ctc();
out << fin.size() << '\n';
for (string i: fin) {
out << i;
}
in.close();
out.close();
}
void sortaretDriver() {
// Citire
ifstream in ("sortaret.in");
ofstream out("sortaret.out");
int n, m; in >> n >> m;
graf x(n, m);
x.citireOrientat( in );
// Afisare
// Citim vectorul invers fata de cum l-am creat, pt ca ultimul nod care e accesat e nodul cu cele mai multe dependente
vector < int > res = x.sortaret();
for (int i = res.size() - 1; i >= 0; i--) {
out << res[i] << " ";
}
in.close();
out.close();
}
void havelHakimiDriver() {
// Citire
ifstream in ("havelhakimi.in");
ofstream out("havelhakimi.out");
int n; in >> n;
vector < int > a;
// Afisare
for (int i = 0; i < n; i++) {
int aux; in >> aux;
a.push_back(aux);
}
out << havelHakimi(a);
in.close();
out.close();
}
void criticalConnectionsDriver() {
// Citire
int n, m;
cin >> n >> m;
graf x(n, m);
x.citireNeorientat(cin);
// Afisare
vector < vector < int >> fin = x.criticalConnections();
for (auto i: fin) {
for (auto j: i) {
cout << j << " ";
}
}
}
int main() {
// Se apeleaza functia corespunzatoare problemei
ctcDriver();
return 0;
}