Hur man löser NP-svÄra problem med hjÀlp av parameteriserade algoritmer

Forskning Àr förmodligen den mest intressanta delen av vÄr utbildning. Idén Àr att redan pÄ universitetet pröva sig inom det valda omrÄdet. Till exempel gÄr studenter frÄn programmen Software Engineering och Machine Learning ofta för att göra forskningsprojekt pÄ företag (frÀmst JetBrains eller Yandex, men inte bara).

I det hÀr inlÀgget ska jag berÀtta om mitt projekt inom Computer Science. Inom ramen för arbetet har jag studerat och implementerat metoder för att lösa ett av de mest kÀnda NP-svÄra problemen: vertex-cover problemet.

Just nu utvecklas en intressant metod för NP-svÄra problem - parametriserade algoritmer. Jag ska försöka ge er en inblick, berÀtta om nÄgra enkla parametriserade algoritmer och beskriva en kraftfull metod som har hjÀlpt mig mycket. Jag presenterade mina resultat pÄ tÀvlingen PACE Challenge: efter de öppna testerna ligger min lösning pÄ tredje plats, medan de slutgiltiga resultaten kommer att bli kÀnda den 1 juli.

Hur man löser NP-svÄra problem med hjÀlp av parameteriserade algoritmer

Om mig

Jag heter Vasily Alfjorov, och jag avslutar just nu mitt tredje Är pÄ HSE - Sankt Petersburg. Jag har varit intresserad av algoritmer Ànda sedan gymnasiet, nÀr jag gick i Moskvas skola 179 och deltog framgÄngsrikt i informatikolympiader.

Ett Àndligt antal specialister inom parametriserade algoritmer gÄr in i en bar...

Exemplet Àr frÄn boken «Parameterized algorithms»

TÀnk dig att du Àr vakten pÄ en bar i en liten stad. Varje fredag kvÀll kommer hÀlften av staden till din bar för att koppla av, vilket ger dig en hel del besvÀr: du mÄste kasta ut de stökiga besökarna för att förhindra slagsmÄl. Till slut blir det trÄkigt och du beslutar dig för att ta förebyggande ÄtgÀrder.

Eftersom staden Àr liten vet du precis vilka par besökare som troligtvis kommer att brÄka om de hamnar i baren tillsammans. Du har en lista med n mÀnniskor som kommer till baren ikvÀll. Du beslutar dig för att inte slÀppa in nÄgra invÄnare pÄ ett sÀtt sÄ att ingen brÄkar. Samtidigt vill din chef inte förlora intÀkter och kommer att bli missnöjd om du inte slÀpper in fler Àn vÄgvektor mÀnniskor.

TyvĂ€rr Ă€r problemet du stĂ„r inför ett klassiskt NP-svĂ„rt problem. Du kanske kĂ€nner det som vertex cover, eller hur man löser ett uppdrag om toppbelĂ€ggning. För sĂ„dana problem Ă€r det generellt sett oklart om det finns algoritmer som fungerar pĂ„ rimlig tid. För att vara exakt, sĂ„ indikerar den icke-bevisade och ganska starka hypotesen ETH (Exponential Time Hypothesis) att detta problem inte kan lösas inom tiden Hur man löser NP-svĂ„ra problem med hjĂ€lp av parameteriserade algoritmer, vilket innebĂ€r att det inte finns nĂ„got bĂ€ttre Ă€n att pröva alla alternativ. Till exempel, om det kommer att komma n = 1000 personer till din bar. DĂ„ kommer ett komplett test att innebĂ€ra Hur man löser NP-svĂ„ra problem med hjĂ€lp av parameteriserade algoritmer alternativ, vilket Ă€r ungefĂ€r Hur man löser NP-svĂ„ra problem med hjĂ€lp av parameteriserade algoritmer — ofattbart mycket. Som tur Ă€r, har din ledning satt en begrĂ€nsning pĂ„ k = 10, sĂ„ antalet kombinationer som du behöver utvĂ€rdera Ă€r mycket mindre: antalet delmĂ€ngder av tio element Ă€r lika med Hur man löser NP-svĂ„ra problem med hjĂ€lp av parameteriserade algoritmer. Detta Ă€r redan bĂ€ttre, men det Ă€r fortfarande för mĂ„nga för att rĂ€kna pĂ„ en dag Ă€ven med en kraftfull kluster.
Hur man löser NP-svÄra problem med hjÀlp av parameteriserade algoritmer
För att utesluta risken för brÄk under sÄdan konfiguration av relationer mellan barbesökarna mÄste man inte slÀppa in Bob, Daniel och Fyodor. Lösningar dÀr endast tvÄ blir kvar ute finns inte.

Betyder detta att det Ă€r dags att ge upp och slĂ€ppa in alla? LĂ„t oss övervĂ€ga andra alternativ. Till exempel, man kan bara neka intrĂ€de till dem som troligtvis kommer brĂ„ka med ett stort antal mĂ€nniskor. Om nĂ„gon kan brĂ„ka med minst k + 1 andra mĂ€nniskor, sĂ„ kan han definitivt inte slĂ€ppas in — annars mĂ„ste alla k + 1 medborgare, som han kan brĂ„ka med, nekas intrĂ€de, vilket definitivt kommer att missnöja ledningen.

Anta att du har tagit bort alla som du kan enligt detta princip. SÄ alla andra kan brÄka med högst vÄgvektor mÀnniskor. Genom att ta bort vÄgvektor personer, kan du förhindra högst Hur man löser NP-svÄra problem med hjÀlp av parameteriserade algoritmer konflikter. SÄ om mer Àn Hur man löser NP-svÄra problem med hjÀlp av parameteriserade algoritmer personer Àr involverade i Ätminstone en konflikt, kan du definitivt inte förhindra dem alla. Eftersom det uppenbarligen finns helt konfliktsfria mÀnniskor som du kommer att slÀppa in, behöver du utvÀrdera alla delmÀngder som bestÄr av tio av tvÄ hundra mÀnniskor. De Àr ungefÀr Hur man löser NP-svÄra problem med hjÀlp av parameteriserade algoritmer, och en sÄdan mÀngd operationer kan redan utvÀrderas pÄ klustret.

Om vi kan sÀkert ta med helt konfliktfria personer, hur Àr det dÄ med dem som deltar i endast en konflikt? Egentligen kan vi ocksÄ slÀppa in dem, genom att stÀnga dörren för deras motstÄndare. I verkligheten, om Alice har konflikt endast med Bob, sÄ om vi slÀpper in Alice av de tvÄ, förlorar vi ingenting: Bob kan ha andra konflikter, medan Alice definitivt inte har nÄgra. Dessutom Àr det meningslöst för oss att inte slÀppa in dem bÄda. Efter sÄdana operationer ÄterstÄr inte mer Hur man löser NP-svÄra problem med hjÀlp av parameteriserade algoritmer Àn gÀster med osÀker öde: totalt har vi Hur man löser NP-svÄra problem med hjÀlp av parameteriserade algoritmer konflikter, dÀr varje konflikt har tvÄ deltagare och varje deltagare Àr involverad i Ätminstone tvÄ konflikter. Det betyder att vi bara behöver gÄ igenom Hur man löser NP-svÄra problem med hjÀlp av parameteriserade algoritmer alternativ, vilket mycket vÀl kan berÀknas pÄ en halv dag pÄ en laptop.

Egentligen kan enkla resonemang leda till Ă€nnu mer fördelaktiga förhĂ„llanden. LĂ„t oss notera att vi nödvĂ€ndigtvis mĂ„ste lösa alla tvister, det vill sĂ€ga frĂ„n varje konfliktpar behöva vĂ€lja minst en person som vi inte slĂ€pper in. LĂ„t oss övervĂ€ga följande algoritm: vi tar en konflikt och tar bort en deltagare och startar rekursivt frĂ„n resterande, sedan tar vi bort den andra och startar ocksĂ„ rekursivt. Eftersom vi vid varje steg tar bort nĂ„gon, blir rekursions trĂ€det för denna algoritm ett binĂ€rt trĂ€d med djup vĂ„gvektor, sĂ„ algoritmen arbetar totalt pĂ„ Hur man löser NP-svĂ„ra problem med hjĂ€lp av parameteriserade algoritmer, dĂ€r n — antalet noder, och m — antalet kanter. I vĂ„rt exempel handlar det om ungefĂ€r tio miljoner, vilket kan berĂ€knas pĂ„ brĂ„kdelar av en sekund, inte bara pĂ„ en laptop utan till och med pĂ„ en mobiltelefon.

Exemplet ovan Ă€r ett exempel pĂ„ parametriserad algoritm. Parametriserade algoritmer Ă€r algoritmer som fungerar pĂ„ tid f(k) poly(n), dĂ€r p — ett polynom, f — en godtycklig berĂ€kningsbar funktion, och vĂ„gvektor — nĂ„gon parameter som mycket vĂ€l kan vara mycket mindre Ă€n uppgiftens storlek.

Alla resonemang fram till denna algoritm ger ett exempel pĂ„ kĂ€rnalisering — en av de vanliga teknikerna för att skapa parametriska algoritmer. Kernalisering innebĂ€r att minska storleken pĂ„ en uppgift till ett vĂ€rde som begrĂ€nsas av en funktion av parametern. Den resulterande uppgiften kallas ofta för kĂ€rnan. Genom enkla resonemang om noderna fick vi en kvadratisk kĂ€rna för uppgiften Vertex Cover, parametiserad efter svarstyp. Det finns Ă€ven andra parametrar som kan vĂ€ljas för denna uppgift (till exempel Vertex Cover Above LP), men vi kommer att diskutera just denna parameter.

Pace Challenge

TÀvling PACE Challenge (The Parameterized Algorithms and Computational Experiments Challenge) började 2015 för att skapa en koppling mellan parametriska algoritmer och metoder som anvÀnds i praktiken för att lösa berÀkningsproblem. De första tre tÀvlingarna handlade om att hitta trÀdets bredd (Treewidth), att hitta Steiner-trÀd (Steiner Tree) och att hitta en uppsÀttning av noder som skÀr cykler (Feedback Vertex Set). I Är var en av uppgifterna dÀr man kunde pröva sina fÀrdigheter den ovan beskrivna uppgiften om nodöverdragning.

TÀvlingen blir mer populÀr för varje Är som gÄr. Om man ska tro preliminÀra uppgifter deltog endast 24 lag i Ärets tÀvling för att lösa uppgiften om nodöverdragning. Det Àr vÀrt att notera att tÀvlingen pÄgÄr inte i nÄgra timmar eller ens en vecka, utan i flera mÄnader. Lag har möjlighet att studera litteratur, komma med egna originalidéer och försöka implementera dem. I grund och botten representerar denna tÀvling ett forskningsarbete. Idéerna bakom de mest effektiva lösningarna och belöningarna för vinnarna presenteras i samband med konferensen IPEC (International Symposium on Parameterized and Exact Computation) som en del av den största Ärliga algoritmkonferensen i Europa ALGO. Mer information om sjÀlva tÀvlingen kan hittas pÄ webbplats, och resultaten frÄn tidigare Är finns ). Ett typiskt exempel pÄ anvÀndning:.

Lösningsschema

För att hantera problemet med toppöverdragning försökte jag tillÀmpa parameteriserade algoritmer. Dessa bestÄr vanligtvis av tvÄ delar: förenklingsregler (som i idealfallet leder till kernalisering) och delningsregler. Förenklingsreglerna Àr en förbehandling av ingÄngen pÄ polynomiell tid. MÄlet med att tillÀmpa sÄdana regler Àr att reducera problemet till ett ekvivalent problem av mindre storlek. Förenklingsreglerna Àr den mest kostsamma delen av algoritmen, och tillÀmpningen av just denna del leder till den totala körtiden. Hur man löser NP-svÄra problem med hjÀlp av parameteriserade algoritmer istÀllet för enkel polynomiell tid. I vÄrt fall baseras delningsreglerna pÄ att för varje topp behöver vi ta som svar antingen den eller dess granne.

Den allmÀnna planen Àr följande: vi tillÀmpar förenklingsreglerna, vÀljer sedan en topp, och gör tvÄ rekursiva anrop: i det första tar vi den som svar, och i det andra tar vi alla dess grannar. Detta kallar vi att dela (branching) vid denna topp.

I detta schema kommer exakt ett tillÀgg att göras i nÀsta stycke.

Idéer för delningsregler (branching)

LÄt oss diskutera hur vi vÀljer en topp vid vilken delningen ska Àga rum.
Huvudidén Àr mycket girig i algoritmisk mening: lÄt oss ta den topp med maximal grad och dela oss just vid den. Varför verkar det som om det Àr bÀttre? För att vi pÄ sÄ sÀtt i den andra grenen av det rekursiva anropet kommer att ta bort vÀldigt mÄnga toppar. Man kan förvÀnta sig att det kommer att finnas en liten graf kvar och att vi snabbt kan bearbeta den.

Denna metod med redan diskuterade enkla tekniker för kernalisering presterar ganska bra och löser vissa tester med nÄgra tusen toppar. Men till exempel fungerar den dÄligt för kubiska grafer (vilket innebÀr grafer dÀr graden av varje topp Àr tre).
Det finns ytterligare en idé, baserad pÄ en ganska enkel tanke: om grafen Àr icke-ansluten kan problemet med dess sammanhÀngande komponenter lösas oberoende, genom att sammanfoga svaren i slutet. Detta Àr förresten den lilla lovade modifieringen i schemat som avsevÀrt kommer att pÄskynda lösningen: tidigare arbetade vi i sÄ fall med produkten av tiderna för att rÀkna svar frÄn komponenterna, men nu arbetar vi med summan. Och för att pÄskynda delningen behöver vi omvandla den anslutna grafen till en icke-ansluten.

Hur gör man detta? Om grafen har en skÀrningspunkt, mÄste man bryta vid den. En skÀrningspunkt Àr en sÄdan toppunkt, vars borttagning gör grafen osammanhÀngande. Man kan hitta alla skÀrningspunkter i grafen med en klassisk algoritm pÄ linjÀr tid. Denna metod pÄskyndar brytningen avsevÀrt.
Hur man löser NP-svÄra problem med hjÀlp av parameteriserade algoritmer
Vid borttagning av vilken som helst av de markerade noderna kommer grafen att dela upp sig i sammanhÀngande komponenter.

Detta kommer vi att göra, men vi vill ha mer. Till exempel att söka smÄ nodskÀrningar i grafen och utföra delning baserat pÄ dessa noder. Det mest effektiva sÀttet jag kÀnner till för att hitta den minimala globala nodskÀrningen Àr att anvÀnda Gomory-Hu-trÀdet, som konstrueras pÄ kubisk tid. I PACE Challenge Àr den typiska storleken pÄ grafen flera tusen noder. Givet detta förhÄllande behöver man utföra miljarder operationer i varje nod i rekursionstrÀdet. Det visar sig att det Àr omöjligt att lösa problemet inom den givna tiden.

Vi försöker optimera lösningen. Den minimala nodskÀrningen mellan ett par noder kan hittas med vilken algoritm som helst som bygger ett maximalt flöde. Man kan anvÀnda Dinic-algoritmen, som i praktiken fungerar mycket snabbt. Jag Àr misstÀnksam mot att man teoretiskt kan bevisa en tidskomplexitetsbedömning Hur man löser NP-svÄra problem med hjÀlp av parameteriserade algoritmer, vilket redan Àr fullt acceptabelt.

Jag har försökt flera gÄnger att söka skÀrningar mellan par av slumpmÀssiga noder och ta den mest balanserade. TyvÀrr gav det dÄliga resultat pÄ de öppna testerna i PACE Challenge. Jag jÀmförde med en algoritm som skar med avseende pÄ noder av maximal grad, med en begrÀnsning för hur djupt man fick gÄ. Efter algoritmen som försökte hitta skÀrningen pÄ detta sÀtt kvarstod större grafer. Detta berodde pÄ att skÀrningarna blev mycket obalanserade: genom att ta bort 5-10 noder lyckades man endast skÀra bort 15-20.

Det Àr vÀrt att notera att i artiklar om de teoretiskt snabbaste algoritmerna anvÀnds mycket mer avancerade tekniker för att vÀlja noder för delning. SÄdana tekniker har en mycket komplicerad implementering och ofta dÄliga uppskattningar av tids- och minnesanvÀndning. Jag har inte lyckats extrahera nÄgra acceptabla metoder för praktisk tillÀmpning frÄn dem.

Hur man tillÀmpar förenklingsregler

Vi har redan idéer för kÀrnalisering. LÄt mig pÄminna om:

  1. Om det finns en isolerad nod, ta bort den.
  2. Om det finns en topp med grad 1, ta bort den och ta dess granne som svar.
  3. Om det finns en topp med grad Ätminstone k + 1, ta den som svar.

Det Àr klart med de tvÄ första, men den tredje har en liten knep. Om vi i det skÀmtande problemet om baren fick en övre grÀns pÄ vÄgvektor, sÄ mÄste man i PACE Challenge helt enkelt hitta ett minsta toppöverdrag. Det Àr en typisk omvandling av sökproblem till beslut/problem, och man gör ofta ingen skillnad mellan de tvÄ slags problem. I praktiken, om vi skriver en lösare för toppöverdraget, kan skillnaden vara mÀrkbar. Till exempel, som i den tredje punkten.

Sett ur ett implementeringsperspektiv kan man gÄ tillvÀga pÄ tvÄ sÀtt. Den första metoden kallas Iterative Deepening. Den innebÀr att vi kan börja med nÄgon rimlig nedre grÀns för svaret och sedan köra vÄr algoritm, med denna begrÀnsning som en övre grÀns för svaret, utan att gÄ djupare ner i rekursionen Àn den begrÀnsningen. Om vi hittar nÄgot svar Àr det garanterat optimalt, annars kan vi bara öka den grÀnsen med ett och köra igen.

Det andra tillvÀgagÄngssÀttet Àr att behÄlla nÄgot nuvarande optimalt svar och söka efter ett svar av mindre storlek, och nÀr man hittar det Àndrar man denna parameter vÄgvektor för att kapa onödiga grenar i sökningen.

Efter att ha genomfört nĂ„gra nattliga experiment stannade jag vid en kombination av dessa tvĂ„ metoder: först kör jag min algoritm med en viss begrĂ€nsning pĂ„ sökdjupet (vĂ€lj den sĂ„ att det tar försumbar tid i förhĂ„llande till huvudlösningen) och anvĂ€nder den bĂ€sta hittade lösningen som en övre grĂ€ns för svaret — det vill sĂ€ga den dĂ€r vĂ„gvektor.

Topp med grad 2

Vi har hanterat toppar med grad 0 och 1. Det visar sig att detta ocksÄ kan göras med toppar med grad 2, men grafen krÀver mer komplexa operationer.

För att förklara detta behöver vi pÄ nÄgot sÀtt benÀmna topparna. LÄt oss kalla en topp med grad 2 för topp v, och dess grannar för toppar x och y. Vi har nu tvÄ fall.

  1. NĂ€r x och y — grannar. DĂ„ kan vi ta som svar x och y, och v ta bort. Och verkligen, frĂ„n denna triangel mĂ„ste vi ta Ă„tminstone tvĂ„ toppar som svar och vi kommer definitivt inte att förlora om vi tar x och y: de har förmodligen fler grannar, medan v de har inga.
  2. NĂ€r x och y — inte grannar. DĂ„ pĂ„stĂ„s det att alla tre topparna kan klistras ihop till en. IdĂ©n Ă€r att det i sĂ„ fall finns ett optimalt svar, dĂ€r vi tar antingen v, eller bĂ„da topparna x och y. I det första fallet mĂ„ste vi ta alla grannar x och y, medan det i det andra inte Ă€r nödvĂ€ndigt. Detta motsvarar exakt de fall dĂ€r vi inte tar den sammanfogade toppen i svaret och dĂ€r vi gör det. Det Ă„terstĂ„r bara att notera att svaret frĂ„n denna operation minskar med ett.

Hur man löser NP-svÄra problem med hjÀlp av parameteriserade algoritmer

Det Àr vÀrt att notera att en sÄdan metod Àr ganska svÄr att genomföra noggrant pÄ ett rÀttvist linjÀrt sÀtt. Att klistra toppen Àr en komplicerad operation, man mÄste kopiera listorna över grannar. Om detta görs slarvigt kan man fÄ en asymptotiskt suboptimal körtid (till exempel, om man efter varje sammanslagning kopierar mÄnga kanter). Jag stannade vid att söka hela vÀgar frÄn toppar av grad 2 och analyserade en mÀngd olika fall, som cykler av sÄdana toppar eller av alla sÄdana toppar utom en.

Dessutom mÄste denna operation vara omvÀndbar, sÄ att vi kan ÄterstÀlla grafen till sitt ursprungliga skick nÀr vi ÄtervÀnder frÄn rekursionen. För att sÀkerstÀlla detta rensade jag inte listorna över kanter för de sammanfogade topparna, efter vilket jag visste exakt vilka kanter som skulle riktas. En sÄdan grafimplementation krÀver ocksÄ noggrannhet, men garanterar rÀttvist linjÀr tid. Dessutom, för grafer med flera tiotusentals kanter, ryms det ganska bra i processorens cache, vilket ger stora hastighetsfördelar.

LinjÀra kÀrnor

Till sist, den mest intressanta delen av kÀrnan.

LÄt oss först minnas att i bipartita grafer kan det minimiella vertextÀcket sökas pÄ Hur man löser NP-svÄra problem med hjÀlp av parameteriserade algoritmer. För detta Àr det nödvÀndigt att anvÀnda algoritmen Hopcroft-Karp för att hitta det maximala matchningen dÀr, och sedan anvÀnda teoremet Königs-Egervåri.

IdĂ©n om den linjĂ€ra kĂ€rnan Ă€r följande: först delar vi grafen, det vill sĂ€ga istĂ€llet för varje topp v introducerar vi tvĂ„ toppar Hur man löser NP-svĂ„ra problem med hjĂ€lp av parameteriserade algoritmer och Hur man löser NP-svĂ„ra problem med hjĂ€lp av parameteriserade algoritmer, och istĂ€llet för varje kant u — v introducerar vi tvĂ„ kanter Hur man löser NP-svĂ„ra problem med hjĂ€lp av parameteriserade algoritmer och Hur man löser NP-svĂ„ra problem med hjĂ€lp av parameteriserade algoritmerDen erhĂ„llna grafen blir tvĂ„delad. Vi ska hitta ett minimalt vertexöverdrag i den. Vissa hörn i den ursprungliga grafen kommer att förekomma dĂ€r tvĂ„ gĂ„nger, vissa bara en gĂ„ng, och vissa inte alls. Nemhauser-Trotters teorem hĂ€vdar att i detta fall kan vi ta bort de hörn som inte förekommit alls och ta med de som förekommit tvĂ„ gĂ„nger i svaret. Dessutom sĂ€ger den att frĂ„n de kvarstĂ„ende hörnen (de som förekommit en gĂ„ng) ska vi ta minst hĂ€lften i svaret.

Vi har just lĂ€rt oss att lĂ€mna högst 2k hörn i grafen. Och verkligen, om svaret Ă€r kvar — minst hĂ€lften av alla hörn, sĂ„ finns det högst 2k.

HÀr har jag lyckats ta ett litet steg framÄt. Det Àr tydligt att det kÀrna som byggs pÄ detta sÀtt beror pÄ vilket minimalt vertexöverdrag i den tvÄdelade grafen vi valt. Det vill sade att vi vill vÀlja det som gör antalet kvarvarande hörn sÄ lÄgt som möjligt. Tidigare kunde detta endast göras pÄ tid Hur man löser NP-svÄra problem med hjÀlp av parameteriserade algoritmer. Jag har dock kommit pÄ en implementering av den hÀr algoritmen som tar tid Hur man löser NP-svÄra problem med hjÀlp av parameteriserade algoritmer, vilket gör att detta kÀrna kan sökas i grafer med hundratusentals hörn vid varje branching-steg.

Resultat

Praktiken visar att min lösning fungerar bra pÄ tester med flera hundra hörn och flera tusen kanter. PÄ sÄdana tester kan man rimligt förvÀnta sig att lösningen hittas inom en halvtimme. Sannolikheten att hitta ett svar inom rimlig tid ökar i princip om grafen har tillrÀckligt mÄnga hörn med hög grad, till exempel grad 10 och uppÄt.

För att delta i tÀvlingen behövde vi skicka in vÄra lösningar till optil.io. Enligt den tabell som presenterades dÀr tabell, ligger min lösning pÄ öppna tester pÄ tredje plats av tjugo med stor marginal till andra plats. För att vara helt Àrlig Àr det inte helt klart hur lösningarna kommer att bedömas i sjÀlva tÀvlingen: till exempel, min lösning klarar fÀrre tester Àn lösningen pÄ fjÀrde plats, men de som den klarar gÄr snabbare.

Resultaten pÄ de stÀngda testerna kommer att bli kÀnda den första juli.

KĂ€lla: habr.com

Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar đŸ”„ Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster