cards-deck: 100-199_university::111-120_theoretic_cs::111_algorithms_datastructures

Dynamic Programming | Principle

anchored to [[111.00_anchor]] enhances on [[111.05_problem_beschreibungen]]


Overview

Unter dem Begriff der dynamischen Programmierung verstehen wir primär ein Prinzip, was uns dabei helfen kann, größere Probleme auf kleinere aufzuteilen und damit unter Umständen schneller zu sein. Als einfaches Beispiel dafür dient etwa Divide and Conquerm, denn hier werden größere Probleme in kleiner aufgeteilt und dann dadurch unter Umständen einfacher zu lösen ( weil wir uns nur noch kleine Probleme anschauen!)

Weitere Informationen über dieses Prinzip:

  • wird meist / primär angewandt, um Probleme zu optimieren bzw zu vereinfachen!
  • Probleme werden dabei so gelöst, dass man ein großes Problem in viele kleine subprobleme aufteilt, diese löst und anschließend die Lösung aus allen zusammenschließt
  • Each subproblem is solved just once and the answer is getting saved somewhere - table or similiar. _This avoids the work of recomputing_the answer every time we need to solve this subproblem.

Example for that might be the idea of divide and conquer because:

  • each subproblem of it is solved at most once!
  • subsproblems are much smaller than their large top-problems

[!Important] Difference dynamic programming and Divide Conquer the formal definition argues that Divide and conquer can be a a subtype of dynamic programming

Further we can say that instead of always splitting the problem into equal smaller problems, we sometimes just use partial splitting in dynamic programming.

[!Definition] Wie können wir ein größeres Problem schnell lösen? #card Durch das Aufteilen des Problems in viele kleine wird es uns ermöglicht, diese kleinen Teilprobleme u.U. schneller lösen und somit schneller zu einer Lösung zu kommen.

Wir können jedes dieser einzelnen Teilprobleme versuchen in kleinere Teilprobleme aufteilen zu können. Dabei kombinieren wir dann immer die ergebenen Lösungen und spoichern die beste Option dieser. Wir werden dann die gespeicherte (beste Lösung!) verwenden, um weitere Teilprobleme gleicher Art nicht nochmal berechnen zu müssen -> können also die Lösung übernehmen! ^1706623471495


Cutting Stick into pieces to maximize cost

Wir betrachten folgend ein Problem, welches sich damit beschäftigt eine Stange so zu zerschneiden, dass dabei der höchste Erlös generiert wird. Wir wollen also die beste Kombination von Aufteilungen unseres Problemes finden, um den höchsten Umsatz zu erhalten. Problembeschreibung: Betrachte folgend, dass wir eine Stange der Länge zerschneiden möchten. Weiter ist der Preis für ein Stück der Länge mit angegeben. Wie könnte ein Vorgehen von uns sein? #card

  1. Wir könnten die Stange nicht zerschneiden, und somit einen Erlös von erhalten.
  2. Alternativ könnten wir auch Stück von Länge zerschneiden und dann den Erlös entsprechend mit , also der Umsatz wird um eins verringert, während unser Erlös eines Stücks zu nehmen wird. Folgend also: 1 Stück von Länge 1: Erlös 1 Stück von Länge 2: Erlös hierbei dann: Es gilt jetzt das Maximum all dieser Betrachtungen zu finden! ^1706623471504

Subprobleme erstellen | Berechnen des Optimums

Am obigen Beispiel der zu zerschneidenden Stange haben wir gesehen, dass wir im Endeffekt alle möglichen Teillösungen durchgehen und aus dieser dann eine beste finden müssen.

[!Definition] Wie können wir diesen Sachverhalt mathematisch beschreiben? #card Wir suchen das Maxima aus den Teilproblemen, ferner also: Wir berechnen also alle möglichen Optionen, wie Anschließend sollten wir nach dieser Fraktionierung in kleine Probleme jetzt auch einfach berechnen können! Am Beispiel des optimalen zerschneidens einer Stange für maximalen Gewinn, sieht es dann etwa folgend aus:

Schnitt(n):
  if(n = 0) 
  	return 0 # we cant cut it with length of 0
  q = min_infinity
  for (i = 1 to n)
  	q = max{ q, p[i]+Schnitt(n-1)  }
  return q # which will be the maximum we found among all the options provided 

Aus dieser Betrachtung heraus können wir dann erkennen, wie Laufzeit sein wird: und für n dann: # also wir kombinieren die Laufzeit eines jeden Teilproblemes und fügen diese dann zusammen ! Umgeformt folgend dann: also nicht soo gut

Wir möchten einen weiteren Versuch zur Berechnung des selben Problemes betrachten: ^1706623471508

[!Important] Berechnen des optimalen Schnitts mittels Bottom-Up Wir werden ähnlich vorgehen, jedoch diesmal bottom up also von klein nach groß !

Schnitt(n): 
  r[0] = 0 
  for j = 1 to n 
  	q = - infinity 
  	for i 1 to j # also wir bewegen uns immer von 1 bis zur derzeitigen Boundary durch j
  		q = max{ q, p[i] + r[j-i]  } # wir betrachten also das maxima von q und der neuen Berechnung aus dem Erlös bei [i], sowie DIfferenz aus einer vorherigen Lösung, welche wir aus [j-i] herausfinden
  	r[j] = q # wir setzen für den gegebene Index jetzt also das Maxima!
  return r[n], da sich hier am Ende das Maxima befinden muss!

Wir können aus dieser Betrachtung schon sehen, ,dass wir in der inneren Iteration nur eine kleine Boundary betrachten und nicht alles von 1 to n

Es ergibt sich folgende Laufzeit für diese Operation: Was schonmal besser als zuvor ist! Einfach, weil wir uns weniger Elemente pro Iteration anschauen, um ein Maxima zu finden.

Konstruktion der optimalen Lösung

Wir möchten jetzt nochmals betrachten, wie aus den vielen kleinen Teillösungen, die Lösung für das gesamte / große Problem geschaffen werden kann.

[!Definition] Rekonstruktion einer optimalen Lösung | Betrachtung des Schnitt-Problems #card Unter Betrachtung, dass wir hier viele kleine Lösungen zum zerschneiden einer Stange gefunden haben, möchten wir die optimale Lösung nochmal rekonstruieren / ihre Findung betrachten Durch das Speichern den optimalen Schnitt für das Teilproblem einer Stange der länge , können wir schon eine erste Annäherung finden. Das passiert indem wir jetzt folgend ersetzen:

if ( q < p[i] + r[j-i]) 
	q = p[i] + r[j-i]
	s[j] = i 

Wobei hier den ersten Schnitt im Teilproblem der Länge mit einer Länge von ist!. Also für Länge ist ein Schnitt an Stelle am besten! Wir erhalten anschließend eine Ausgabe von:

while ( n > 0) 
  print s[n]
  n = n- s[n]  			

Also wir geben jeweils die Stelle des Schnittes aus und verringern dann die Größe der Stange, bis wir bei 0 angekommen sind! ^1706623471513

Beispiel | Matrixmultiplikation optimieren

Wir möchten folgend eine Optimierung der Matrixmultiplikation von anschauen.

Auch hier bieten sich wieder verschiedene Ansätz an, das ganze zu berechnen.

[!Important] Greedy-Ansatz zur Berechnung Wir können sehr naiv und greedy eine erste Lösung dafür finden, brauchen aber drei nested For-loops, also werden wir sehr sehr langsam sein! Betrachten wir dafür folgenden PseudoCode zur Berechnung dieser MatrixMultiplikation

C = empty Matrix of size (A.col, B.row)
for ( Arow = 1 to A.row)
  for ( Bcol = 1 to B.col )
  	c(Arow,Bcol) = 0
  	for k = 1 to A.col
  		# iterating over available entries 
  		c(Arow,Bcol) = c(Arow,Bcol) + a(Arow,k)*b(k,Bcol)
  	end for 
  end for 
end for 

Die starker Vernestung der Schleifen gibt uns folgende eine Laufzeit von:

Betrachten wir diese Komplexität für die einfache Multiplikation, dann wird es sehr wichtig, für folgende Aufgabe eine bessere Berechnung finden zu können: Berechnen wir jetzt –> Dafür möchten wir eine optimale Klammerung finden, also das Problem so aufteilen ,dass wir es hoffentlich schneller berechnen können!

Idee zur Lösung: Wir können die Berechnung in kleine Matrixmultiplikationen aufteilen und so folgend immer in zweier-paaren berechnen: , Also folgend berechnen wir jetzt und den fortfolgenden . Diese beiden Terme können wir dann multiplizieren und dann über minimieren!

–> Es muss hier bei beiden Teilproblemen die Klammerung optimal sein!

[!Tip] Zusammengefasster Plan zur besseren Berechnung Wir möchten also folgend vorgehen: Berechne minimale Kosten für Anschließend wird das Minimum aller Multiplikationen für die einzelnen Matrizen sein Wir wollen anschließend noch bestimmen!

[!Warning] Matrix hat jetzt die Dimension

Betrachten wir jetzt die rekursive Berechnung: Hier wird sein, also das minimum, was wir schon kennen oder das neu berechnete minimum Weiterhin gilt:

Wir möchten weiterhin noch speichern, wobei das Minimum bestimmt

Aus dieser Betrachtung können wir jetzt eine 3-Fache-For-Schleife konstruieren

n = |Matrizen | -1 

for ( i = 1 to n) 
	min[i,i] = 0

for ( Index = 2 to n)
	for i = 1 to n-Index + 1
		J = i + Index -1
		min[i,j] = infinity
		for  k = i to j+1 
			q = min[i,k] + min[k+1,j] + p_i-1 * p_k *p_j
			if q < min[i,j] 
				min[i,j] = q 
				s[i,j] = k
		emd for
	end for 
end for 

und damit haben wir jetzt eine entsprechende Berechnung gefunden. Ihre Laufzeit beläuft sich auf , also ist richtig scheiße xd


Example | Edit Distances

[!Definition] Edit Distance what do we mean with those? #card We would like to define distances between strings x and y The given distance will define the steps to convert String X into String y by either :

  • deleting a letter
  • inserting a letter
  • replacing a letter by another The Edit Distance between two strings will be the minimal number of elementary operations that are required to transform x to y.

Use cases of edit distances are things like:

  • spell checks, and suggestions,
  • similiarity between genes,
  • in machine learning - similiar principle to compute distances between various structured objects >> graphs or such ^1706623471518

Computing the Edit Distances

Consider we are given two string and With we denote the problem of computing the edit distance between both prefixes of >> fractions of the actual word, we are computing it step by step. -> We will divide into three different separations that we can look after >> they all have one letter less, at different positions. Why this is possible, maybe be obvious with observing their three possible alignments:

  1. is aligned to That is the last letter is matched to “-” > no letter In that case
  2. Case 2: is aligned to Then
  3. is aligned to THen

As visual representation: ![[Pasted image 20230113110223.png]]

given diagram denotes all given cases. With these three cases we have the possibility to find the smallest distance by simply checking for these conditions at each given round.

The resulting pseudo-code would be:


for i = 0, 1, 2, . . . , m:  
	E(i, 0) = i  
for j = 1, 2, . . . , n:  
	E(0, j) = j  
for i = 1, 2, . . . , m:  
	for j = 1, 2, . . . , n:  
		E(i, j) = min{E(i − 1, j) + 1, E(i, j − 1) + 1, E(i − 1, j − 1) + diff(i, j)}  
return E(m, n)

Running Time

overall the running time is

  • we simply fill all the entries of the m x n table of problems E(i,j)
  • Each entry can be computed using preious entries in constant time.

Knapsack-Problem, but dynamic programmed

Review the Knapsack-problem from here [[111.39_greedyAlgorithm#Knapsack Problem]]

Idea for dynamic programming:

  • Define the problem K(V,j) the maximum value we can pack in a knapsack of volume V if can we just use items 1,…,j
  • Further we are looking for
  • we require two dynamics

For finding the best solution we observe two distinct cases:

  1. Case1 we dont use a given item j. Then we can compute : >> we dont do anything
  2. Case2 we use item j. -> Then we first pack j in the knapsack, and have to figure out which of the remaining items 1,…j-1 >> all but the one we took, will fit in the remaining volume.

>> we once assume the weight of our backpack without the new item and with the new item. and continue calculating from there. We thus can result with the given equation of:

Dynamic in the total weight :: ^1706623471522 We also want to be dynamic in the total weight: -> starting with total volume of 1, we increase the volume by 1 until we reach our set maximum.

So now that means we have to fill a _table of n x [] subproblems.

# Assumption: all vali and voli and voltotal are integers!  
for all (j = 1, ..., n), Initialize K (0, j) = 0, end for  
for all (v = 1, ..., voltotal ), Initialize K (v , 0) = 0, end for  
for j = 1, ..., n  
	for v = 1, ..., voltotal  
		if volj > v  
			K (v , j) = K (v , j − 1)  
		else  
			K (v , j) = max{K (v , j − 1), K (v − volj , j − 1) + valj }  

Return K (vtotal , n)

Whenever we set up dynamic programming we need to ensure that we have already solved the previous subproblems and when we need them. So the order of the loops can be critical.

In our knapsack case the whole system would not work if we have the loop of the volume and then the loop with the items

[!Definition] Running Time for Knapsack-Problem Attention: Denote that we do not make any assumptions on It might just be a fixed constant, than the complexity would boil down to Or it could be exponentially large with n, so the running time woulld be exponential in n as well.

we could also achieve a running time of where V is the sum of the values of all objects.