Zoznam úloh

4. Koľko strihaní potrebujeme?

Zadanie

Pokojne sedíte vo svojej kancelárii a vychutnávate si kávu. Ste radi, že vás práve povýšili na CEO celoštátnej firmy „Kolosálne Spojenie Psikov“ a užívate si výhľad zo svojho okna. Spomínate si, ako ste začínali pracovať pre túto firmu, ktorá rozdeľuje elektrinu do všetkých miest nášho milovaného štátu. Pamätáte si výstavbu masívnej elektrárne a pokladanie káblov medzi našimi malebnými mestami.

Zrazu začujete krik a do vašej kancelárie vbehne vaša sekretárka Miška s desivou správou: gigantická elektráreň má problémy a jej produkcia elektriny kolíše. To môže viesť k preťaženiu siete a následným explóziám rozvodných staníc. Vypnúť elektráreň nie je možné, pretože by to zruinovalo vašu firmu. Našťastie vás napadne riešenie:

Každé mesto má rozvodnú stanicu, ktorú viete na diaľku vypnúť. Tým odpojíte mesto od siete, čím riskujete kritiku. Aby ste predišli kolapsu, potrebujete vypínať mestá v takom poradí, že ak odpojíte od siete prvých $j$ miest v poradí, všetky zvyšné mesta zostanú spojené s elektrárňou.

Úloha

Elektrická sieť pozostáva z $n$ miest, očíslovaných od $0$ po $n-1$, $m$ káblov a práve jednej elektrárne, ktorá sa nachádza v meste $k$. Elektrina prúdi od elektrárne do všetkých miest, do ktorých sa z nej dá dostať káblom (káble vedia prenášať energiu obojsmerne).

Keď vypneme mesto od elektriny, vypneme všetky káble ktoré cez neho vedú, a teda cez neho nesmie ani prúdiť elektrina do iných miest.

Nájdite postupnosť v ktorej vieme odpájať mestá od elektriny, tak aby pre každé $1 \leq j \leq n$ platilo, že ak od elektriny odpojíme prvých $j$ miest, neovplyvníme tak elektrinu do zvyšných $n-j$ miest.

Všimnite si, že ak od elektriny odpojíte mesto s elektrárňou, do ďalších miest už elektrina ísť nevie (nemá odkiaľ).

Formát vstupu

  • Prvý riadok obsahuje čísla $1 \leq n \leq 10^5$ (počet miest), $0\leq m \leq \min(2\cdot 10^5, \frac{n(n-1)}{2})$ (počet káblov) a $0\leq k\leq n - 1$ (číslo mesta s elektrárňou), oddelené medzerou.

  • Nasleduje $m$ riadkov s dvojicami čísel $0\leq a_i\neq b_i\leq n-1$ oddelených medzerou, označujúcich, že medzi mestami $a_i$ a $b_i$ sa nachádza kábel.

Je garantované, že v zadanej elektrickej sieti má každé mesto prístup k elektrine.

Formát výstupu

Vypíšte jeden riadok v ktorom sa nachádza $n$ medzerami oddelených čísel: postupnosť v ktorom vieme odpájať mestá od elektriny.

V prípade, že existuje viac postupností, vypíšte ľubovoľnú.

Malé varovanie

V prípade, že používate python a rekurziu, nezabudnite, že prednadstavený limit rekurzie v pythone je $1000$, a pre väčšie hĺbky ju treba manuálne nastaviť v programe cez sys.setrecursionlimit.

Bodovanie a obmedzenia

Existujú štyri testovacie sady, každá za dva body, v ktorých platia nasledovné obmedzenia:

Sada 1 2 3 4
$1 \leq N \leq$ $10$ $10^5$ $200$ $10^5$
$1 \leq M \leq$ $45$ $10^5$ $1\,000$ $2\cdot 10^5$

Zároveň, v druhej sade navyše platí, že každé mesto je prepojené káblami s práve dvoma inými mestami.

Príklady

Input:

3 3 1
0 1
1 2
2 0

Output:

0 2 1

Tento vstup by sa mohol nachádzať v druhej sade. Keď odpojím od energie mesto číslo $0$, elektrina vie medzi elektrárňou (v meste $1$) a mestom $2$ prúdiť cez priamy kábel. Keď následne odpojíme mesto $2$, v meste $1$ stále je elektrina, lebo sa v ňom nachádza elektráreň. Napokon, keď vypneme elektráreň, ovplyvní to len to mesto, v ktorom sa nachádza.

Input:

3 2 2
0 1
1 2

Output:

0 1 2

Všimnite si, že nemôžeme najskôr odpojiť mesto $1$, keďže jediný spôsob, akým sa elektrina vie dostať do mesta $0$ je cez mesto $1$.

Input:

2 1 0
0 1

Output:

1 0

Nemáme na výber. Najskôr musíme vypnúť elektrinu v meste bez elektrárne

Input:

1 0 0

Output:

0

Máme jediné mesto, v ktorom je elektráreň.

Input:

8 11 2
1 0
2 0
1 6
7 1
6 7
2 6
3 2
4 5
5 2
3 4
2 1

Output:

0 7 6 1 5 4 3 2

Je možné že pre nejaký vstup existuje viacero správnych riešení. Akékoľvek z nich bude akceptované.

Skúšame všetky možnosti

Nič nepokazíme hrubou silou: vygenerujeme všetky možné rôzne poradia miest, tých je $n\times (n-1) \times \dots 1 = n!$, napríklad pomocou next_permutation alebo itertools.permutations(), a pre každé takéto poradie overíme, či spĺňa podmienku. Konkrétne si pre každé poradie vieme postupne simulovať vypínanie miest.

Po každom odpojení prvých $1 \leq i\leq n - 1$ miest, si treba overiť, či všetky zostávajúce mestá sú stále spojené s elektrárňou. Ako prvé skontrolujeme, či sme náhodou neodpojili aj ju (ak áno, musí byť posledné vypnuté mesto v poradí).

Následne si vieme spustiť prehľadávanie grafu z elektrárne (DFS alebo BFS, viď kuchárku). Potom si len stačí overiť, či sme každé miesto už buď odpojili, alebo navštívili.

Pre každé možné poradie spustíme $n$-krát prehľadávanie celého grafu, ktoré zaberie v najhoršom prípade $O(n+m)$ času. Poradí je $n!$, takže časová zložitosť je $O(n! \cdot n \cdot (n + m))$. Pamäťová zložitosť je $O(n + m)$, keďže si musíme náš graf celý pamätať.

Krok ku riešeniu

Všimnite si, že už len zisťovanie, či nejaké poradie funguje nám zaberie kvadratický čas – $O(n(n+m))$, čo je príliš veľa.

Skúsme, namiesto tipovania riešení a skúšania či fungujú, sa zamyslieť nad tým aké riešenia určite fungovať budú. Všimnime si, že mesto ktoré odstránime posledné vieme – je ním mesto, v ktorom je elektráreň. Nazvime ho $a_n$.

Čo vieme povedať o meste, ktoré odpojíme ako predposledné? Zjavne musí priamo susediť s elektrárňou, inak by stratilo prúd skôr než ho odpoja.

Zoberme si teda akékoľvek mesto súvisiace s elektrárňou, nazvime ho $a_{n-1}$.

Keď už máme mestá ktoré odpojíme ako posledné a predposledné ($a_n$ a $a_{n-1}$), nájdeme si mesto, ktoré odpojíme ako pred-predposledné, následne nájdeme to pred-pred-posledné, a tak ďalej.

Inak povedané, postupnosť zostavujeme odzadu. A teda namiesto toho, aby sme mestá odstraňovali, budeme mestá postupne pridávať. A chceme ich pridávať tak, aby sieť, ktorú si postupne budujeme, bola vždy súvislá, a nachádza sa v nej elektráreň.

Druhú podmienku, ako sme si už povedali hore, splníme tak, že poslednú vždy odstránime elektráreň. Ako splníme prvú podmienku? Jednoducho, keď pridávame mesto $a_i$, pridáme mesto ktoré sme ešte nepridali (nie je medzi $a_{i+1},\cdot a_n$), ale ktoré je priamo spojené s niektorým $a_{i+j}$.

Na základe tohto nápadu vieme implementovať riešenie bežiace v $O(n \cdot (n+m))$: najskôr nadstavíme $a_n=k$ (mesto s elektrárňou). Potom, budujeme postupnosť odzadu: ak už máme $a_n, a_{n-1},\dots, a_{i+1}$, si prejdeme susedov týchto miest, a nájdeme takého ktorý sa ešte v postupnosti nenachádza. Zakaždým prejdeme najviac $m$ hrán, takže dostaneme horeuvedenú časovú zložitosť.

Vzorové riešenie

Odtiaľto sme už len kúsok od vzorového riešenia. Všimnite si, že v riešení hore pozeráme na hrany viackrát. Stačilo by nám, napríklad, si vždy keď do postupnosti pridáme nové $a_i$, si jeho susedov pridať do fronty (alebo zásobníka). Potom, keď hľadáme nový vrchol do postupnosti, prejdeme si túto frontu (zásobník) vrcholov. Ak sme už vrchol dali do postupnosti, odstránime ho z fronty (zásobníku) a túto hranu už nikdy neuvidíme – nemusíme.

Ak sme ešte vrchol nevideli, nastavíme ho ako nový člen postupnosti a pridáme jeho susedov do fronty (zásobníku).

Takto robíme to, čo predtým, len efektívnejšie: ku každému susedovi sa dostaneme maximálne raz, ale zároveň, keďže je naša mestská sieť súvislá, určite naplníme celú postupnosť. Takto teda dostaneme riešenie bežiace v časovej a aj pamäťovej zložitosti $O(n + m)$.

A nielen to! Záležiac od toho akú dátovú štruktúru ste použili, naprogramovali ste vlastne BFS (v prípade fronty), alebo DFS (v prípade zásobníka)!

Prečo toto riešenie vždy

funguje?

Pozrime sa napokon na to, prečo je takáto postupnosť odpájania $a_1,\dots,a_n$ v súlade s požiadavkami zo zadania.

  1. Zachovanie spojitosti: Keď vypíname mestá v reverznom poradí akom sme ich pridávali (čiže od $a_1$ po $a_k$), vždy vypneme najskôr tie mestá, ktoré boli pridané ako posledné pri budovaní siete. Ale pridávali sme ich predsa tak, aby mestá $a_i,a_{i+1},\dots,a_n$ boli súvislé, čiže vypnutie prvých $i$ miest nikdy nepreruší spojenie medzi elektrárňou a zvyšnými mestami.

  2. Elektráreň posledná: Keďže sme začali prehľadávanie v elektrárni, tá sa automaticky stane prvým prvkom v poradí návštev, a teda posledným prvkom v poradí vypínania.

  3. Úplné pokrytie: Elektrická sieť je súvislá, takže ak nám ostávajú ešte nejaké mestá, je medzi nimi, a už pridanými mestami nejaká hrana.

n,m,k = map(int,input().split())
susedia = [[] for _ in range(n)]

for _ in range(m):
    a,b = map(int, input().split())
    susedia[a].append(b)
    susedia[b].append(a)

prejdeny = [False] * n
poradie = []
stack = [k]
prejdeny[k] = True

while stack:
    vrchol = stack.pop()
    poradie.append(vrchol)

    for sused in susedia[vrchol]:
        if not prejdeny[sused]:
            prejdeny[sused] = True
            stack.append(sused)

print(*reversed(poradie))
#include<bits/stdc++.h>

using namespace std;

#define FOR(i,n)    for(int i=0;i<(int)n;i++)
#define FOB(i,n)    for(int i=n;i>=1;i--)
#define MP(x,y) make_pair((x),(y))
#define ii pair<int, int>
#define lli long long int
#define ld long double
#define ulli unsigned long long int
#define lili pair<lli, lli>
#ifdef EBUG
#define DBG if(1)
#else
#define DBG if(0)
#endif
#define SIZE(x) int(x.size())
const int infinity = 2000000999 / 2;
const long long int inff = 4000000000000000999;

typedef complex<long double> point;

template<class T>
T get() {
    T a;
    cin >> a;
    return a;
}

template <class T, class U>
ostream& operator<<(ostream& out, const pair<T, U> &par) {
    out << "[" << par.first << ";" << par.second << "]";
    return out;
}

template <class T>
ostream& operator<<(ostream& out, const set<T> &cont) {
    out << "{";
    for (const auto &x:cont) out << x << ", ";
    out << "}";
    return out;
}

template <class T, class U>
ostream& operator<<(ostream& out, const map<T,U> &cont) {
    out << "{";
    for (const auto &x:cont) out << x << ", ";

    out << "}"; return out;
}

template <class T>
ostream& operator<<(ostream& out, const vector<T>& v) {
    FOR(i, v.size()){
      if(i) out << " ";
      out << v[i];
    }
    out << endl;
    return out;
}

bool ccw(point p, point a, point b) {
    if((conj(a - p) * (b - p)).imag() <= 0) return false;
    else return true;
}

void dfs(int v, vector<bool> &seen, vector<int> &poradie, vector<vector<int> >&hrany) {
    seen[v] = true;
    for (int w : hrany[v]) {
        if (!seen[w]) dfs(w, seen, poradie, hrany);
    }
    poradie.push_back(v);
}

int main() {
    cin.sync_with_stdio(false); cin.tie();
    cout.sync_with_stdio(false); cout.tie();
    int n = get<int>();
    int m = get<int>();
    int k = get<int>();
    vector<int> poradie;
    vector<bool> seen(n, false);
    vector<vector<int> > hrany(n);
    FOR(i, m) {
        int a = get<int>();
        int b = get<int>();
        hrany[a].push_back(b); hrany[b].push_back(a);
    }

    dfs(k, seen, poradie, hrany);
    FOR(i, n) {
        cout << (i ? " " : "") << poradie[i];
    }
    cout << endl;

}
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