Օղակի վերացում ուղղորդված, կշռված գրաֆիկում

Օղակի վերացման ալգորիթմները սովորաբար օգտագործվում են օպտիմիզացման և գրաֆիկական ալգորիթմներում՝ կատարելագործելու աշխատանքը՝ հեռացնելով ավելորդ կամ ավելորդ օղակները հաշվարկներում: Թեև հատուկ օգտագործման դեպքերը կարող են տարբեր լինել, հայտնի օրինակները, որտեղ պահանջվում են օղակների վերացման ալգորիթմներ, են էջերի դասակարգման ալգորիթմները, մատրիցային բազմապատկումը, կոմպիլյատորների օպտիմալացումը և այլն:

Իմ օգտագործման դեպքը բավականին տարբեր է:

Այս տարի ես հնարավորություն ունեցա մեկնել հարավային Իսպանիա և վայելել իմ սիրելի զբաղմունքը՝ ճանապարհային հեծանվավարություն:

Մի քանի ընկերների հետ գնացի այնտեղ և հենց սկզբից պայմանավորվեցինք, որ բոլոր ծախսերը ձայնագրելու և կիսելու ենք։

Բջջային բազմաթիվ հավելվածներ ունեն նման ֆունկցիոնալություն: Մենք ընտրեցինք Tricount-ը: Դա հեշտ օգտագործման ծրագիր է, որը հետևում է ծախսերին, ցույց է տալիս մնացորդները և ճանապարհորդության վերջում առաջարկում է փողի բաշխման եղանակ:

Ես նախկինում ներդրել էի նմանատիպ ծրագրակազմ, այնպես որ ես բացատրեցի, թե ինչպես է ծրագրաշարի այս մասը աշխատում իմ ընկերներին լեռներում երկար զբոսանքներից մեկի ժամանակ:

Քանի որ դա հետաքրքիր թեմա է, ես կփորձեմ նույնը անել ձեզ համար, սիրելի ընթերցող…

Ես կօգտագործեմ մի խումբ երևակայական ընկերներ, ովքեր գնացել են ճանապարհորդության և որոշել են կիսել ծախսերը։ Ցանկացած մարդ կարող է վճարել ամեն ինչի համար, բայց նրանք համաձայնեցին, որ գրանցեն յուրաքանչյուր ծախս, իսկ հետո ծախսերը հավասարապես բաժանեն, ինչպես մենք արեցինք:

Ինչպես և շատ այլ բաներ, այս խնդիրը կարող է ներկայացվել գրաֆիկով.

Նման գրաֆիկը պետք է գագաթ ունենա խմբի յուրաքանչյուր ընկերոջ համար: Ուղղորդված եզրը ներկայացնում է վճարման պարտավորություն:

Եթե ​​գագաթից արտաքին եզր կա, ապա սկզբնական գագաթի ընկերը պարտական ​​է վերջավոր գագաթի ընկերոջը:

Արժեքը ներկայացված է եզրի կշռով:

Ահա այսպիսի գրաֆիկի օրինակ.

Եկեք ամփոփենք յուրաքանչյուր ընկերոջ մնացորդները (առաջին թիվն այն է, թե յուրաքանչյուր մարդ որքան է պարտք, իսկ երկրորդը, թե որքան է նա պարտք).

Ալիս՝ (-80+40) = -40

Բոբ՝ (-30+30) = 0

Չարլզ՝ (-60+40) = -20

Դիլան՝ (-20+80) = 60

Եթե ​​հաշվեկշիռը բացասական է, ապա տվյալ անձը պետք է վճարի մյուսներին (ուղևորության ընթացքում նա վճարել է ավելի քիչ, քան մյուսները): Եթե ​​մնացորդը դրական է, ապա կոնկրետ անձը ճանապարհորդության ընթացքում վճարել է ավելի շատ, քան մյուսները։

Քանի որ ընկերները համաձայնել են ծախսերը հավասարապես բաժանել, ընդհանուր մնացորդը միշտ պետք է լինի 0:

Մեր նպատակն է նվազագույնի հասցնել վճարումների քանակը և դրանց գումարները, որոնք մասնակիցները պետք է կատարեն ճանապարհորդության ավարտին:

Հենց այստեղ է հայտնվում հանգույցի վերացումը:

Մեր օրինակի գրաֆիկում մենք ինքներս հեշտությամբ կարող ենք նույնականացնել մի քանի հանգույց:

Սկսենք պարզից. Ալիսը պարտք է Չարլզին 20, մինչդեռ Չարլզը Ալիսին 40:

Այս օղակը կարող է վերացվել, եթե մենք հանենք Ալիսի պարտքը Չարլզին (20) այն գումարից, որը Չարլզը պետք է վճարի Ալիսին (40):

Կատարելով այս քայլը՝ մենք արդյունավետորեն վերացնում ենք մեկ օղակ, ինչի արդյունքում Էլիսը ոչինչ չի պարտք Չարլզին, և Չարլզը ստիպված է լինում միայն 40-ի փոխարեն Ալիսին վճարել 20:

Այժմ, երբ մենք հասկանում ենք, թե ինչպես է աշխատում հանգույցի վերացումը, մենք կավտոմատացնենք այս գործընթացը՝ ստեղծելով ալգորիթմ, որը կարող է անել վերը նշված քայլերը նույնիսկ շատ ավելի մեծ գրաֆիկներում:

Մենք կներկայացնենք մեր գրաֆիկը՝ օգտագործելով հիմնական զանգվածը: Մեկնարկային գագաթը կպահվի առաջին ինդեքսում, վերջի գագաթը երկրորդ ինդեքսում, իսկ համապատասխան արժեքը ցույց կտա առաջին անձից մյուսին պարտքի գումարը:

Այսպիսով, մեր օրինակում մեր գրաֆիկի համապատասխան մասը կունենա հետևյալ տեսքը.

$graph['alice']['charles'] = 20;
$graph['charles']['alice'] = 40;

Եկեք տեսնենք մեր կոդի առանցքը.

public function reducePayments(): self 
{

        // Get the first vertex in the graph
        $vertices = array_keys($this->graph);
        $firstVertex = reset($vertices);

        // Loop until there are no more cycles in the graph
        while ($cycle = $this->findCycle($firstVertex)) {
            // Cancel the cycle
            $this->cancelCycle($cycle);
        }

        return $this;

}

Ի՞նչ ենք մենք այստեղ անում։

Մենք փորձում ենք գտնել բոլոր ցիկլերը մեր գրաֆիկում և վերացնել դրանք մեկ առ մեկ:

Մեր findCycle մեթոդը հիմնականում իրականացնում է Depth First Search մեր գրաֆիկում: Ես չեմ պատրաստվում մանրամասնել, թե ինչպես է աշխատում DFS-ը, բայց եթե ձեզ հետաքրքրում է, ապա այս հոդվածի վերջում կարող եք գտնել հղում դեպի ամբողջական աղբյուրի կոդը:

Ցիկլը բացահայտելուց հետո մենք պետք է չեղյալ հայտարարենք այն՝ նվազեցնելով եզրերի քաշը օղակի մեջ ամենացածր քաշով, հետևելով նախորդ օրինակում ցուցադրված նույն մոտեցմանը:

protected function cancelCycle($cycle) 
{
        
     // Find the minimum weight of the cycle
     $min_weight = $this->minWeight($cycle);
        
     // Loop through the edges in the cycle
     foreach ($cycle as list($v, $u)) {
         // Subtract the minimum weight from the edge weight
         $this->graph[$u][$v] -= $min_weight;
     }
        
 }

Այս մեթոդը գտնում է ցիկլի մեջ ամենացածր քաշ ունեցող եզրը և դուրս է բերում այն ​​այդ ցիկլի բոլոր եզրերից:

Եթե ​​որևէ եզրի կշիռը դառնում է 0, ապա մենք այլևս այն չենք համարում եզր (և այն կրկին ՉԻ օգտագործվի մեր DFS որոնման մեջ).

protected function getAdjacentEdges($v) {
    return array_keys(
        // Do not return those edges which are already reduced to 0 weight ...
        array_filter($this->graph[$v], fn($w) => $w !== 0)
    );
}

Եթե ​​մենք գործադրենք մեր reducePayments մեթոդը, ապա մենք կստանանք հետևյալ նոր գրաֆիկը.

Դուք հեշտությամբ կարող եք ասել, որ այս գրաֆիկը իր մեջ որևէ օղակ չունի:

Եկեք արագ հաշվարկենք մնացորդները կրկին.

Ալիս՝ (-40+0) = -40

Բոբ: (-10+10) = 0

Չարլզ՝ (-20+0) = -20

Դիլան՝ (0+60) = 60

Ինչպես տեսնում եք, մնացորդները չեն փոխվել, սակայն վճարային գործարքների թիվը զգալիորեն նվազել է (8-ից 4):

Սա այն է, ինչ մենք ակնկալում էինք։

Հուսով եմ, որ այժմ դուք կարող եք բացատրել հանգույցի վերացումը ձեր ընկերներին, ինչպես նաև ճանապարհի վրա երկար ճանապարհորդությունների ժամանակ:

Ամբողջական սկզբնական կոդը կարելի է ներբեռնել այստեղից՝ https://gist.github.com/dihjital/e4353725a27dbbe12da402d771467883