Страницы

Поиск по вопросам

четверг, 13 февраля 2020 г.

Кратчайший путь в графе с дополнительными условиями

#python #алгоритм #графы


Задача:

Автомобилист хочет проехать от города u до города v, по стране с количеством городов
n. Известны длины дорог между всеми городами, дороги двухсторонние. Автомобиль имеет
ограниченный запас топлива k, в начале пути бак полностью заполнен. Восполнять запас
топлива до максимального k он может в некоторых городах по дороге. Нужно посчитать
сколько минимум нужно будет сделать заправок по дороге чтобы попасть из города u в город v.

Формат входных данных

Первая строка содержит пять целых чисел: k — сколько километров может проехать машина
без дозаправок, n — количество городов, m — количество дорог, u — номер города начала
дороги, и v — номер города, куда нужно доехать (1 ≤ k ≤ 500, 2 ≤ n ≤ 10 000, 0 ≤ m
≤ 10 000, 1 ≤ u,v, ≤ n, u ≠ v).

В следующих m строках задаются дороги. В i-й из этих строк записаны три числа p_i,
q_i, r_i —
номера двух городов, которые соединяет очередная двухсторонняя дорога, и её длина.
(1 ≤ p_i, q_i ≤ n, 1≤ r_i ≤ 10^9)

Следующая строка содержит целое число l — количество заправок (0 ≤ l ≤ n). Наконец,
последняя строка содержит l чисел a_1, a_2, . . . , a_i — номера городов с заправками
в порядке возрастания (1 ≤ a_1 < a_2 < ... < a_l ≤ n).

Формат выходных данных
Выведите -1, если невозможно доехать от города с номером u до города с
номером v, или минимальное количество заправок, если это возможно.

Примеры:

input #1:

3 3 3 1 3
1 2 3
1 3 4
2 3 3
2
2 3


output #1:

1


input #2:

3 3 3 1 3
1 2 2
1 3 4
2 3 2
0


output #2:

-1


input #3:

3 3 3 1 3
1 2 2
1 3 4
2 3 1
0


output #3:

0


Ограничение по времени: 2 секунды;

Ограничение по памяти: 512 мегабайт

Моё решение:

k,n,m,u,v = [int(i) for i in input().split()] 

adj = {}
for j in range(m):
    p,q,r = [int(i) for i in input().split()]
    if p-1 in adj:
        adj[p-1].append([q-1,r])
    else:
        adj[p-1] = [[q-1,r]]
    if q-1 in adj:
        adj[q-1].append([p-1,r])
    else:
        adj[q-1] = [[p-1,r]]

l = int(input())

refills = [False] * n
if not l is 0:
    for i in input().split():
        refills[int(i)-1] = True

count = [0] * n
tank = [None] * n
level = [-1] * n

def bfs(s):
    global level,charging,k
    level[s] = 0 
    queue = [s]
    tank[s] = k
    while queue: 
        v = queue.pop(0)

        for w_ in adj[v]: 
            w = w_[0]; r = w_[1];  
            if tank[w] is None:
                tank[w] = k
            if level[w] is -1 and r<=tank[v]:  

                level[w] = r+level[v]
                if refills[w]:
                    tank[w] = k
                    count[w]+=1
                else:
                    tank[w] -=r
                queue.append(w)  
bfs(u-1)
if level[v-1] is -1:
    print(-1)
else:
    print(count[v-1])


Я использую поиск в ширину с дополнительными условиями, алгоритм достаточно прост.
Но моё решение выдаёт ошибки на некоторых тестах (входные данные на которых происходит
ошибка я не знаю). 

Подскажите, пожалуйста, что не так в алгоритме. Либо посоветуйте иной способ решения.
    


Ответы

Ответ 1



Задача на самом деле достаточно простая. Давайте посмотрим на ограничения. 10к вершин. 500км запаса хода максимум (!!). Кстати можно сразу убирать дороги длиннее k, всё равно проехать не сможем. А теперь самый интересный момент - из 1 вершины {i} сделаем k вершин {(i,d)} которые будут показывать количество заправок на маршруте от старта к i, при этом в пункте i осталось d км топлива. Оценим сложность. 10к * 500 = 5кк. По памяти это около 20 мегабайт. По времени. 5кк вершин. Количество ребёр в сумме пусть тоже будет 5кк. Алгоритм комбинированного обхода (или как его правильно) O( (N+M) ) ~ 10kk вполне успевает. Идея алгоритма - идём без заправок пока можем, но складываем отдельно места где можно заправится. Потом эти места обрабатываем в порядке очереди. Ну как бы и всё. Дальше дело техники сделать переход. Из i->j переход возможен только из вершин где топлива достаточно (обычный цикл от длины до k) без увеличения ответа. И с увелечением ответа на 1 в k - длину. Ответ - минимум по всем "размноженным" последним вершинам. Реализацию проще делать через хранение массива [0,k] в каждой обычной вершине. Код пока не пишу. Если возникнут вопросы по реализации - помогу.

Ответ 2



Предлагаю решение, похожее на алгоритм Дейкстры. Не уверен, можно ли в принципе уложиться во временной лимит 2 секунды при использовании Python. Я потестировал это решение на больших рандомных графах - 6000 городов, 9000 дорог, результаты от 0.8 до 4 секунд в зависимости от удачности сложения графа. Я создавал графы для тестирования с помощью модуля networkx, затем переводил их в нужный для задачи формат. Могу добавить сюда код, если нужно. Алгоритм Стартуем из текущего города (в начале это один город - первый, на следующих итерациях перебираем все конечные города предыдущего заезда) и едем до всех возможных городов на расстоянии одного бака. Все эти города можно сразу добавлять в ответ - мы уже знаем количество заправок необходимое, чтобы доехать них. Проезжая очередной город фиксируем его расстояние до ближайшей заправки в словарь: если город имеет заправку, то расстояние до заправки = 0. если не имеет, то пишем расстояние до ближайшего города с заправкой, из уже посещённых. Города, дальше которых машина не смогла проехать из-за нехватки топлива, запоминаем - на следующем витке алгоритма будем их заправлять в ближайшем посещённом городе (просто вычитая расстояние до этого города из стартового запаса хода) и повторять шаги 1, 2, 3. Другими словами: первая заправка - едем во все стороны, добавляя каждый посещённый город в ответ. Топливо кончилось - встали в конечном городе. Пошла вторая заправка. Все вставшие машины заправляем и опять едем до упора. Так мы доберёмся до каждого города на карте. Или недоберёмся - заправки кончились, искомый город не нашли, возвращаем -1. Замечание: все дороги, длина которых больше, чем максимальный запас хода (полный бак) можно исключить в самом начале - на этапе формирования списка смежности. source.py def find_shortest_path(): def make_adj_dict(roads_num, max_pos_dist): adj_dict = {k : {} for k in range(1, roads_num + 1)} while roads_num > 0: city_a, city_b, distance = map(int, input().split()) if distance <= max_pos_dist: adj_dict[city_a][city_b] = distance adj_dict[city_b][city_a] = distance roads_num -= 1 return adj_dict max_possible_distance, cities_num, roads_num, start_city, end_city = map(int, input().split()) adj_dict = make_adj_dict(roads_num, max_possible_distance) refuel_num = int(input()) if refuel_num > 0: cities_with_gs = {int(num) : True for num in input().split()} else: cities_with_gs = {} answers = {} one_tank_reachable_cities = set() one_tank_reachable_cities.add(start_city) should_be_refueled_set = set() dist_to_last_gs = {k : 0 if k in cities_with_gs else float("inf") for k in range(1, cities_num + 1)} fuel_left_dict = {k : 0 for k in range(1, cities_num + 1)} fuel_left_dict[start_city] = max_possible_distance for refuel_cnt in range(refuel_num + 1): if should_be_refueled_set: for city in should_be_refueled_set: fuel_residue = max_possible_distance - dist_to_last_gs[city] if fuel_residue >= 0: fuel_left_dict[city] = fuel_residue one_tank_reachable_cities.add(city) should_be_refueled_set = set() while one_tank_reachable_cities: city_a = one_tank_reachable_cities.pop() answers[city_a] = refuel_cnt for city_b, a_to_b_dist in adj_dict[city_a].items(): dist_to_last_gs[city_b] = min(dist_to_last_gs[city_a] + a_to_b_dist, dist_to_last_gs[city_b]) if city_b in answers: continue fuel_before_ride = fuel_left_dict[city_a] if a_to_b_dist <= fuel_before_ride: one_tank_reachable_cities.add(city_b) fuel_after_ride = fuel_before_ride - a_to_b_dist fuel_left_dict[city_b] = max(fuel_after_ride, fuel_left_dict[city_b]) else: should_be_refueled_set.add(city_a) return answers[end_city] if end_city in answers else -1 Тестирование cnt = 1 while True: try: print(find_shortest_path(), end='') print(f"\t#Case № {cnt}") except EOFError: break cnt += 1 input.txt ### Case № 1, output = 1 3 3 3 1 3 1 2 3 1 3 4 2 3 3 2 2 3 ### Case № 2, output = 0 3 3 3 1 3 1 2 2 1 3 4 2 3 1 0 ### Case № 3, output = -1 3 3 3 1 3 1 2 2 1 3 4 2 3 2 0 ### Case № 4, output = 3 50 7 8 1 7 1 2 50 1 3 50 1 4 50 2 5 50 3 5 60 4 5 40 5 6 40 6 7 30 4 2 3 5 6 ### Case № 5, output = 1 120 7 8 1 7 1 2 50 1 3 50 1 4 50 2 5 50 3 5 60 4 5 40 5 6 40 6 7 30 2 2 3 Строки начинающиеся с ### нужны для наглядности, перед запуском программы их надо убрать. Я их не удаляю из файла input.txt вручную, а пропускаю через sed перед подачей на ввод программы (пользуюсь Ubuntu): ./source.py < <(sed '/#/d' input.txt) Output 1 #Case № 1 0 #Case № 2 -1 #Case № 3 3 #Case № 4 1 #Case № 5

Комментариев нет:

Отправить комментарий