Zoznam úloh

3. Aspoň jedna cesta von

Zadanie

Po veľkom úspechu pri stavbe druhého slovenského mrakodrapu v Kokave nad Rimavicou sa dcérska firma Trojstenu (ktorá sa zaoberá cestnými stavbami) rozhodla pustiť do stavby ciest. Keďže zamestnanci v tejto firme sú výrazne lepší programátori ako robotníci, tak sa nejaké cesty síce postavili, ale iba tak veľmi náhodne. Čo je ešte horšie, všetky postavené cesty sú iba jednosmerné, na druhý smer sa trochu zabudlo.

To sa ľuďom ani trochu nepáčilo a teda sa začali búriť. Aby vedenie Trojstenu upokojilo situáciu, rozhodlo sa sľúbiť ľuďom, že označí postavené cesty tak, aby z každého mesta viedla aspoň jedna cesta. Bohužiaľ, tento nerozvážny sľub vedenie dalo pred tým, než si overili, že to je možné.

Keďže všetci v Trojstene sa vybrali otáčať cedule na cestách, tak neostal nikto, kto by vlastne zistil, či je ich možné pootáčať tak aby splnili sľub. Vedeli by ste to zistiť vy?

Úloha

Máme cestnú sieť, ktorá sa skladá z miest a ciest medzi nimi. Tieto cesty sú všetky jednosmerné a vašou úlohou je zistiť, či ich vieme orientovať tak, aby z každého mesta išla von aspoň jedna cesta. Každá cesta spája vždy práve dve mestá, nemôže ísť z mesta do toho istého mesta a medzi dvomi mestami vedie vždy najviac jedna cesta.

Formát vstupu

V prvom riadku vstupu sú dve medzerou oddelené čísla $n, m$ ($1 \leq n \leq 10^6$, $0 \leq m \leq 10^6$) počet miest a počet ciest.

Nasleduje $m$ riadkov, na každom sú dve medzerou oddelené čísla $a,b$ ($0 \leq a,b < n, a \neq b$), ktoré hovoria, že mestá $a$ a $b$ sú spojené cestou. Mestá sú označené číslami od $0$ po $n-1$.

V jednotlivých sadách platia nasledujúce obmedzenia:

Sada 1 2 3 4
$1 \leq n \leq$ $20$ $100$ $1\,000$ $100\,000$
$1 \leq m \leq$ $40$ $10\,000$ $1\,000\,000$ $1\,000\,000$

Formát výstupu

Vypíšte jediný riadok, na ktorom je buď text ano, ak vieme orientovať cesty tak, aby z každého mesta viedla aspoň jedna cesta. Ak to nejde, vypíšte nie.

Príklady

Input:

3 3
0 1
1 2
2 0

Output:

ano

V tomto prípade vieme napríklad orientovať cesty tak, že cesty idú 1 -> 0, 0 -> 2 a 2 -> 0.

Input:

4 3
0 1
1 2
2 0

Output:

nie

Podobný prípad, ale máme o jedno mesto naviac (mesto 3). Z tohoto mesta žiadna cesta vychádzať nemôže, keďže na žiadnej ceste nie je.

Input:

4 2
0 1
2 3

Output:

nie

Nech orientujeme cesty akokoľvek, nevieme zariadiť, aby zo všetkých miest vychádzala aspoň jedna cesta.

Input:

6 6
0 1
1 2
2 0
3 4
4 5
5 3

Output:

ano

Ak cesty orientujeme ako 0 -> 1, 1 -> 2, 2 -> 0, 3 -> 4, 4 -> 5, 5 -> 3, tak z každého mesta vychádza aspoň jedna cesta.

Vašou úlohou bolo zistiť, či vieme orientovať cesty tak, aby z každého mesta vychádzala aspoň jedna cesta. Tí z vás, ktorí už počuli o grafoch, si určite všimli, že túto úlohu vieme reprezetovať pomocou grafu, kde mestá sú vrcholy a cesty sú hrany. V ďalšom texte teda budeme pre jednoduchosť používať grafovú terminológiu. V grafovej terminológii teda zadanie úlohy znie, že máme zistiť, či vieme orientovať hrany v neorientovam grafe tak, aby z každého vrcholu vychádzala aspoň jedna hrana.

Pozorovania

Dôležitým pozorovaním pri riešení tejto úlohy je, že si môžeme graf rozdeliť na komponenty súvislosti a každý komponent súvislosti skúmať samostatne. Je to preto, lebo to, či vieme správne orientovať hrany v jednom komponente nijako neovplyvňuje ostatné komponenty. Ak aspoň v jednom komponente nevieme orientovať hrany podľa zadania, tak potom je odpoveď nie. Vďaka tomuto sa môžeme v ďalšom texte zoberať iba grafmi, ktoré sú tvorené presne jedným komponentom súvislosti.

Začnime riešenie úlohy tak, že si napíšeme niektoré prípady, kedy učite nevieme hrany orientovať podľa zadania. Ak máme jeden komponent súvislosti, ktorý sa skladá z jediného samostatného vrcholu, do ktorého nejdú žiadne hrany, tak v ňom určite nevieme orientovať hrany podľa zadania (tento vrchol nemá žiadnu hranu, takže z neho nemá aká vychádzať). Rovnako, ak napríklad máme komponent, ktorý sa skladá presne z dvoch vrcholov spojených hranou, z jedného z nich hrana vychádzať nemôže.

Ak si takto rozoberieme ešte niekoľko prípadov zistíme, že ak je komponent grafu strom, tak v ňom nevieme orientovať hrany podľa zadania. Predstavme si totiž ľubovoľný zakorenený strom. Ak v ňom orientujeme hrany zvrchu dole (od koreňa), tak žiadna hrana nebude vychádzať z listov. Ak orientujeme hrany opačne (od listov), tak žiadna hrana nebude vychádzať z koreňa. A samozrejme, ak orientujeme hrany hocijako inak, tak niekde v strede grafu nám vznikne vrchol/vrcholy, z ktorých žiadna hrana nevychádza.

Hlavná myšlienka

Už vieme, že ak je komponent grafu strom, tak v ňom nevieme zorientovať hrany podľa zadania. Otázka je, že či vo všetkých ostatných druhoch grafov vieme správne zorientovať hrany. Odpoveď je áno. Stačí ak doplníme do stromu na akékoľvek miesto jednu hranu. Potom môžeme strom zakoreniť v jednom z týchto vrcholov, kam sme pridali hranu. Teraz sa dajú v strome všetky hrany orientovať z listov smerom hore. To znamená, že ak by náš graf bol strom (bez pridanej hrany), tak jediný vrchol z ktorého by nevychádzala hrana by bol koreň. Do koreňa vedie ale ešte jedna hrana naviac (tá, ktorá je v grafe naviac oproti stromu). Túto hranu vieme orientovať smerom z koreňa. Z vrcholu kam hrana vedie už nejaká hrana vychádza (ide smerom hore do jeho rodiča), takže tým nič nepokazíme.

Postup sa teda dá zhrnúť takto: - rozdelíme si graf na komponenty súvislosti - prejdeme všetky komponenty súvislosti a ak každý obsahuje oproti stromu aspoň jednu hranu naviac, tak odpovedáme ano - inak odpovedáme nie

Ako nájsť “hrany naviac”?

Teraz sa pozrime na to, ako zistiť, či graf obsahuje nejakú “hranu naviac” oproti stromu. Predstavme si napríklad, ako prehľadáva tento strom prehľadávanie do hĺbky. Toto prehľadávanie postupne prechádza všetky vrcholy a v každom vrchole sa pozrie na všetkých jeho susedov. Týchto susedov môžeme rozdeliť do troch kategórií. Susedný vrchol môže byť: - ešte nenavštívený - ten vrchol z ktorého sme do aktuálneho vrcholu prišli (a teda je navštívený) - hociktorý iný už navštívený vrchol

Ak si predstavíme ako postupne prechádza prehľadávanie do hĺbky stromom, tak každú hranu v strome vidíme dva krát: raz, keď po nej ideme do nového vrcholu a druhý krát, keď by sme sa po nej vrátili do vrcholu z ktorého sme do aktuálneho vrcholu prišli (vedie do rodiča aktuálneho vrcholu). Tieto dva prípady zodpovedajú prvým dvom kategóriám susedných vrcholov.

Ak máme v grafe nejakú hranu naviac oproti stromu, tak táto hrana nevedie do nového vrcholu a ani nevedie do rodiča aktuálneho vrcholu (keďže podľa zadania môžeme predpokladať, že v grafe neexistujú násobné hrany).

Hrany naviac teda vieme nájsť pomerne jednoducho: stačí spustiť prehľadávanie do hĺbky z ľubovoľného vrcholu. V každom vrchole si potrebujeme pamätať z ktorého vrcholu sme sa do aktuálneho vrcholu dostali (nášho rodiča). Ak nájdeme susedný vrchol, ktorý je už navštívený a nie je to náš rodič, tak sme našli “hranu naviac” a vieme, že v tomto komponente vieme zorientovať hrany podľa zadania.

Zhrnutie a zložitosti

Ako sme už písali vyššie, tak postupne prechádzame všetky komponenty súvislosti grafu a o každom sa rozhodujeme samostatne. Ak každý komponent súvislosti obsahuje oproti stromu aspoň jednu hranu naviac, tak odpovedáme ano, inak odpovedáme nie. To, že či komponent obsahuje nejakú hranu naviac oproti stromu zisťujeme prehľadávaním do hĺbky.

Tento program si okrem pár premenných potrebuje pamätať vstupný graf, ktorý obsahuje $n$ vrcholov a $m$ hrán, takže pamäťová zložitosť je $O(n+m)$. Čo sa týka časovej zložitosti, tak na grafe niekoľko krát (pre každý komponent jeden krát) spustíme prehľadávanie do hĺbky. Je dobré si uvedomiť, že nezáleží na tom, koľko komponentov súvislosti vlastne graf obsahuje. Na určenie časovej zložitosti stačí, že vieme, že dokopy vo všetkých prehľadávaniach prejdeme celý graf (budeme v každom vrchole presne raz a na každú hranu sa pozrieme konštantný počet krát). Časová zložitosť je teda $O(n+m)$.

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

vector<vector<ll> > G;
vector<bool> visited;

bool dfs(ll v, ll parent){
    bool answer = false;
    visited[v] = true;
    for(ll i=0;i<G[v].size();i++){
        ll u = G[v][i];
        if(u!=parent && visited[u])
            answer = true;
        if(!visited[u]){
            answer = dfs(u, v) || answer;
        }
    }
    return answer;
}

int main() {
    cin.tie(0)->sync_with_stdio(0);

    ll n, m;
    cin >> n >> m;
    G.resize(n);
    visited.resize(n, false);

    for (ll i=0;i<m;i++){
        ll a, b;
        cin >> a >> b;
        G[a].push_back(b);
        G[b].push_back(a);
    }

    for(ll i = 0; i<n; i++){
        if(!visited[i]){
            if(!dfs(i, -1)){
                cout<<"nie"<<endl;
                return 0;
            }
        }
    }
    cout<<"ano"<<endl;
    return 0;
}
from sys import setrecursionlimit

setrecursionlimit(1000000)

def dfs(v, parent):
    answer = False
    visited[v] = True
    for u in G[v]:
        if u!=parent and visited[u]:
           answer = True
        if not visited[u]:
            answer = dfs(u, v) or answer 
    return answer

def solve():
    for i in range(n):
        if not visited[i]:
            result = dfs(i, -1)
            if not result:
                print("nie")
                return
    print("ano")

n, m = map(int, input().split())
G = [[] for _ in range(n)]
visited = [False for _ in range(n)]
for i in range(m):
    a, b = map(int, input().split())
    G[a].append(b)
    G[b].append(a)

solve()
Pre odovzdávanie sa musíš prihlásiť.
Trojsten

Korešpondenčný seminár z programovania zastrešuje občianske združenie Trojsten.

Kontakt
Ďalšie projekty