Valmistautuminen ohjelmointihaastatteluihin · Oppitunti

Ahneus vai DP: milloin kumpaakin käytetään

Tunnistakaa ahneilla menetelmillä ratkaistavien ongelmien tunnusmerkit ja ne ongelmat, jotka edellyttävät DP:tä, hyödyntämällä ahneusvalinnan ominaisuutta ja vaihtoargumenttia.

Oppitunti 1/413 vaihetta

Ahneus vai DP: milloin kumpaakin käytetään on ilmainen Valmistautuminen ohjelmointihaastatteluihin-oppitunti CoddyKitissä. Tämä on oppitunti 1/4. Voit lukea koko oppitunnin alta ilmaiseksi ja harjoitella sen jälkeen käytännössä selaimessa sisäänrakennetulla koodieditorilla ja ympäri vuorokauden käytettävissä olevan tekoälytuutorin avulla. Oppitunti kuuluu Valmistautuminen ohjelmointihaastatteluihin-oppimispolkuun, ja edistymisesi synkronoituu verkon ja CoddyKit-sovelluksen välillä. Valmistautuminen ohjelmointihaastatteluihin-kurssilla on yhteensä 4 oppituntia.

Ahneiden menetelmien ja DP:n yleiskatsaus

Sekä ahneet menetelmät että dynaaminen ohjelmointi ratkaisevat optimointiongelmia — niillä etsitään enimmäis- tai vähimmäisarvoa tai optimaalista järjestystä. Ahne menetelmä tekee jokaisessa vaiheessa paikallisesti optimaalisen valinnan tarkastelematta aiempia päätöksiä uudelleen. DP käy läpi kaikki mahdollisuudet, mutta käyttää memoisaatiota välttääkseen saman laskennan toistamisen. Kun tiedätte, kumpaa menetelmää kannattaa käyttää, voitte säästää tuntikausia virheellisen ahneen ratkaisun tai tarpeettoman monimutkaisen DP-taulukon virheenkorjaukselta.

# Greedy: always take the locally best option
# Example: coin change with coins [1, 5, 10, 25]
# Greedy: take as many 25s as possible, then 10s, etc.
# This works for standard denominations but NOT all coin sets!

# DP: explore all possibilities via memoisation
# Example: coin change with coins [1, 3, 4] and target 6
# Greedy would pick 4, then 1, 1 → 3 coins
# DP finds: 3 + 3 → 2 coins (optimal!)
print('Greedy can fail when local optimum != global optimum')

Ahneen valinnan ominaisuus

Ongelma täyttää ahneen valinnan ominaisuuden, kun maailmanlaajuisesti optimaalinen ratkaisu voidaan aina rakentaa tekemällä paikallisesti optimaalisia (ahneita) valintoja. Muodollisesti on olemassa optimaalinen ratkaisu, joka alkaa ahneella valinnalla, joten takaisinpaluuta ei tarvita. Tämän todistamiseen käytetään yleensä vaihtoargumenttia: oletetaan, ettei jokin optimaalinen ratkaisu sisällä ahnetta valintaa, ja osoitetaan sitten, että valinta voidaan vaihtaa sen tilalle heikentämättä ratkaisua.

# Exchange argument example: Activity Selection
# Greedy: always pick the activity that ends earliest
# Proof: suppose optimal solution starts with activity A (not earliest-ending)
# Let G be the earliest-ending activity.
# Replace A with G in the solution:
# - G ends no later than A, so G does not conflict with any activity A allowed
# - The solution remains valid with at least as many activities
# Therefore greedy choice (earliest end) is always safe.

activities = [(1,4), (3,5), (0,6), (5,7), (3,9), (5,9), (6,10), (8,11), (8,12), (2,14)]
activities.sort(key=lambda x: x[1])  # sort by end time
print('Sorted by end:', activities[:4], '...')

Optimaalinen alirakenne

Sekä ahneet menetelmät että DP edellyttävät optimaalista alirakennetta: koko ongelman optimaalinen ratkaisu sisältää osaongelmien optimaaliset ratkaisut. Ero on siinä, voidaanko osaongelmien optimaaliset ratkaisut määrittää ahneesti (tutkimatta kaikkia vaihtoehtoja) vai onko useita valintoja verrattava keskenään. Jos teette valinnan ja jäljelle jäävä osaongelma on rakenteeltaan sama, ahne menetelmä toimii. Jos useita valintoja on verrattava, käyttäkää DP:tä.

# Greedy works: activity selection
# Making the greedy choice (earliest-ending) leaves a sub-problem
# that is structurally identical (activity selection on remaining activities)
# and the greedy choice for the sub-problem is still valid.

# DP needed: 0/1 knapsack
# After choosing to include/exclude item i, the remaining sub-problem
# depends on WHICH item we chose — different choices yield different sub-problems.
# No single greedy rule works for all inputs.

print('Greedy: sub-problem is unique after each choice')
print('DP: sub-problem depends on which choice was made')

Päällekkäiset osaongelmat ovat merkki DP:n tarpeesta

Jos sama osaongelma ratkaistaan rekursiivisessa hajotelmassa useita kertoja, tarvitaan memoisaatiota käyttävää DP:tä. Piirtäkää rekursiopuu ja etsikää toistuvia solmuja. Fibonaccin tapauksessa fib(3) lasketaan puussa kahdesti, kun lasketaan arvoa fib(5). Kun kolikot ovat [1,3,4] ja tavoitesumma 6, summien 3, 2 ja 1 osaongelmat esiintyvät useita kertoja. Päällekkäiset osaongelmat ja optimaalinen alirakenne yhdessä tarkoittavat DP:tä.

# Recursion tree for coin change [1,3,4], target=6
# bt(6) → bt(5) → bt(4) → bt(3) (repeated!)
#              → bt(2) → bt(1) (repeated!)
#         → bt(3) (repeated!)
#       → bt(2) (repeated!)

# Without memoisation: exponential time
# With DP table: O(target * len(coins)) time

def coin_change_dp(coins, amount):
    dp = [float('inf')] * (amount + 1)
    dp[0] = 0
    for a in range(1, amount + 1):
        for c in coins:
            if c <= a:
                dp[a] = min(dp[a], dp[a - c] + 1)
    return dp[amount] if dp[amount] != float('inf') else -1

print(coin_change_dp([1, 3, 4], 6))  # 2 (3+3)
print(coin_change_dp([2], 3))        # -1 (impossible)

Klassiset ahneet ongelmat

Ongelmat, joissa ahneen menetelmän oikeellisuus voidaan todistaa: (1) toimintojen/aikavälien aikataulutus — valitaan aikaisimmin päättyvä toiminto. (2) pienin virittävä puu — Primin ja Kruskaliin algoritmit. (3) Huffman-koodaus — yhdistetään aina kaksi pienimmän esiintymistiheyden solmua. (4) murtolukureppu — valitaan kohteet suurimman arvo/paino-suhteen mukaan. (5) Jump Game — seurataan suurinta saavutettavaa indeksiä. Kaikille näille on olemassa vaihtoargumenttiin perustuva oikeellisuustodistus.

# Fractional Knapsack: greedy works
def fractional_knapsack(items, capacity):
    # Sort by value/weight ratio descending
    items.sort(key=lambda x: x[1]/x[0], reverse=True)
    total = 0
    for weight, value in items:
        if capacity <= 0: break
        take = min(weight, capacity)
        total += take * (value / weight)
        capacity -= take
    return total

items = [(10, 60), (20, 100), (30, 120)]  # (weight, value)
print(fractional_knapsack(items, 50))  # 240.0

# 0/1 Knapsack: greedy FAILS
# Must use DP (can't take fractions)

Milloin ahne menetelmä epäonnistuu: vastaesimerkit

Vastaesimerkin löytäminen on nopein tapa kumota ahne oletus. Kun kolikot ovat [1, 3, 4] ja tavoitesumma 6, ahne menetelmä (suurimmasta alkaen) valitsee 4:n ja sitten 1+1:n, eli tarvitsee 3 kolikkoa. DP löytää ratkaisun 3+3, joka käyttää 2 kolikkoa. 0/1-repussa suhteen perusteella valitseva ahne menetelmä voi valita parhaan suhteen omaavan kohteen mutta jättää huomiotta yhdistelmät, jotka täyttäisivät kapasiteetin paremmin. Jos pystytte muodostamaan vastaesimerkin alle minuutissa, vaihtakaa DP:hen.

# Counterexample: coin change with non-standard coins
def greedy_coins(coins, amount):
    coins.sort(reverse=True)
    count = 0
    for c in coins:
        while amount >= c:
            amount -= c
            count += 1
    return count if amount == 0 else -1

def dp_coins(coins, amount):
    dp = [float('inf')] * (amount + 1)
    dp[0] = 0
    for a in range(1, amount + 1):
        for c in coins:
            if c <= a: dp[a] = min(dp[a], dp[a-c] + 1)
    return dp[amount] if dp[amount] < float('inf') else -1

coins, target = [1, 3, 4], 6
print('Greedy:', greedy_coins(coins[:], target))  # 3 (4+1+1)
print('DP:    ', dp_coins(coins, target))          # 2 (3+3)

Vertailutaulukko: ahne menetelmä ja DP

Keskeiset erot rinnakkain: aikakompleksisuus — ahneen menetelmän aikavaativuus on yleensä O(n log n) (lajittelu määrää vaativuuden), kun taas DP:n on O(n × tilojen määrä). Tilakompleksisuus — ahne menetelmä käyttää O(1) aputilaa, DP O(tilojen määrä). Oikeellisuus — ahne menetelmä vaatii todistuksen, kun taas DP on aina oikea, jos tilat ja rekurrenssi ovat oikein määritettyjä. Soveltuvuus — ahne menetelmä sopii aikataulutukseen, virittäviin puihin ja Huffman-koodaukseen; DP sopii reppuongelmaan, sekvenssien kohdistamiseen ja negatiivisten painojen lyhimpiin polkuihin.

# Performance comparison
import time

def time_it(func, *args):
    start = time.time()
    result = func(*args)
    return result, time.time() - start

# Large coin change test
coins = [1, 5, 10, 25, 100]
amount = 10000

def dp_coins(coins, amount):
    dp = [float('inf')] * (amount + 1)
    dp[0] = 0
    for a in range(1, amount + 1):
        for c in coins:
            if c <= a: dp[a] = min(dp[a], dp[a-c]+1)
    return dp[amount]

result, elapsed = time_it(dp_coins, coins, amount)
print(f'DP coin change(amount={amount}): {result} coins in {elapsed:.4f}s')

Päätöksentekokehys

Haastattelutehtävän päätöspuu: (1) Voitteko todistaa ahneen valinnan ominaisuuden vaihtoargumentin avulla? Jos kyllä → ahne menetelmä. (2) Menevätkö osaongelmat päällekkäin (saavutetaanko sama tila useilla tavoilla)? Jos kyllä → DP. (3) Pyydetäänkö tehtävässä laskemaan tai luettelemaan kaikki ratkaisut? → DP tai takaisinhaku. (4) Pyydetäänkö tehtävässä yhtä optimaalista arvoa luonnollisesti järjestetyille kohteille? Harkitkaa ahnetta menetelmää. (5) Jos ette ole varmoja, toteuttakaa DP — se on aina oikea, jos rekurrenssi on oikein määritetty, vaikka se olisi hitaampi.

# Decision questions to ask:
questions = [
    '1. Is there a natural ordering (by time, ratio, size)?',
    '2. Does making the greedy choice leave a smaller same-type problem?',
    '3. Can I construct a counterexample quickly?',
    '4. Are sub-problems reused across different choice sequences?',
    '5. Does the problem involve counting or listing (not just optimising)?',
]
for q in questions:
    print(q)

print()
print('Greedy signals: scheduling, spanning tree, Huffman, jump game')
print('DP signals: knapsack, edit distance, LCS, coin change (general)')

Aikaväliongelmat: ahne menetelmä vai DP

Aikaväliongelmat jakautuvat ahneisiin ja DP-ratkaisuihin. Päällekkäiset aikavälit (poistetaan mahdollisimman vähän): aikavälit lajitellaan päättymisajan mukaan ja valitaan ahneesti — menetelmä on todistetusti optimaalinen. Painotettu aikavälien aikataulutus (kokonaispainon maksimointi): tarvitaan DP:tä, koska painavat aikavälit voivat olla päällekkäisiä useiden kevyiden aikavälien kanssa, mikä edellyttää kaikkien kelvollisten osajoukkojen vertailua. Ratkaiseva tekijä on, ovatko kaikki aikavälit samanpainoisia (ahne menetelmä) vai onko niiden paino vaihteleva (DP).

# Non-overlapping intervals: greedy works
def erase_overlap_intervals(intervals):
    if not intervals: return 0
    intervals.sort(key=lambda x: x[1])
    count = 0
    last_end = float('-inf')
    for start, end in intervals:
        if start >= last_end:
            last_end = end  # keep this interval
        else:
            count += 1  # remove this interval
    return count

print(erase_overlap_intervals([[1,2],[2,3],[3,4],[1,3]]))  # 1
print(erase_overlap_intervals([[1,2],[1,2],[1,2]]))        # 2

Ongelman vihjeiden tunnistaminen

Yleisiä tehtävänannon vihjeitä: 'toimintojen vähimmäismäärä', 'suurin voitto', 'optimaalinen valinta' → kyseessä voi olla ahne menetelmä tai DP, joten tarkistakaa päällekkäisyydet. 'laske tapojen määrä' → aina DP. 'etsi mikä tahansa kelvollinen aikataulu' → ahne menetelmä voi sopia. 'kaikki mahdolliset' → takaisinhaku. 'vierekkäisiä ei voi ottaa' → DP (House Robber). 'kokoukset, aikavälit, tehtävät' → todennäköisesti ahne menetelmä. Vihjeiden yhdistäminen algoritmiperheisiin nopeuttaa haastattelutehtävän diagnosointia.

# Signal-to-algorithm mapping
signals = {
    'minimum steps/coins/operations': 'DP (unless trivially greedy)',
    'maximum profit/value with constraint': 'DP (knapsack family)',
    'count ways to reach/achieve': 'DP (always)',
    'all combinations/permutations': 'Backtracking',
    'schedule tasks within time': 'Greedy (sort by deadline/end)',
    'cannot pick adjacent': 'DP (house robber pattern)',
    'free to pick any subset': 'DP or Greedy (check overlap)',
    'interval merging/selecting': 'Greedy (sort by end time)',
}
for signal, algo in signals.items():
    print(f'{signal!r}: → {algo}')

Ahneen menetelmän oikeellisuuden todistaminen

Ahneen algoritmin oikeellisuus todistetaan käyttämällä vaihtoargumenttia: (1) Oletetaan, että on olemassa optimaalinen ratkaisu OPT, joka poikkeaa ahneesta ratkaisusta G ensimmäisen valinnan kohdalla. (2) Osoitetaan, että ahne valinta voidaan vaihtaa OPT-ratkaisuun kasvattamatta tavoitefunktion arvoa. (3) Induktiolla päätellään, että ahne ratkaisu on vähintään yhtä hyvä kuin mikä tahansa optimaalinen ratkaisu. Haastatteluissa ei tarvita täydellistä todistusta, mutta vaihtoargumentin idean selittäminen osoittaa syvällistä ymmärrystä.

# Exchange argument demo: earliest-finish-time activity selection
# Suppose OPT starts with activity A (not earliest-ending)
# Let G = earliest-ending activity available
# A.end >= G.end (G ends earlier or same time)

# Swap A for G in OPT:
# - G.end <= A.end, so G does not conflict with anything A allowed after it
# - OPT remains valid with the same number of activities
# - Repeat: after swap, OPT begins with G, matching greedy first choice
# By induction, OPT can be transformed to match G activity by activity
# without losing activities → greedy is optimal

print('Exchange argument: any OPT can be modified to match Greedy without loss')
print('This proves Greedy >= OPT in objective value')

Pikatarkistus

Testatkaa ymmärrystänne Data Structures & Algorithms — Coding Interview Prep -tuotteen tämän oppitunnin käsitteistä.

Oppitunnin yhteenveto

Tässä oppitunnissa opitte, että ahne menetelmä on oikea, kun ahneen valinnan ominaisuus pätee ja voidaan todistaa vaihtoargumentilla, DP:tä tarvitaan, kun osaongelmat menevät päällekkäin (sama osaongelma saavutetaan useilla tavoilla) eikä niitä voida ratkaista yhdellä ahneella säännöllä ja nopein tapa kumota ahne oletus on muodostaa vastaesimerkki epätyypillisillä syötteillä. Seuraavaksi ratkaisemme aikavälien aikataulutus- ja yhdistämistehtäviä ahneella päättymisajan mukaan lajittelevalla menetelmällä.

Aloita maksutta

Opi Valmistautuminen ohjelmointihaastatteluihin tekoälytuutorin avulla — ilmaiseksi

Kirjoita ja suorita oikeaa koodia selaimessa, saa välitöntä apua tekoälytuutorilta ympäri vuorokauden ja jatka siitä, mihin jäit, verkossa tai sovelluksessa.

Kurssit
90
Oppitunnit
360

Usein kysytyt kysymykset

Onko oppitunti ”Ahneus vai DP: milloin kumpaakin käytetään” ilmainen?

Kyllä – oppitunnin ”Ahneus vai DP: milloin kumpaakin käytetään” koko tekstin voi lukea täällä verkossa ilmaiseksi. Jos haluat harjoitella interaktiivisesti sisäänrakennetulla koodieditorilla ja ympäri vuorokauden käytettävissä olevan tekoälytuutorin avulla sekä avata koko Valmistautuminen ohjelmointihaastatteluihin-kurssin, päivitä CoddyKit PROhon. Valmistautuminen ohjelmointihaastatteluihin-kurssilla on yhteensä 4 oppituntia.

Mitä opin oppitunnilla ”Ahneus vai DP: milloin kumpaakin käytetään”?

Tunnistakaa ahneilla menetelmillä ratkaistavien ongelmien tunnusmerkit ja ne ongelmat, jotka edellyttävät DP:tä, hyödyntämällä ahneusvalinnan ominaisuutta ja vaihtoargumenttia. Harjoittelet Valmistautuminen ohjelmointihaastatteluihin-aihetta koodilla, jonka suoritat suoraan selaimessa. Ympäri vuorokauden käytettävissä oleva tekoälytuutori vastaa kysymyksiisi oppitunnin aikana.

Tarvitsenko kokemusta aloittaakseni Valmistautuminen ohjelmointihaastatteluihin-opiskelun?

Aiempi kokemus ei ole tarpeen. CoddyKitin Valmistautuminen ohjelmointihaastatteluihin-oppimispolku sopii vasta-alkajista edistyneisiin, joten voit aloittaa tästä tai alusta ja edetä omaan tahtiisi. Tämä on oppitunti 1/4.

Kuinka kauan ”Ahneus vai DP: milloin kumpaakin käytetään”-oppitunnin suorittaminen kestää?

Useimmat CoddyKitin oppitunnit kestävät noin 5–10 minuuttia. Jokainen oppitunti on lyhyt ja interaktiivinen, joten edistyt tasaisesti ja voit jatkaa siitä, mihin jäit – sekä verkossa että sovelluksessa.

Voinko kirjoittaa ja suorittaa koodia tällä Valmistautuminen ohjelmointihaastatteluihin-oppitunnilla?

Kyllä. Jokainen Valmistautuminen ohjelmointihaastatteluihin-oppitunti sisältää sisäänrakennetun koodieditorin, joten voit kirjoittaa ja suorittaa oikeaa koodia suoraan selaimessa ja saada välitöntä palautetta tekoälyltä – paikallista asennusta ei tarvita.

Kaikki tämän kurssin oppitunnit

  1. Ahneus vai DP: milloin kumpaakin käytetään
  2. Välin ajoitus ja yhdistäminen
  3. Jump Game I ja II
  4. Task Scheduler ja Gas Station
← Takaisin: Valmistautuminen ohjelmointihaastatteluihin