Musala Soft Logo Конкурс по програмиране на Musala Soft и PC Magazine Bulgaria PC Magazine Bulgaria Logo
  Състезателна система
Сезон 2010 - 2011
Сезон 2009 - 2010
Сезон 2008 - 2009
Сезон 2007 - 2008
Сезон 2006 - 2007
Сезон 2005 - 2006
Правила
Задача 1
Задача 2
Задача 3
Задача 4
Задача 5
Задача 6
Класиране
Финален кръг
Сезон 2004 - 2005
Сезон 2003 - 2004
Сезон 2002 - 2003
Сезон 2001 - 2002
Сезон 2000 - 2001

Задача 1 от брой 10/2005 - ТИЧАНЕ - Анализ

Веселин Георгиев


                За дадената задача не съществуванай-доброрешение, тъй като единственият алгоритъм, който гарантира получаване на оптималния отговор (най-дългия път) е пълното изчерпване. Дори и да се приложат оптимизации на изчерпването, то в голяма част от случаите то няма да успее да приключи работа при зададените ограничения.

                На такива задачи практиката показва, че е подходящо да се изпробват много различни методи за решение. Обикновено те не гарантират оптимален отговор, но затова пък работят задоволително бързо. В моето решение има точно 4 различни подхода за решаването на задачата.

                Първият, Greedy(), няма нужда да разисквам в детайли. Ако обозначим с goods[N][M] масивът със стойностите на лакомствата, а със spend[N][M] – масивът с изразходваните калории за прекосяване, то Greedy() избира този съсед, при който goods[y][x] – spend[y][x] е максимално. Разбира се, взима се под внимание дали вече не е минавано през квадратчето. След което прави избрания ход и отново, полакомначин, избрира следващия.

                Вторият метод е RandomWalk(). Той представлява хибрид междулакомо  ислучайнорешение. Избора на ход се прави по следния начин. Четирите съседа се сортират поапетитносткато се ползва същия критерий както при Greedy(). След което, в зависимост от една промелнива, наречена tolerance, се взимат тези съседи, които са с не повече от tolerance точки от най-”апетитниясъсед. От така избраните съседи се взима произволен и се прави съответния ход.

                Държанието на този алгоритъм силно зависи от tolerance. При малки стойности на промелнивата, метода се държи като чисто Greedy, а при големикато чистослучайно-вървящорешение. Изпробват се всички възможности за tolerance от 1 до 60.

                Проверката ми показва, че тези два метода не дават по-добри резултати от изчерпването на нито един тест, използван от журито, но в голяма част от тестовете дават еднакви резултати, което е добре за методи, работещи около 0.1 секунда, сравнени с изчерпването, което аз съм ограничил до 3 секунди.

                Третия метод е BFS(). Идеята е подобна на изчерпващо търсене в ширина, с една съществена разлика. И тя е следната: Ако до едно и също квадратче стигат два пътя с еднаква дължина и еднаква стойност остатъчни калории, те се считат за еднакви (макар, че може да не санапример, ако картата с неизядените квадратчета е различна). Ето и псевдокод на алгоритъма:

 

       // situaciq ::= poziciq + karta s izqdeni lakomstwa + ostavashti kalorii

       P, Q – opashki ot situacii

       A[N][M] – masiv s naj-dobrite kalorii do wsqka kletka

      

       P[0] = nachalnata_situaciq;

       dyljina_na_nameriniq_pyt = 0

       while (P ne e prazna) {

             Q stawa praznata opashka

             Zarejdame kletkite na masiva A s -infinity

             while (P ne e prazna) {

                    P.pop(S);

                    if (S e kraina situaciq) {

                           if (dyljina_na_namereniq_put > nai_dobra_dyljina) {

                                  saved_best = S

                           }

                    } else {

                           za_wseki_sysed D na S {

                                  if (kaloriite_s_koito_stigame_v_D > A[D.y][D.x]) {

                                        if (A[D.y][D.x] = -infinity)

                                               Q.push(D);

                                        else {

                                               namirame elementa v Q, koito e podobril

                                               A[D.y][D.x] predniq pyt i go zamestwame s

                                               D

                                        }

                                        podobrqwame A[D.y][D.x]

                                        zapisvame che v (D.y, D.x) sme doshli ot S

                                  }

                           }

                    }

             }

             P = Q

             dyljina_na_namereniq_put = dyljina_na_namereniq_put+1

       }

       vuzstanovqvame nai-dobriq pyt, polzwaiki zapisanoto saved_best

 

                Последният метод е Backtrace(). Иначе казанотърсене в дълбочина. За разлика от предния метод обаче, пази се пълен запис на ситуацията и така два пътя, които се различават само в картата с изядените лакомства се считат за различни и се разглеждат отделно.

                Търсенето обхожда съседите в ред, съобразен с критерия, ползван от Greedy(), почва се от най-”апетитниясъсед и се върви към най-безперспективния. За да не се разглеждат две еднакви ситуации (които съвпадат 1:1) се използва хеш-таблица със затворено хеширане. Методът за хеширане е заимстван от някой реализации на шах-алгоритми и представлява следното:

                Създават се 4 таблици, xor_X[M], xor_Y[N], xor_Length[MAXLEN] и xor_pos[N][M]. Тук MAXLEN се избира да съвпада с най-дългия възможен път, който за тази задача е 32768. Тези таблици се запълват със случайни числа. Хеш ключа се изчислява като побитово изключващо или (xor) на xor_X[<текущо x>], xor_Y[<текущо y>], xor_Lenght[<текуща дължина на пътя>] и xor_pos[y][x] за всеки (y, x), които още не са изядени. Полученото 32-битово число е напълно случайно, но съответства еднозначно на ситуацията, като две сходни ситуации (при съвсем малки разлики) ще имат напълно различни хеш-ключове.

                Така полученият ключ се дели с остатък на големината на хеш-таблицата. Тази големина е просто число, подбрано така, че хеш-таблицата да заема около 230 Мегабайта оперативна памет. Всеки елемент от хеш-таблицата представлява следната структура:

 

typedef struct{

       unsigned stop_key;

       int energy;

} hash_entry;

 

където energy пази с колко остатъчни калории може да се стигне до тази ситуация, а полето stop_key се използва за справяне с колизиите. Това се прави по следният начин. При разглеждане на ситуация, освен първия ключ, който в последствие се взима под модул големината на таблицата, се прави и втори ключ. Вторият ключ се създава по абсолютно същият начин, само че се използват различни таблици със случайни числа (т.е. за всяка таблица xor_...[...] има две версииедната се ползва за първото хеширане, другатаза второто). Вторият ключ няма нищо общо с първия и именно той се записва в stop_key. Идеята на stop_key е, че ако две ситуации са различни, но са съвпаднали по първия ключ (колизия), шансовете са изключително малки те да съвпаднат и по втория ключ (като вероятността за съвпадение е под 2-60). В началото хеш таблицата се запълва с нули, като хеширащата функция е модифицирана така, че никога не създава ключове със стойност 0. Това позволява да се разпознаватнезапълненитеклетки в хеш-таблицата. Ако се е получилалошаколизиясъвпадащ първи и несъвпадащ втори ключняма какво да се правирекурсията продължава напред.

                Сега, когато изчерпването стигне до ситуация, която съвпада по първи и по втори ключ в хеш таблицата, алгоритъма приема, че такава ситуация вече е разглеждана и в зависимост от полето energy (ако то е по-голямо или равно на текущо оставащите калории) рекурсията може да сеотреже”. Вкарани са и някои други оптимизации, като например, че хеширането не се прави на всяко извикване на рекурсията (защото то е с тежест N*M операции), а се пазят изчислени двата ключа и при преминаване в друга ситуация само се un-xor-ват старите x, y, length и се xor-ват новите (използва се изключително свойството, че (A xor B) xor B = A).

                Това е подробно решението ми. Greedy() и RandomWalk() вървият без ограничение по време. BFS() е отрязан на 1.2 секунди, а на Backtrace() му се отпускат 3 секунди. Ако се поиграе с тези ограничения, вероятно могат да се постигнат по-добри резултати. BFS() е по-бързо от Backtrace() в 7 тестови примера, по-бавно в 2 теста и са равни в останалите 11 теста.

 

Автор: Веселин Георгиев


За въпроси можете да ни пишете на адрес: konkurs@musala.com.

Supported by Musala Soft Ltd.

Copyright 2000-2010 by Musala Soft Ltd. All rights reserved.