Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
Dynami c Program m i n g
Dynamicprogrammingisatechniquethatappearsinmanyformsinlotsofvastlydifferentsituations.
Thecoreideaisasimpleapplicationofdivideandconquer.First,wesplittheproblemintoseveral
parts,allofwhicharesmallerinstancesofthesameproblem.Thisgivesusarecursivesolution.And
thenwesimplyruntherecursivesolution,makingsurenevertosolvethesamesubproblemtwice.
Thisisbestshownwithanexample(orafew).
Whenever you have a problem involving trees, it's always a good idea to think about a recursive
solution.Hereisone.Wewillpickoneofthennodestobetheroot.Ifthenodesarenumberedfrom1
ton,imaginepickingnodenumberi.Thissplitstheproblemnicelyinto2subproblems.Intheleft
subtree, we have a BST on i1 nodes, and there are Ci1 ways of building those. In the right
subtree,wehaveninodesandso, C ni waysofbuildingaBST.Thetwosubtreesareindependent
ofeachother,sowesimplymultiplythetwonumbers.
Thatisifwehavechosennodeitobetheroot.Pickingadifferentrootissuretogiveusadifferent
tree,sotogetallthepossibletrees,wepickallthepossiblerootnodesandmultiplythenumbersfor
thetwosubtreesthatweget.Thisgivesusaneat,recursiveformula.
n
Cn =i=1 Ci1 C ni
Ofcourse,weneedsomebasecases.Simplysetting C0 tobe1isenoughthereisjustonewayto
makeatreewithzeronodes.
A mathematician would now try to get a closed form solution to this recurrence, but we have
computers.Whatwillhappenifwesimplywritearecursivefunctiontocalculate Cn ?Hereitis.
intC(intn){
if(n==0)return1;
intans=0;
for(inti=1;i<=n;i++)
ans+=C(i1)*C(ni);
returnans;
}
Thisworksfine,butthereisaslightproblemit'sridiculouslyslow.It'snotjustexponentialintime;
it'sworse.C(n)makes2nrecursivecalls.Itisevenworsethanfactorial!Thisseemswrong.Afterall,
Cn onlydependsonthenumbers C0 through Cn1 ,andtherearenofthesenumbers.There
mustbealotofrepeatingrecursivecallstoC(i)thatareresponsibleforthehorriblerunningtime.
Hereisasimplewaytofixthat.Wewillkeepatableofpreviouslycomputedvaluesof Ci .Thenif
thefunctionC()iscalledwithparameteri,andwehavealreadycomputedC(i)oncebefore,wewill
simplylookupitsvalueinthetableandavoidspendingalotofrunningtime(again).Wewillneedto
CPSC 490 Dynamic Program ming: Memoization
initializethecellsofthistablewithsomedummyvaluesthatwouldtelluswhetherwehavecomputed
thevalueofthatcellalreadyornot.Thistechniqueiscalledmemoization,andhereishowitwould
changethecode.
intCval[1024];
intC(intn){
if(Cval[n])!=1)returnCval[n];
intans=0;
if(n==0)ans=1;
for(inti=1;i<=n;i++)
ans+=C(i1)*C(ni);
returnCval[n]=ans;
}
intmain(){
for(inti=0;i<1024;i++)Cval[i]=1;
//callC(n)foranynupto1023
}
TherunningtimeofC(n)isnowquadraticinnbecausetocomputeC(n),wewillneedtocomputeall
oftheC(i)valuesbetween0andn1,andeachonewillbecomputedexactlyonce,inlineartime.
Unfortunately,wewillnotbeabletocomputeC(1000)thiseasilybecauseitishugeandwillcausean
overflow,butthat'saminordetail.:)
The Cn arecalledCatalannumbers,andthereisactuallyaniceformulaforthem.
2 n !
C n=
n ! n1 !
Theyappearallovertheplace,includingthenumberofwaysoftriangulatingaconvexpolygonand
thenumberofwaysofputtingbracketsintoanexpression.Youcanfindlotsofcoolfactshere:
http://mathworld.wolfram.com/CatalanNumber.html
Lucky Numbers
Thistechniqueofrememberingpreviouslycomputedanswersinatable,thustradingoffmemoryfora
gainincomputationtime,iscalledmemoization.[Idon'tknowanyonewhohasagoodexplanationof
whythereisno'r'in"memoization".]Hereisafunlittleproblemthatcanbesolvedusingthesame
technique.
13isanunluckynumber.Infact,anynumberthatcontains13isalsounlucky.Forexample,130,213
234513235and2451325areallunlucky.Alloftheothernumbersareluckybydefault.Howmany
luckynumbersaretherewithndigits?
We will try the same approach here. First, we will find a recursive solution, and then add a
memoization table to it in order to avoid calling the recursive function twice with the same
parameters.
Inanndigitnumber,thefirstdigitcanbeanythingbetween1and9,butallsubsequentdigitscanbe
anythingbetween0and9.Thisdifferenceisalittleinconvenient,sowewillletthefirstdigitbe0as
well.Thisway,insteadofcountingnumberswithndigits,wewillcountallthenumberswithupton
digits.ThenifT[n]isthenumber oflucky numbers withuptondigits,wecancountthosewith
CPSC 490 Dynamic Program ming: Memoization
exactlyndigitsbysubtractingT[n1]fromT[n],eliminatingallthosewithfewerthanndigits.
SohowdowecomputeT[n]thenumberofluckynumberswithuptondigits?Let'slookatthefirst
digit.Itcanbeanything;wehave10choicesforit.However,whenitis1,thentheseconddigitcan
notbe3(orthenumberwouldstartwith"13").HereisarecursivefunctionforT[n].
T[ n]=10T[ n1]11T[ n2]
Thefirstterm( 10T[ n1] )says,"pickanyofthe10valuesforthefirstdigitandmakesurethe
restofthenumberislucky."Thiscountsalltheluckynumberswithuptondigits,butwegetafew
unluckyonesthere,too.Namely,wealsocountthenumbersthatstartwith13andhavenoother13s
appearinganywhere.Thesecondtermsubtractsallthe"almostlucky"numberswehaveerroneously
counted.ThereareexactlyT[n2]ofthembecausetheyalllooklike13xxx,wherexxxisan(n2)digit
luckynumber.
SinceT[n]dependsonbothT[n1]andT[n2],weneedtwobasecaseshere.T[0]is1,andT[1]is10.
HereisasimplerecursiveimplementationthatusesamemoizationtabletostorethevaluesofT[n].
intT[1024];
intlucky(intn){
if(T[n]==1)
T[n]=10*lucky(n1)lucky(n2);
returnT[n];
}
intmain(){
T[0]=1;
T[1]=10;
for(inti=2;i<1024;i++)T[i]=1;
//Calllucky(x)tocomputetheanswerforx
}
Onceagain,weuse1tomarkthespacesinthememoizationtablethatarestilluncomputed.This
time,wetakecareofthebasecasesinthemain()function,whichmakeslucky()extremelyshort.Note
thatwehavetoactuallyuserecursivecallstolucky(n1)andlucky(n2)inorderforthistowork.We
cannotsimplyuseT[n1]andT[n2].Thistime,thefunctionworksinlineartime,computingT[i]for
allthevaluesbetween2andthetargetnumber,n.Eachvaluetakesconstanttimetocomputeandis
computedexactlyonce.Withoutmemoization,thelucky()functionwouldrequireexponentialtime
2 n recursivecalls.Thedifferenceinrunningtimeishuge.
Hamilto ni a n Path
Alotofhardproblemscanbesolvedusingmemoization.Sometimes,itiseasytocomeupwitha
O n ! bruteforcesolutiontoaproblem,andinmanycases,addingmemoizationwillimprovethe
runningtimeto O 2n ,whichmeansanimprovementfromn=10or11ton=25or30.
RecalltheHamiltonianpathproblem.Inanunweighted graph,findapathfromstotthatvisitseach
vertexexactlyonce. Thenaivebacktracking solution would start atsand tryallof its neighbours
recursively, making sure never to visit the same vertex twice. Branchandbound can improve the
runningtimeconsiderably,onaverage,butintheworstcase,thisapproachisstillnfactorialintime.
Thealgorithmwouldlooksomethinglikethis.
CPSC 490 Dynamic Program ming: Memoization
boolseen[32];
voidhpath(intu){
if(u==t){/*Checkthatwehaveseenallvertices.*/}
else{
for(intv=0;v<n;v++)if(!seen[v]&&graph[u][v]){
seen[v]=true;
hpath(v);
seen[v]=false;
}
}
}
Whatwecandoischangetheseen[]arraytobeasingleinteger,usingonebitpervertex.Thenwe
can pass 'seen' as a second parameter to hpath(). Now if hpath() ever gets called with the same
parameters twice, we will not do the same work again. Instead, we will store the answer in a
memoizationtableandsimply returnit. Sincethereareonly n2n differentparameter pairs, and
each recursive call requires O n time (one 'for' loop), we will get a total running time of
O n2 2n muchbetterthan O n ! .
boolmemo[20][1<<20];
voidhpath(intu,intseen){
if(memo[u][seen])return;
memo[u][seen]=true;
if(u==t){/*checkthatseen==(1<<n)1(seeneveryvertex)*/}
else{
for(intv=0;v<n;v++)
if(!(seen&(1<<v))&&graph[u][v])
hpath(v,seen|(1<<v));
}
}
Initially,wewouldcallhpath()withu=sandseen=(1<<s)indicatingthatweareatsandhaveseen
salready.Thefirsttwolinesmakesurethathpath()isnevercalledtwicewiththesamevaluesofu
andseenthereisnoreasontodothesameworktwice.memo[][]isassumedtobeinitialisedtofalse
tostartwith.
Noteagainthatwearetradingmemory(20MBinthiscase)foranimprovementinrunningtime.This
isthemainadvantageofmemoization.Dynamicprogramming(DP)isatechniqueforfillingoutthe
memoizationtableiteratively,insteadofusingrecursion,andwewilllookatitnext.Manypeopleuse
thetermsDPandmemoizationinterchangeably.