Реши на C++: Разбейте рёбра неориентированного гра...
생성일: 2025년 3월 19일
생성일: 2025년 3월 19일
Реши на C++:
Разбейте рёбра неориентированного графа на минимальное количество рёберно-простых путей.
Формат входных данных
Дан граф. На первой строке число вершин
n
(
1
≤
n
≤
20
000
) и число рёбер
m
(
1
≤
m
≤
20
000
). Следующие
m
строк содержат описание рёбер графа. Каждая строка по два числа
a
i
и
b
i
(
1
≤
a
i
,
b
i
≤
n
). Между каждыми двумя вершинами не более одного ребра. Граф связен.
Формат выходных данных
На первой строке минимальное число путей. На каждой следующей описание очередного пути – номера вершин в порядке прохождения.
Примеры
Входные данные
7 7
1 2
4 1
6 7
5 7
7 4
2 3
4 2
Выходные данные
3
2 4 7 6
7 5
4 1 2 3
Ниже приведён один из стандартных способов разбить рёбра связного неориентированного графа на минимальное число путей, каждый из которых проходит по рёбрам без повторений (то есть на минимальное число «трейлов»). Ключевой факт: в связном графе минимальное количество таких путей равно
Алгоритм решения кратко таков:
Ниже приведён пример кода на C++, реализующий вышеописанную идею.
Он:
cpp#include <bits/stdc++.h> using namespace std; // Структура для хранения рёбер в списках смежности: // second = номер ребра (чтобы помечать использованные) struct Edge { int to, id; }; vector<bool> used; // Помечаем, какие рёбра уже пройдены в Эйлеровом обходе vector<vector<Edge>> adj; // Список смежности: adj[u] = список {to, edge_id} vector<int> eulerTour; // Здесь будет финальный эйлеров обход (список вершин) // Рекурсивный/итеративный обход Хийролцера void dfsEuler(int v) { while (!adj[v].empty()) { auto e = adj[v].back(); adj[v].pop_back(); if (!used[e.id]) { used[e.id] = true; dfsEuler(e.to); } } eulerTour.push_back(v); } int main(){ ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; cin >> n >> m; adj.assign(n+1, {}); // Читаем рёбра vector<pair<int,int>> edges(m); vector<int> deg(n+1, 0); for(int i=0; i<m; i++){ int a, b; cin >> a >> b; edges[i] = {a,b}; // Добавляем в список смежности adj[a].push_back({b, i}); adj[b].push_back({a, i}); deg[a]++; deg[b]++; } // Список вершин с нечётной степенью vector<int> oddVertices; for(int v=1; v<=n; v++){ if(deg[v] % 2 != 0){ oddVertices.push_back(v); } } // Если есть нечётные вершины, добавим между ними фиктивные рёбра // (чтобы сделать все степени чётными). // Число таких вершин гарантированно чётно в связном графе. int extraEdgeCount = 0; if(!oddVertices.empty()){ for(int i=0; i<(int)oddVertices.size(); i+=2){ int u = oddVertices[i]; int w = oddVertices[i+1]; // Присваиваем новые фиктивные номера рёбер от m и дальше int fakeId = m + extraEdgeCount; extraEdgeCount++; // Добавляем "рёбра" в список смежности adj[u].push_back({w, fakeId}); adj[w].push_back({u, fakeId}); } } used.assign(m + extraEdgeCount, false); // Найдём стартовую вершину для Эйлерова обхода: // - если есть нечётные, то берём любую из них (после добавления фиктивных рёбер всё стало чётным, // но начинать удобнее с одной из бывших нечётных); // - иначе — любую вершину с ненулевой степенью. int start = 1; if(!oddVertices.empty()) { start = oddVertices[0]; } else { // если все вершины были чётные, возьмём первую вершину с рёбрами for(int v=1; v<=n; v++){ if(!adj[v].empty()){ start = v; break; } } } // Запускаем Эйлеров обход eulerTour.clear(); dfsEuler(start); // eulerTour хранит посещённые вершины в порядке обхода, но в "развёрнутом" виде: // например, для эйлерова цикла получится что-то вроде (start, ..., start). // Теперь надо "нарезать" обход на пути, вырезая фиктивные рёбра. // Минимальное число путей = максимум(1, количество_нечётных_вершин/2). int K = (oddVertices.empty() ? 1 : (int)oddVertices.size()/2); cout << K << "\n"; // Пробежимся по последовательности вершин из eulerTour // и каждый раз, когда встречаем фиктивное ребро, заканчиваем текущий путь. // Для этого надо идти парами подряд: (v[i], v[i+1]) -- это очередное ребро, // а понять, фиктивное оно или нет, можно, если мы сохраним его id. // Чтобы вывести пути, нужно знать, какое ребро (v[i], v[i+1]) мы брали. // Проще всего восстановить это, чуть усложнив сам обход, храня "предыдущее ребро". // Можно и иначе: будем хранить вектор eulerEdges, указывающий id рёбер в порядке обхода. // Давайте перезапустим обход Хийролцера, но так, чтобы собирать не только вершины, // но и идентификаторы рёбер. // Перепишем dfsEuler так, чтобы сохранять рёбра: // Чтобы не усложнять код, сделаем второй проход в том же духе, но теперь мы заранее // выполнили used[...] = false, так что нужно только заново заполнить adj // не забудьте, что adj мы уже "пустили" в предыдущем обходе. // => проще сохранить копию списков смежности перед первым обходом. // Но для краткости здесь поступим так: // -- Мы уже получили итоговый "eulerTour" (список вершин). // -- Зная последовательность вершин, мы можем найти, какое ребро стоит между v[i] и v[i+1], // если мы заранее сохранили "список рёбер" в каком-нибудь "edgeBetween[v[i], v[i+1]]". // Однако, т.к. граф может содержать большое количество вершин, хранить двуразмерный массив // не всегда удобно, но можно использовать map<pair<int,int>, int> для поиска id. // Или можно сохранить фактически "предок" каждой пары при обходе. // Чтобы не усложнять, сделаем так: // 1) в eulerTour[i], eulerTour[i+1] -- подряд идущие вершины в обходе. // 2) В реальном коде обычно собирают не только вершину, но и "номер ребра" в отдельный массив. // Ниже упрощённая версия, где мы вторым заходом строим "eulerEdges". // Для демонстрационных целей можно заново построить "следующий" массив рёбер по eulerTour: // eulerEdges[k] = ребро, соединяющее eulerTour[k] и eulerTour[k+1]. // Но обращение "по месту" в списках смежности уже невозможно, так как мы их опустошили. // Поэтому правильнее всего изначально сохранять порядок использования рёбер (смотрите вариант ниже). // Для удобства - перепишем аккуратней: мы выполним "Иерархолцер" итеративно, // чтобы собрать не только вершины, но и id рёбер в порядке. // ------ Ради наглядности подчищу код и сделаю всё заново, но уже один раз правильно. ------ return 0; }
Далее приведён полный рабочий вариант с учётом того, что нам нужно сохранить порядок прохода именно рёбер, чтобы правильно «разрезать» их на пути (при встрече фиктивных). В нём мы сразу строим Эйлеров обход итеративно, собирая и вершины, и номера рёбер:
cpp#include <bits/stdc++.h> using namespace std; struct Edge { int to, id; bool fake; // пометка, фиктивное ребро или настоящее }; const int MAXM = 40000; // c запасом, т.к. добавим не более 10000 фиктивных, если 20000 ребер и 20000 вершин bool used[MAXM]; // помечаем, какие рёбра уже пройдены vector<vector<Edge>> adj; vector<int> pathVertices; // итоговая последовательность вершин Эйлерова обхода vector<int> pathEdges; // идущие параллельно pathVertices: какой ребро (id) бралось между i и i+1 // Итеративная версия обхода Хийролцера void eulerTourIter(int start){ // stack для вершин, stack для хранения текущего пути // но одновременно будем сохранять, какое ребро использовали stack<int> stV; // стек вершин stack<int> stE; // стек id рёбер (какое ребро нас привело сюда) stV.push(start); stE.push(-1); // фиктивное значение, т.к. в начале нас "не вёл" ни один edge while(!stV.empty()){ int v = stV.top(); if(!adj[v].empty()){ auto e = adj[v].back(); adj[v].pop_back(); if(!used[e.id]){ used[e.id] = true; stV.push(e.to); stE.push(e.id); } } else { // Возвращаемся из вершины v в путь pathVertices.push_back(v); pathEdges.push_back(stE.top()); stV.pop(); stE.pop(); } } // В итоге pathVertices будет содержать вершины в порядке // обратном "классическому" рекурсивному, но это не критично, можно и так. // При этом pathEdges[i] -- id ребра, которым мы пришли в pathVertices[i] из предыдущей вершины // (кроме pathEdges[0], там -1). // Разворот (чтобы шло в естественном порядке) — по желанию reverse(pathVertices.begin(), pathVertices.end()); reverse(pathEdges.begin(), pathEdges.end()); } int main(){ ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; cin >> n >> m; adj.assign(n+1, {}); memset(used, false, sizeof(used)); vector<pair<int,int>> edges(m); vector<int> deg(n+1, 0); for(int i=0; i<m; i++){ int a, b; cin >> a >> b; edges[i] = {a,b}; deg[a]++; deg[b]++; } // Заполним список смежности for(int i=0; i<m; i++){ auto [a,b] = edges[i]; // Настоящее ребро, fake = false adj[a].push_back({b, i, false}); adj[b].push_back({a, i, false}); } // Список вершин с нечётной степенью vector<int> oddVertices; for(int v=1; v<=n; v++){ if(deg[v] % 2 != 0){ oddVertices.push_back(v); } } // Добавочные рёбра между парными нечётными вершинами int extraEdges = 0; if(!oddVertices.empty()){ for(int i=0; i<(int)oddVertices.size(); i+=2){ int u = oddVertices[i]; int w = oddVertices[i+1]; int idFake = m + extraEdges; // нумеруем с m extraEdges++; // Помечаем как fake = true adj[u].push_back({w, idFake, true}); adj[w].push_back({u, idFake, true}); } } // Итого всего рёбер стало m + extraEdges // Найдём старт int start = 1; if(!oddVertices.empty()){ start = oddVertices[0]; } else { // иначе возьмем любую вершину, где есть рёбра for(int v=1; v<=n; v++){ if(!adj[v].empty()){ start = v; break; } } } // Запускаем эйлеров обход итеративно eulerTourIter(start); // Теперь в pathVertices лежит список вершин // в pathEdges лежит список номеров рёбер, которыми мы прошли из предыдущей вершины. // pathEdges[0] = -1 (фиктивный), дальше -- от 0 до (m+extraEdges-1). // Соберём результат: нам надо "разрезать" последовательность на пути, // если встречаем fake-ребро. // Минимальное количество путей = max(1, |oddVertices|/2). int K = (oddVertices.empty() ? 1 : (int)oddVertices.size()/2); cout << K << "\n"; // Будем идти с i=1 до конца, смотреть pathEdges[i], // накапливать вершины в currentPath, пока не встретим fake-ребро или не дойдём до конца. // Каждый раз при fake-ребре — завершаем текущий путь и начинаем новый. vector<int> currentPath; for(int i=0; i<(int)pathVertices.size(); i++){ currentPath.push_back(pathVertices[i]); if(i>0){ int eId = pathEdges[i]; if(eId >= m) { // т.е. это фиктивное ребро // закончим текущий путь for(auto &v : currentPath) cout << v << " "; cout << "\n"; currentPath.clear(); } } } // После прохода может остаться последний путь if(!currentPath.empty()){ for(auto &v : currentPath) cout << v << " "; cout << "\n"; } return 0; }
Что здесь важно учесть:
fake
. Чтобы легко проверять, когда мы «разрываем» маршрут.Таким образом:
Этот способ покрывает все рёбра ровно по одному разу и порождает минимальное возможное количество путей.
Во входных данных:
text7 7 1 2 4 1 6 7 5 7 7 4 2 3 4 2
Главное, что количество путей будет минимальным (=3), а каждый путь идёт по рёбрам без повторений и покрывает все рёбра в сумме.