/
Блог Хекслета
/
Дневник студента
/

10 алгоритмов для программистов, которые упрощают нам работу. Часть 2

10 алгоритмов для программистов, которые упрощают нам работу. Часть 2

9 февраля 2023 г.

6 минут
3
10 алгоритмов для программистов, которые упрощают нам работу. Часть 2

В этой статье я хотел бы представить свой топ-10 алгоритмов, которые могут помочь вам в работе. Бонусом покажу на примерах, как ими пользоваться.

Сам список включает в себя следующие алгоритмы:

  1. QuickSort - алгоритм разделения и покорения, который рекурсивно разбивает массив на более мелкие подмассивы и сортирует их.

  2. MergeSort — алгоритм «разделяй и властвуй», который рекурсивно делит массив пополам, сортирует половинки, а затем объединяет их обратно.

  3. Бинарный поиск - алгоритм поиска, который многократно делит интервал поиска пополам, ища определенное значение в отсортированном массиве.

  4. Поиск в глубину (DFS) — алгоритм обхода графа, который исследует как можно дальше по каждой ветви, прежде чем вернуться назад.

  5. Breadth-first Search (BFS) — алгоритм обхода графа, который исследует все вершины графа или все узлы дерева уровень за уровнем.

  6. Динамическое программирование - метод решения проблем путем разбиения их на более мелкие подпроблемы и хранения решений этих подпроблем, чтобы избежать лишней работы.

  7. Алгоритм Беллмана-Форда — алгоритм кратчайшего пути с одним источником, который также может обнаруживать отрицательные циклы в графе.

  8. Алгоритм Дейкстры — алгоритм кратчайшего пути с одним источником, который работает путем многократного ослабления оценок расстояния между вершинами.

  9. Алгоритм поиска A* - алгоритм поиска, который использует эвристику для направления поиска и обычно используется для поиска пути и решения задач обхода графа.

  10. Knapsack Problem — задача комбинаторной оптимизации, целью которой является максимизация общей стоимости предметов в рюкзаке без превышения его вместимости.

Для примерах реализации я буду использовать Typescript, так как это ЯП с сильной типизацией, но в тоже время достаточно простой для понимания.

Заметка: все вышеперечисленные алгоритмы реализованы с помощью Typescript и могут быть найдены в менеджере пакетов npm.

Реализацию QuickSort, MergeSort, Бинарного поиска, Поиска в глубину (DFS) мы можете найти в первой части.

Сегодня мы разберем Breadth-first Search (BFS), Динамическое программирование и Алгоритм Беллмана-Форда

Поехали! 🚀

Пример алгоритма поиска в ширину (BFS), реализованного на TypeScript:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class Graph {
    constructor(private numOfVertices: number) {
        this.adjList = new Map<number, number[]>();
    }

    private adjList: Map<number, number[]>;

    addVertex(v: number) {
        this.adjList.set(v, []);
    }

    addEdge(v: number, w: number) {
        this.adjList.get(v).push(w);
        this.adjList.get(w).push(v);
    }

    bfs(start: number) {
        let visited: boolean[] = new Array(this.numOfVertices).fill(false);
        let queue: number[] = [];

        visited[start] = true;
        queue.push(start);

        while (queue.length !== 0) {
            let vertex = queue.shift();
            console.log(vertex);

            let neighbours = this.adjList.get(vertex);
            for (let i = 0; i < neighbours.length; i++) {
                let neighbour = neighbours[i];
                if (!visited[neighbour]) {
                    visited[neighbour] = true;
                    queue.push(neighbour);
                }
            }
        }
    }
}

let g = new Graph(6);
let vertices = [0, 1, 2, 3, 4, 5];

for (let i = 0; i < vertices.length; i++) {
    g.addVertex(vertices[i]);
}

g.addEdge(0, 1);
g.addEdge(0, 2);
g.addEdge(1, 3);
g.addEdge(2, 4);
g.addEdge(3, 5);

console.log('BFS:');
g.bfs(0);

Данная реализация BFS использует представление графа в виде списка смежности, где каждая вершина представлена ключом в карте, а ее значение — массив соседних вершин.

Функция bfs начинает работу с инициализации очереди, добавления в нее начальной вершины и пометки ее как посещенной. Затем с помощью цикла while выполняется итерация по очереди, удаление первой вершины, ее печать, добавление в очередь всех ее непосещенных соседей и пометка их как посещенных.

Стоит отметить, что в данном примере показан обход BFS, начиная с вершины 0, но его можно начать с любой вершины, и для реализации BFS полезно использовать структуру данных очереди.

Кроме того, это базовая реализация BFS, и ее можно использовать для различных целей, таких как поиск кратчайшего пути между двумя вершинами, проверка существования пути между двумя вершинами и решение таких задач, как обход лабиринта и поиск кратчайшего пути в сетке.

Детальное объяснение можете найти тут

Динамическое программирование

Динамическое программирование (ДП) — это метод решения проблем путем разбиения их на более мелкие подпроблемы и хранения решений этих подпроблем, чтобы избежать лишней работы. Этот подход особенно полезен для решения, которые имеют перекрывающиеся подпроблемы, т.е. одни и те же подпроблемы решаются несколько раз.

Существует две основные техники, используемые в динамическом программировании:

Мемоизация: Этот метод предполагает хранение решений подпроблем в кэше или таблице, чтобы их можно было использовать в дальнейшем. — Табулирование: Эта техника предполагает заполнение таблицы снизу вверх, начиная с наименьших подпроблем и доводя до окончательного решения.

Вот пример использования динамического программирования для решения проблемы последовательности Фибоначчи:

1
2
3
4
5
6
7
8
9
10
11
12
function fibonacci(n: number, memo: number[]): number {
    if (n <= 0) return 0;
    if (n === 1) return 1;
    if (memo[n] !== undefined) return memo[n];

    memo[n] = fibonacci(n - 1, memo) + fibonacci(n - 2, memo);
    return memo[n];
}

let memo = new Array(10);
let result = fibonacci(8, memo);
console.log(result);

В этом примере функция fibonacci принимает на вход число n и массив memo и возвращает n-е число последовательности Фибоначчи, используя мемоизацию. Функция проверяет, хранится ли уже результат для текущего n в массиве memo, если да, то возвращает его, в противном случае вычисляет результат путем сложения результатов (n-1)-го и (n-2)-го чисел Фибоначчи и сохраняет его в массиве memo для дальнейшего использования.

Этот подход имеет временную сложность O(n), поскольку мы вычисляем каждое число последовательности Фибоначчи только один раз и можем повторно использовать результаты предыдущих вычислений.

Динамическое программирование может быть применено ко многим проблемам, таким как проблема кратчайшего пути, проблема ранца и некоторые проблемы оптимизации. Важно понять суть проблемы, прежде чем применять динамическое программирование, так как некоторые проблемы не могут быть решены с помощью этого метода. Также важно понимать временную и пространственную сложность решения.

Алгоритм Беллмана-Форда

Алгоритм Форда-Беллмана позволяет найти кратчайшие пути из одной вершины графа до всех остальных, даже для графов, в которых веса ребер могут быть отрицательными. Тем не менее, в графе не должно быть циклов отрицательного веса, достижимых из начальной вершины, иначе вопрос о кратчайших путях является бессмысленным. При этом алгоритм Форда-Беллмана позволяет определить наличие циклов отрицательного веса, достижимых из начальной вершины.

Алгоритм Форда-Беллмана использует динамическое программирование. Введем функцию динамического программирования:

— F[k][i] — длина кратчайшего пути из начальной вершины до вершины i, содержащего не более k ребер.

Начальные значения зададим для случая k=0. В этом случае F[0][start] = 0, а для всех остальных вершин i F[0][i] = INF, то есть путь, состоящий из нуля ребер существует только от вершины start до вершины start, а до остальных вершин пути из нуля ребер не существует, что будем отмечать значением INF.

Далее будем вычислять значения функции F увеличивая число ребер в пути k, то есть вычислим кратчайшие пути, содержащие не более 1 ребра, кратчайшие пути, содержащие не более 2 ребер и т. д. Если в графе нет циклов отрицательного веса, то кратчайший путь между любыми двумя вершинами содержит не более ребра ( - число вершин в графе), поэтому нужно вычислить значения F[n-1][i], которые и будут длинами кратчайших путей от вершины start до вершины i).

Рассмотрим, как вычисляется значение F[k][i]. Пусть есть кратчайший маршрут из вершины start до вершины i, содержащий не более k ребер. Пусть последнее ребро этого маршрута есть ребро j-i. Тогда путь до вершины j содержит не более k-1 ребра и является кратчайшим путем из всех таких путей, значит, его длина равна F[k-1][j], а длина пути до вершины i равна F[k-1][j] + W[j][i], где W[j][i] есть вес ребра j-i. Дальше необходимо перебрать все вершины j, которые могут выступать в качестве предыдущих, и выбрать минимальное значение F[k-1][j] + W[j][i].

Получаем следующий алгоритм:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
/// <reference path="q.module.d.ts" />

import q = module('q');

export interface IList {
    forEach(op: () => void ): void;
    toArray(): any[];
}

export interface INode {
    id: number;
}

interface INodeHash {
    [id: number]: INode;
}

interface ResultMap {
    [id: number]: DistanceResult;
}

var id = 0;
export class Node implements INode {
    private _id = id++;
    
    public get id(): number {
        return this._id;
    }
}

export class NodeList implements IList {
    private _nodes = <INodeHash>{};
    private _length = 0;

    public getNode(id: string): INode;
    public getNode(id: number): INode;

    public getNode(id: any): INode {
        if (!this._nodes[id]) return null;
        else return this._nodes[id];
    }

    public addNode(node: INode): void {
        this._nodes[node.id] = node;
        this._length++;
    }

    public removeNode(node: INode): void {
        this._nodes[node.id] = undefined;
        this._length--;
    }

    public forEach(op: (u: INode, list?: NodeList) => void ) {
        for (var id in this._nodes) {
            op(this._nodes[id], this);
        }
    }

    public toArray(): INode[] {
        var nodeArray: INode[] = [];

        this.forEach((node) => nodeArray.push(node));

        return nodeArray;
    }

    public get length(): number { return this._length; }
}

export class EdgeMap {
    private _edges = {};

    public setEdge(one: INode, two: INode, distance: number) {

        if (!this._edges[one.id]) this._edges[one.id] = {};
        if (!this._edges[two.id]) this._edges[two.id] = {};

        this._edges[one.id][two.id] = distance;
        this._edges[two.id][one.id] = distance;
    }

    public getEdge(one: INode, two: INode): number {
        if (this._edges[one.id] === undefined || this._edges[one.id][two.id] === undefined) return Number.POSITIVE_INFINITY;
        else return this._edges[one.id][two.id];
    }

    public forEach(op: (u: INode, v: INode, distance: number) => void ) {
        var _this = this;

        _this.nodes.forEach(function (node1) {
            for (var n2id in _this._edges[node1.id]) {
                var node2 = _this.nodes.getNode(n2id);
                op(node1, node2, _this._edges[node1.id][node2.id]);
            }
        });
    }

    constructor(public nodes: NodeList) {

    }
}

export class DistanceResult {

    public toString() {
        return "{via:" + ((this.via) ? this.via.id.toString() : '-') + ",distance:" + this.distance + "}";
    }

    constructor(public distance: number, public via: INode) {

    }
}

export class DistanceResultList {
    private _distances = <ResultMap>{};

    public forEach(op: (distance: number, id: number, iteration: number) => void ) {
        var i = 0;
        for (var key in this._distances) {
            op(this._distances[key], Number(key), i++);
        }
    }

    public getDistanceFrom(source: INode): DistanceResult {
        if (!this._distances[source.id]) return this._distances[source.id] = new DistanceResult(Number.POSITIVE_INFINITY, null);
        return this._distances[source.id];
    }

    public copy() {
        var c = new DistanceResultList(this.destination);
        var distances = <ResultMap>{};
        for (var key in this._distances) {
            distances[key] = this._distances[key];
        }
        c._distances = distances;
        return c;
    }

    public toString() {
        var out = "{";
        this.forEach(function (distance, id, i) {
            if (i > 0) out += ',';
            out += id.toString() + ":" + distance.toString();
        });
        out += "}";
        return out;
    }

    constructor(public destination: INode) {
        this._distances[destination.id] = new DistanceResult(0, null);
    }
}

export class Graph {

    public addNode(node: INode): void {
        this.nodes.addNode(node);
    }

    constructor(public nodes: NodeList, public edges: EdgeMap) {
    }

    private updatePaths(paths: DistanceResultList) {
        this.edges.forEach(function (u, v, w) {
            var uPath = paths.getDistanceFrom(u);
            var vPath = paths.getDistanceFrom(v);

            if (uPath.distance + w < vPath.distance) {
                vPath.distance = uPath.distance + w;
                vPath.via = u;
            }
        });

        return paths;
    }

    private negativeCycleExists(paths: DistanceResultList) {
        var failure = false;

        this.edges.forEach((u, v, w) =>
        {
            if (paths.getDistanceFrom(u).distance + w < paths.getDistanceFrom(v).distance)
                failure = true;
        });

        return failure;
    }

    public getShortestPathsAsync(destination: INode): Qpromise {
        var _this = this,
            d = q.defer();

        var shortestPaths = new DistanceResultList(destination);

        var request = q(shortestPaths);
        for (var i = 0; i < this.nodes.length - 1; i++) {
            request = request.then(function (paths: DistanceResultList) {
                d.notify(paths.copy());
                return _this.updatePaths(paths);
            });
        }

        request.then(function (paths) {
            d.notify(paths.copy());
            if (_this.negativeCycleExists(paths)) {
                d.reject(new Error("negative cycle"));
            } else {
                d.resolve(paths.copy());
            }
        });

        return d.promise;
    }

    public getShortestPathsSync(destination: INode): DistanceResultList {
        var shortestPaths = new DistanceResultList(destination);

        for (var i = 0; i < this.nodes.length - 1; i++) {
            this.updatePaths(shortestPaths);
        }

        if (this.negativeCycleExists(shortestPaths)) throw new Error("negative cycle");

        return shortestPaths.copy();
    }
}

Реализацию на других языках можно найти тут и тут

На этом все!

Спасибо за ваше внимание. В следующей части реализуем такие алгоритмы как Knapsack Problem и Алгоритм поиска A*.

Александр Куманцев

3 года назад

3

+7 800 100 22 47

бесплатно по РФ

+7 495 085 21 62

бесплатно по Москве

108813 г. Москва, вн.тер.г. поселение Московский,
г. Московский, ул. Солнечная, д. 3А, стр. 1, помещ. 20Б/3
ОГРН 1217300010476
ИНН 7325174845