Анализ на Задача 4 от конкурса на Musala Soft & PC Magazine
- Оцветяване
Поставената задача не е безизвестна и вероятно част от участниците в конкурса
са я срещали в една или друга форма. Въпреки това тази задача беше избрана от журито,
заради множеството подходи за решаване, които тя позволява, заради изобретателността
и нестандартния анализ, които доброто й решаване изискват.
Очевидно и леко за реализация решение е последователно да се изчерпват възможностите
за оцветяване. Backtracking техника би намерила решение, но времето което експоненциалната
й сложност (O(2^N)) изисква прави този вариант неприемлив - той би намерил решение
за приемливо време само когато броят на точките е много малък. Добри можем да наречем
решения със сложност O(N^2), O(N*logN), O(N). Решения с такава сложност предполагат
добър анализ на възможните конфигурации и техните елементи и някаква "умна" и бърза
стратегия за оцветяване.
Ще изложим решението на журито. Стратегията, която то реализира е показателна
със своята простота и висока ефективност - сложност O(N). Нека първо анализираме
поставената цел - в целевата конфигурация не е от значение как точно са подредени
точките, а е важно по всеки хоризонтал и всеки вертикал броят на черните и броят
на белите точки да се различават най-много с единица. Това, от своя страна означава,
че по всяка линия (хоризонтална или вертикална), на всяка една черна/бяла точка
може да се съпостави една бяла/черна точка, като евентуално една точка от линията
може да остане "самотна" - да няма съответна с противоположен цвят. Ако имаме целевото
оцветяване, тривиално e да намерим такова разбиване на точките по двойки по хоризонтали
и вертикали, като най много по една точка да остане "самотна" на хоризонтал и вертикал.
Сега ще се убедим и в обратното - ако имаме разбиване на точките по двойки по
указания начин, то лесно ще намерим и решение. Нека сме разбили точките по двойки
(като на нито една точка все още не е присвоен цвят) и следваме следната стратегия
за оцветяване (алгоритъмът е описан в псевдо код):
Докато съществува неоцветена точка (точка A), повтаряй:
начало
1. оцветяваме А в произволен цвят (например бяло)
2. обявяваме A за текуща точка
3. обявяваме цвета на точка A за текущ цвят
4. Ако текущата точка има съответна (учавства в двойка с) неоцветена
точка по хоризонтала (точка B), то:
4.1. оцветяваме точка B в цвят противоположен
на текущия цвят.
4.2. рекурсивно изпълняваме стъпка 5
като за текущи точка и цвят обявяваме точка B и нейния цвят.
5. Ако текущата точка има съответна (учавства в двойка с) неоцветена
точка по вертикала (точка B), то:
5.1. оцветяваме точка B в цвят противоположен
на текущия цвят.
5.2. рекурсивно изпълняваме стъпка 4
като за текущи точка и цвят обявяваме точка B и нейния цвят.
край.
Ще покажем, че следното условие е инвариантно, т.е остава изпълнено след всяка
итерация на цикъла: на всяка линия броят на черните точки е равен на броя на белите
точки, ако самотната точка на тази линия (ако такава съществува) не е оцетена; в
противен случай броят на черните точки се различва с единица от броя на белите.
Очевидно това условие е изпълнено в началото, когато нито една точка не е оцветена.
Ще покажем, че то остава в сила след всяка итерация на описания алгоритъм. От това
следва, че след приключването на алгоритъма задачата ще е решена, защото условието
ще е изпълнено и всички точки ще са оцветени. Нека разгледаме какво представлява
една итерация на алгоритъма: на практика всяка итерация оцветява максималната верига
от точки, такава че съдържа първоначално избраната точка и:
1. веригата е алтернираща по хоризонтали и вертикали,
т.е. всеки три последователни точки точки от веригата образуват прав ъгъл (а не
лежат на една права)
2. всички точки от веригата са неоцветени
Веригата е максимална когато не можем да я разширим повече нито от единия, нито
от другия й край. Т.е. за всеки от краищата й имаме че: или точката там не участва
в двойка (т.е. това е самотната точка на съответната линия), или точката, с която
образуват двойка е вече оцветена. Сега е моментът да обърнем внимание по какъв начин
сме оцветили веригата - всеки две съседни точки са оцветени в различен цвят. Лесно
се вижда, че междинните точки от веригата (тези, които имат съседи по веригата и
от двете си страни) не променят разликата между броя на черните и броя на белите
точки на линиите, на които лежат - това е така, защото за всяка от тях на хоризонтала
и верикала, на който лежи са добавени по една черна и една бяла точка. Повече внимание
изискват граничните случаи - точките, които са краища на веригата. Ако една точка
е крайна защото е самотна, то преди нейното оцветяване броят на черните точки е
бил равен на броя на белите точки по съответната линия (това следва от инвариантното
условие) и след нейното оцветяване разликата между черните и белите точки ще е единица,
т.е. отново запазваме условието. Ако една точка е крайна, защото съответната й е
вече оцветена (това може да се случи само ако съответната й точка е от същата верига),
то те задължително ще са с различни цветове и условието отново ще се запази. Двете
точки са с различни цветове заради условието за алтерниране на веригата и съответното
алтерниране на цветовете, т.е. за да стигнем до линия, която вече сме посетили трябва
да сме сменили цвета нечетен брой пъти и следователно сме "донесли" противоположния
на вече оцветената точка цвят. С това доказахме инвариантността на условието, а
съответно и коректността на алгоритъма. Забележете, че посоченият алгоритъм не прави
"грешни" ходове - всяка точка се оцветява веднъж и повече не се разглежда - това
определя линейната сложност на алгоритъма и високата му скорост.
Остана единствено да покажем как да разделим точките по двойки при дадена начална
конфигурация - това разбира се е тривиална задача, но даже и тук може да се прояви
съобразителност и да се спести време и памет. Това можем да сторим паралелно с прочитането
на входните данни. Тъй като възможните стойности на координатите на точките са силно
ограничени, можем да помним последната точка от всяка линия, която все още не е
свързана с друга по линията - когато нова точка от тази линия постъпи от входа -
маркираме двете точки като двойка в съответните структури и освобождаваме линията
да "чака" за следващата двойка.
Анализи на решения изпратени от участници:
Стоян Йорданов (RTF Format)
|