Лекция · 3.10

Графтағы алгоритм түрлері

Google Maps сенің үйіңнен мектепке дейінгі ЕҢ ҚЫСҚА жолды қалай табады? Ол миллиондаған көше мен қиылысты (граф түрінде) сақтап, арнайы алгоритммен «іздейді». Өткен сабақта графты сипаттауды үйрендік, ал бүгін — графтан нақты жауап табатын алгоритмдерді: DFS, BFS және ең қысқа жол іздеу тәсілдерін қарастырамыз.

Тереңнен іздеу (DFS)

Графтағы іздеу — белгілі бір шартқа сай төбені табу үшін графтың барлық төбесін жүйелі қарап шығу. Ең кең тараған екі әдіс бар — оларды лабиринтте жүру мысалымен түсінуге болады.

Анықтама
Тереңнен іздеу (Depth First Search, DFS) — бір тармақты СОҢЫНА ДЕЙІН толық өтіп, содан кейін ғана келесі тармаққа өту арқылы графты зерттеу.
🌀
Аналогия: DFS — лабиринтте бір қабырғаны қолыңмен ұстап, әрдайым сол жаққа бұрылып жүру әдісіне ұқсас. Тұйыққа тірелгенде ғана артқа қайтасың.
Есеп (қиын): DFS арқылы қаралған төбелер саны

Берілді: 10 төбелі граф, көршілестік тізімі, бастапқы төбе s=0.

n = 10
adj_list = [[2, 4, 6], [9], [0, 3], [2, 4], [0, 3], [], [0, 7, 8], [6], [6], [1]]
s = 0
visited = [False] * n

def dfs(v):
    visited[v] = True
    for w in adj_list[v]:
        if visited[w] == False:
            dfs(w)

dfs(s)
print(visited.count(True))

Түсіндірме: dfs(v) функциясы v төбесін «қаралды» деп белгілеп, оның әр көршісі үшін, егер ол әлі қаралмаса, ӨЗІН-ӨЗІ рекурсивті шақырады — дәл осылай бір тармақ соңына дейін жүреді.

Жауабы: visited.count(True) — бастапқы төбеден жетуге болатын барлық төбенің саны.

Көлденеңінен іздеу (BFS)
Анықтама
Көлденеңінен іздеу (Breadth First Search, BFS) — алдымен бастапқы төбемен КӨРШІЛЕС барлық төбені қарайды (1 қадам қашықтықтағылар), содан кейін 2 қадам қашықтықтағыларды, т.с.с.
🌊
Аналогия: BFS — суға тас тастағанда пайда болатын дөңгелек толқындарға ұқсас: алдымен жақын аймақ, содан кейін бірте-бірте алысырақ аймақ қамтылады.

DFS рекурсия (немесе стек) қолданса, BFS уақытша ақпаратты сақтау үшін кезек (жаңа элемент соңына қосылып, ескісі басынан алынатын тізім) қолданады.

Есеп (қиын): BFS арқылы қашықтық деңгейін табу

Берілді: 6 төбелі граф, көршілестік тізімі, бастапқы төбе s.

adj = [[1, 3], [0, 3, 4, 5], [4, 5], [0, 1, 5], [1, 2], [1, 2, 3]]
level = [-1] * len(adj)

def bfs(s):
    global level
    level[s] = 0
    stack = [s]
    while stack:
        v = stack.pop(0)
        for w in adj[v]:
            if level[w] == -1:
                level[w] = level[v] + 1
                stack.append(w)

bfs(0)
print(level)

Түсіндірме: stack.pop(0) кезектің БАСЫНАН алады (нағыз кезек әрекеті); level[w] == -1 болса, төбе әлі қаралмаған дегенді білдіреді, оған алдыңғы деңгейден 1 артық мән беріліп, кезекке қосылады.

«Тойымсыз» (greedy) алгоритм — әрдайым тиімді ме?

Ең ҚЫСҚА жолды табу үшін ең қарапайым идея — әр қадамда ЕҢ ЖАҚЫН қалаға өту. Бірақ бұл ӘРДАЙЫМ дұрыс жауап бере ме?

Есеп (қиын): тойымсыз алгоритм оптимал ма?

Берілді: A-B(7), A-C(9), A-F(14), B-C(10), C-F(2), C-E(6), F-E(9). A-дан E-ге қысқа жол керек.

«Тойымсыз» шешім: A→B (7) → C (10) → F (2) → E (9), жалпы: 28.

Нақты ең қысқа жол: A→C (9) → F (2) → E (9), жалпы: 20.

Жауабы: тойымсыз алгоритм 28 береді, бірақ ОҢТАЙЛЫ жауап — 20! Демек, әр қадамда ЖЕРГІЛІКТІ ең жақсы таңдау ЖАЛПЫ ең жақсы нәтижеге әкелмеуі мүмкін.

⚠️
Маңызды түсінік: «Тойымсыз» алгоритм әрқашан оптимал бола бермейді, БІРАҚ кейбір есеп түрлерінде (мыс. төмендегі Прима-Крускал есебі) ол әрқашан дұрыс шешім береді.
Ең қысқа жол алгоритмдері

Информатикада ең қысқа жол іздеудің бірнеше атақты алгоритмі бар — олардың айырмашылығы, НЕ ІЗДЕЙТІНІНДЕ:

АлгоритмНе табады
Прима-КрускалБарлық төбені байланыстыратын МИНИМАЛДЫ ЖАЛПЫ ұзындықтағы ағаш (N қаланы ең аз кабельмен байланыстыру)
ДейкстраБІР бастапқы төбеден БІР соңғы төбеге дейінгі ең қысқа жол
ФлойдБАРЛЫҚ төбе жұптары арасындағы ең қысқа жол (бір реттен)

Прима-Крускал — тойымсыз алгоритм, БІРАҚ бұл жерде ол әрқашан дұрыс жұмыс істейді: әр қадамда ағашқа қосылмаған ЕҢ АРЗАН қабырға таңдалады, циклдің құрылмауын бақылау үшін әр төбе «түспен» белгіленеді.

col = [i for i in range(N)]
ostov = []
for k in range(N - 1):
    minDist = 1e10
    for i in range(N):
        for j in range(N):
            if col[i] != col[j] and W[i][j] < minDist:
                iMin, jMin, minDist = i, j, W[i][j]
    ostov.append((iMin, jMin))
    c = col[jMin]
    for i in range(N):
        if col[i] == c:
            col[i] = col[iMin]

Флойд алгоритмі ерекше ықшам — небәрі үш кірістірілген цикл арқылы БАРЛЫҚ жұптың ең қысқа жолын табады, әр аралық k төбе арқылы өту қысқарақ бола ма деп тексереді:

for k in range(N):
    for i in range(N):
        for j in range(N):
            if a[i][k] + a[k][j] < a[i][j]:
                a[i][j] = a[i][k] + a[k][j]

Дейкстра алгоритмі бір бастапқы төбеден бастап, әр қадамда ТЕКСЕРІЛМЕГЕН, ЕҢ ЖАҚЫН төбені таңдап, оның арқылы басқа қашықтықтарды «қысқартады»:

D[K] = 0  # бастапқы төбе қашықтығы 0
for i in range(N):
    M = -1
    for j in range(N):
        if not Use[j] and (M == -1 or D[j] < D[M]):
            M = j
    Use[M] = True
    for j in range(N):
        if D[j] > A[M][j] + D[M]:
            D[j] = A[M][j] + D[M]
🎯 ҰБТ байланысы
DFS/BFS айырмашылығы мен алгоритмдердің қолданылу аймағын ажырату маңызды.
1
DFS — рекурсия/стек, «тереңге» бірінші жүреді; BFS — кезек, «деңгей-деңгей» жүреді.
2
Тойымсыз алгоритм жалпы жағдайда ОҢТАЙЛЫ болмауы мүмкін.
3
Дейкстра — бір-екі төбе арасы, Флойд — барлық жұп, Прима-Крускал — минималды ағаш екенін ажырату.
Сабақ қорытындысы
  • DFS — бір тармақты соңына дейін өтіп зерттейді (рекурсия/стек); BFS — деңгей-деңгей зерттейді (кезек).
  • «Тойымсыз» алгоритм әр қадамда жергілікті ең жақсыны таңдайды, бірақ жалпы оптимал болмауы мүмкін.
  • Прима-Крускал — минималды ішкі ағаш; Дейкстра — бір жұп арасындағы қысқа жол; Флойд — барлық жұп арасындағы қысқа жол.