Split Array Largest Sum
Otrzymujesz tablicę nums nieujemnych liczb całkowitych oraz liczbę całkowitą k. Podziel nums na dokładnie k części, z których każda jest niepustym ciągiem sąsiadujących wartości, a części zachowują swoją kolejność. Każda część ma sumę, a koszt podziału to największa z tych sum.
Zwróć najmniejszy koszt, jaki można uzyskać przy dowolnym podziale na k części.
Funkcja
- numsinteger-array
- wartości nieujemne, w kolejności
- kinteger
- liczba spójnych części, na które należy je podzielić
- Zwracainteger
- najmniejsza możliwa wartość sumy największej części
Ograniczenia
1 ≤ nums.length ≤ 50000 ≤ nums[i] ≤ 1051 ≤ k ≤ nums.length- Każda część zawiera co najmniej jedną wartość. Część, której wszystkie wartości wynoszą 0, ma sumę 0, co jest dozwolone.
Przykłady
- Wejście
- nums = [6, 2, 9, 4, 7, 3]k = 3
- Wyjście
- 13
- Wyjaśnienie
- Podział
[6, 2],[9, 4],[7, 3]ma sumy 8, 13 i 10, więc jego koszt wynosi 13. Żaden podział nie ma kosztu 12: pakując elementy od lewej do prawej tak, aby każda suma wynosiła najwyżej 12, otrzymujemy[6, 2],[9],[4, 7],[3]— cztery części, choć dozwolone są tylko trzy.
- Wejście
- nums = [8, 1, 1, 1, 5]k = 2
- Wyjście
- 8
- Wyjaśnienie
- Ósemka znajduje się w pewnej części, więc żaden podział nie może kosztować mniej niż 8.
[8]i[1, 1, 1, 5]dają w sumie 8, więc można osiągnąć 8.
- Wejście
- nums = [3, 0, 4]k = 3
- Wyjście
- 4
- Wyjaśnienie
- Trzy wartości i trzy części pozostawiają jedną wartość na część, a ich sumy wynoszą 3, 0 i 4. Suma środkowej części wynosi 0, co jest w porządku: część musi jedynie zawierać wartość.
+20 ukrytych testów przy wysłaniu
Pytanie dodatkowe
Każde zachłanne sprawdzenie odczytuje wszystkie n wartości. Dzięki sumom prefiksowym sprawdzenie może zamiast tego znaleźć, gdzie kończy się każda część, za pomocą wyszukiwania binarnego. Jak szybko działa cała metoda, gdy k jest małe, a nums jest długie?
Podpowiedzi
Otwieraj je po kolei. Każda zdradza trochę więcej.
Załóżmy, że ktoś obiecuje, że największa część może mieć sumę co najwyżej
c. Czy potrafisz szybko stwierdzić, czykczęści wystarczy?Wypełniaj części od lewej do prawej i zamykaj część tylko wtedy, gdy następna wartość spowodowałaby przekroczenie
c. Dzięki temu używasz najmniejszej liczby części, a większecnigdy nie wymaga ich więcej.Wyszukaj binarnie
cmiędzy największą wartością a sumą całkowitą. Jeśli zachłanny zliczający wynik wynosi co najwyżejk, odpowiedzią jestclub mniejsza wartość; w przeciwnym razie jest większa.
Rozwiązanie
Te dwa wymagania są ze sobą sprzeczne: musisz użyć dokładnie k części, a jednocześnie chcesz, aby największa część była jak najmniejsza. Próba sprawdzenia każdego miejsca dla k-1 cięć prowadzi do eksplozji liczby możliwości, a programowanie dynamiczne po prefiksach zmniejsza ją do O(k·n²), co wciąż jest zbyt wolne dla 5000 wartości. Szybki pomysł polega na odwróceniu pytania. Zamiast szukać najlepszego podziału, zgadnij górny limit i sprawdź, czy k części może się w nim zmieścić. Odpowiada na to jedno zachłanne przejście, odpowiedzi zmieniają się tylko raz wraz ze wzrostem limitu, a wyszukiwanie binarne znajduje tę zmianę w około 29 przejściach.
Programowanie dynamiczne na prefiksach
Poprawne, ale nie kończy się na największych testach
Intuicja
Przyjrzyj się ostatniej części podziału. Jeśli pierwsze j wartości tworzą p części, ostatnia część to pewien ciąg nums[i..j-1], a pierwsze i wartości tworzą pozostałe p-1 części. Koszt jest większą z dwóch liczb: kosztu tych p-1 części oraz sumy ostatniego ciągu. Niezależnie od tego, jaki jest ostatni ciąg, chcesz podzielić pierwsze i wartości możliwie najniższym kosztem, a ten najlepszy podział nie zależy od niczego, co znajduje się po jego prawej stronie. Możesz więc obliczyć go raz i wykorzystać ponownie.
Oznaczmy przez best[p][j] najmniejszy koszt podziału pierwszych j wartości na p części. Przy jednej części nie ma wyboru: best[1][j] to suma pierwszych j wartości. Dla większej liczby części wypróbuj każdy początek i ostatniej części: best[p][j] = min over i of max(best[p-1][i], prefix[j] - prefix[i]), gdzie prefix[j] jest sumą pierwszych j wartości. Początek i mieści się w zakresie od p-1, ponieważ p-1 niepustych części wymaga co najmniej p-1 wartości, do j-1, ponieważ ostatnia część musi zawierać jedną wartość. Odpowiedzią jest best[k][n]. Wiersz p odczytuje tylko wiersz p-1, więc wystarczą dwa wiersze długości n+1.
W pierwszym przykładzie podział [6, 2, 9, 4] na dwie części może zakończyć pierwszą część po 6 (koszt max(6, 15) = 15), po 2 (max(8, 13) = 13) lub po 9 (max(17, 4) = 17), więc best[2][4] = 13. Następnie best[3][6] sprawdza ostatnią część [7, 3] i otrzymuje max(13, 10) = 13, czego nie da się poprawić żadnym innym początkiem.
Problemem jest czas działania. Jest k wierszy, n końców w każdym wierszu i maksymalnie n początków dla każdego końca: maksymalnie k·n²/2 kroków. Przy n = 5000 i k = 2500 wewnętrzna pętla wykonuje około 1.8 × 10^10 razy: 18 sekund nawet przy 10^9 prostych kroków na sekundę. Warto jednak znać programowanie dynamiczne: nigdy nie zakłada, że wartości są nieujemne, więc działa także tam, gdzie szybka metoda zawodzi.
Algorytm
- Zbuduj
prefix, gdzieprefix[j]jest sumą pierwszychjwartości. - Ustaw wiersz dla jednej części:
best[j] = prefix[j]. - Dla każdej liczby części
pod 2 dokoraz każdego końcajodpdonwyznacz minimum dlaiodp-1doj-1z wartościmax(best[i], prefix[j] - prefix[i]). - Zapisz te minima w nowym wierszu i ustaw go jako
best. - Zwróć
best[n].
def splitArray(nums, k):
n = len(nums)
# prefix[j] is the sum of the first j values
prefix = [0] * (n + 1)
for i, x in enumerate(nums):
prefix[i + 1] = prefix[i] + x
# best[j]: the smallest largest part when the first j values form one part
best = prefix[:]
for parts in range(2, k + 1):
nxt = [0] * (n + 1)
for j in range(parts, n + 1):
lowest = prefix[j] # never worse than one part holding everything
for i in range(parts - 1, j):
# the first i values form parts-1 parts, nums[i..j-1] is the last part
worst = max(best[i], prefix[j] - prefix[i])
if worst < lowest:
lowest = worst
nxt[j] = lowest
best = nxt
return best[n]Wyszukiwanie binarne największej sumy
Intuicja
Odwróćmy pytanie. Wybierz limit c i zapytaj: czy da się podzielić nums na k części, tak aby suma każdej z nich była nie większa niż c? Odpowiedzią na zadanie jest najmniejszy limit, dla którego odpowiedź brzmi „tak”. To pytanie jest znacznie łatwiejsze od pierwotnego z dwóch powodów.
Po pierwsze, odpowiada na nie jedno zachłanne przejście. Idź od lewej do prawej i dodawaj wartości do bieżącej części, dopóki jej suma nie przekracza c; gdy następna wartość spowodowałaby przekroczenie c, zamknij część i zacznij nową od tej wartości. W ten sposób uzyskasz najmniejszą liczbę części, jakiej może użyć dowolny podział przy tym limicie. Porównaj ten podział z dowolnym innym poprawnym podziałem, część po części. Obie pierwsze części zaczynają się od pierwszej wartości, a algorytm zachłanny zatrzymuje się dopiero wtedy, gdy kolejna wartość się nie mieści, więc jego pierwsza część kończy się co najmniej tak daleko. Druga część algorytmu zachłannego zaczyna się wtedy w miejscu nie wcześniejszym niż początek drugiej części drugiego podziału. Wartości do końca tamtej części stanowią jej fragment, a ponieważ nie ma wartości ujemnych, suma fragmentu nigdy nie przekracza sumy całości, więc się mieszczą i algorytm zachłanny ponownie sięga co najmniej tak daleko. Algorytm zachłanny nigdy nie zostaje w tyle, więc nigdy nie potrzebuje więcej części.
Po drugie, mniejsza liczba części niż k jest równie dobra jak dokładnie k części. Jeśli algorytm zachłanny potrzebuje m < k części, podziel część zawierającą co najmniej dwie wartości na dwie części. Ich sumy nie przekraczają sumy całości, ponieważ żadna wartość nie jest ujemna, a ponieważ n ≥ k, zawsze można znaleźć taką część, dopóki nie uzyskasz k części. Zatem warunek testu to partsNeeded(c) ≤ k.
Teraz kluczowa właściwość: test jest monotoniczny. Jeśli limit c działa, to c+1 też działa, ponieważ ten sam podział nadal mieści się w większym limicie. Dla limitów od max(nums) do sum(nums) odpowiedzi układają się w ciąg: nie, nie, ..., nie, tak, tak, ..., tak, a szukasz pierwszego „tak”. Zakres jest bezpieczny na obu końcach: żaden limit mniejszy niż max(nums) nie pomieści tej wartości, a suma wszystkich wartości zawsze mieści się w jednej części. Pierwsze „tak” odpowiada też rzeczywistemu kosztowi, a nie tylko granicy: gdyby suma żadnej części w tym podziale nie była równa c, limit c-1 również by wystarczył.
Prześledź pierwszy przykład: [6, 2, 9, 4, 7, 3] dla k = 3. Limity mieszczą się w zakresie od 9 do 31. Przy limicie 20 otrzymujemy części [6, 2, 9], [4, 7, 3]: 2 części, tak, więc zakres zmienia się na 9–20. Przy limicie 14 otrzymujemy [6, 2], [9, 4], [7, 3]: 3 części, tak, zakres 9–14. Przy limicie 11 otrzymujemy [6, 2], [9], [4, 7], [3]: 4 części, nie, zakres 12–14. Przy limicie 13 potrzeba 3 części, tak, zakres 12–13. Przy limicie 12 potrzeba 4 części, nie, więc odpowiedzią jest 13.
Każde przejście odczytuje n wartości, a zakres za każdym razem zmniejsza się o połowę. Przy sumie S wynoszącej maksymalnie 5 × 10^8 daje to około 29 przejść po 5000 wartości, czyli mniej więcej 150000 kroków.
Algorytm
- Ustaw
lo = max(nums)ihi = sum(nums). - Gdy
lo < hi, obliczmid = lo + (hi - lo) / 2. - Policz części, których zachłanny algorytm potrzebuje przy limicie
mid: zacznij od 1 części i sumy bieżącej równej 0; gdy dodanie wartości przekroczyłobymid, dodaj część i rozpocznij sumowanie od tej wartości. - Jeśli liczba części jest nie większa niż
k, ustawhi = mid; w przeciwnym razie ustawlo = mid + 1. - Zwróć
lo.
def splitArray(nums, k):
def parts_needed(cap):
# Fill each part left to right and start a new one only when the next value would pass cap.
parts, current = 1, 0
for x in nums:
if current + x > cap:
parts += 1
current = x
else:
current += x
return parts
lo, hi = max(nums), sum(nums) # one value per part at best, everything in one part at worst
while lo < hi:
mid = (lo + hi) // 2
if parts_needed(mid) <= k:
hi = mid # mid works, so the answer is mid or smaller
else:
lo = mid + 1 # mid needs more than k parts, so the answer is larger
return lo
Pułapki i przypadki brzegowe
Wyszukiwanie jest krótkie, więc błędy tkwią w zachłannym sprawdzaniu i granicach.
- Rozpoczynanie od
lomniejszego niżmax(nums). Zachłanne sprawdzanie umieszcza wartość większą od limitu w osobnej części i kontynuuje, więc uznaje limit 5 za wystarczający dla[1, 9]przyk = 2. Zacznij od największej wartości albo spraw, by sprawdzanie kończyło się niepowodzeniem, gdy pojedyncza wartość przekracza limit. - Sprawdzanie
partsNeeded(c) == k. Zachłanny algorytm często potrzebuje mniej części niżk: dla[3, 0, 4]ik = 3limit 4 pozwala upakować wartości jako[3, 0],[4]. Przy użyciu==żaden limit nie przejdzie sprawdzania. Mniejszą liczbę części zawsze można jeszcze podzielić, więc sprawdzaj warunek≤ k. - Zliczanie części od 0. Pierwsza część istnieje, zanim jakakolwiek wartość ją przepełni, więc zliczanie zaczyna się od 1.
- Ustawianie
hi = mid - 1, gdymiddziała. W ten sposób można pominąć samą odpowiedź. Pozostawhi = midi wykonuj pętlę, dopókilo < hi. - Rozpoczynanie
iw DP od 0. Komórkabest[i]dlai < p-1oznacza mniejszą liczbę wartości niż części, co jest niemożliwe przy podziale, a w wierszu wypełnionym zerami daje koszt 0. Dla[100, 1, 1]ik = 3DP zwraca wtedy 2 zamiast 100. Zacznijiodp-1. - Przepełnienie przy większych limitach. Tutaj suma wynosi co najwyżej
5 × 10^8, więc mieszczą ją liczby całkowite 32-bitowe. Jeśli wartości osiągają10^6, już 2148 takich wartości przekracza2^31-1, więc użyj sum 64-bitowych.
Najczęstsze pytania4
Jaka jest złożoność czasowa problemu Split Array Largest Sum?
Wyszukiwanie binarne działa w czasie O(n log S), gdzie n to długość nums, a S to suma jego elementów. Każde sprawdzenie zachłanne to jedno przejście po tablicy, a zakres limitów zmniejsza się o połowę po każdym sprawdzeniu: około 29 sprawdzeń, gdy S = 5 × 10^8. Wykorzystuje ono dodatkowe miejsce O(1). Programowanie dynamiczne ma złożoność czasową O(k·n²) i pamięciową O(n).
Dlaczego sprawdzanie wykonalności jest monotoniczne?
Jeśli każda część pewnego podziału sumuje się do co najwyżej c, to w tym samym podziale każda część jest też co najwyżej równa c+1. Zatem gdy limit zadziała, zadziała też każdy większy limit, a gdy limit nie zadziała, nie zadziała też żaden mniejszy limit. Odpowiedzi tworzą serię odpowiedzi „nie”, po której następuje seria odpowiedzi „tak”, a właśnie taką granicę ma znaleźć wyszukiwanie binarne.
Dlaczego zachłanne sprawdzanie znajduje najmniejszą liczbę części?
Algorytm zachłanny dodaje kolejne wartości do części, dopóki dodanie następnej nie przekroczyłoby limitu. Porównaj go z dowolnym poprawnym podziałem, część po części. Każda część utworzona przez algorytm zachłanny zaczyna się w tym samym miejscu co część o tym samym numerze w drugim podziale lub później, więc jej wartości aż do końca tej części stanowią fragment części, która mieści się w limicie. Żadna wartość nie jest ujemna, więc ten fragment też się mieści, a algorytm zachłanny obejmuje co najmniej tyle samo elementów. Algorytm zachłanny nigdy nie zostaje w tyle, więc dzieli tablicę na nie więcej części niż jakikolwiek inny podział.
Czy wyszukiwanie binarne działa z liczbami ujemnymi?
Nie. W przypadku wartości ujemnych dodanie wartości może obniżyć sumę, więc algorytm zachłanny może zamknąć część zbyt wcześnie i pominąć podział, który działa. Podział części może również podnieść sumę jednego fragmentu powyżej sumy całości, więc mniej niż k części nie oznacza już, że podział na k części zadziała. Programowanie dynamiczne nie opiera się na żadnym z tych założeń i pozostaje poprawne, działając w czasie O(k·n²).
Podobne zadania
Zadania oparte na tych samych pomysłach. Rozwiązanie dwóch lub trzech utrwala schemat.
Python
def splitArray(nums, k):
# Wpisz kod tutajPrzypadek 1
Przypadek 2
Przypadek 3
Wejście
nums = [6, 2, 9, 4, 7, 3] k = 3
Oczekiwane
13