Страницы: 1
RSS
Проверка древовидной иерархии на ошибки
 
Добрый вечер!  
 
В теории графов не силен.    
Дана организационная структура в виде кодов должностей (столбец A) и для каждой из них код должности руководителя (столбец B).  
Задача в том, чтобы максимально экономно проверить дерево на ошибки.    
 
Дополнительные комментарии:  
- значения в столбце A должны быть уникальны  
- только президент может не иметь кода должности руководителя в столбце B (orphan node)  
- в столбце A, президента может и не быть, но тогда его код будет в столбце B его подчиненных, т.е. в этом случае orphan nodes не допускаются  
- одна должность (child) не может иметь более одного руководителя (parent), т.е. не матричная организация  
- остальное как в классической иерархии  
 
Буду признателен даже за обрывочные идеи или указания на ресурсы. :-)
KL
 
Кирилл,  можно сделать за один проход по данным, плюс еще один проход по словарю:  
 
1. Создать один словарь Dictionary из всех элементов данных, независимо ID это или PARENT столбец, потому что какой-нибудь parent может оказаться одновременно и child-ом.  
 
2. Ключ каждого элемента словаря – из текстовых значений столбцов ID, PARENT  
 
3. Значение каждого элемента – массив (1 To 4) соответственно:  
 
- ChildCount As Integer (кол-во детей - складывать по ключу из столбца ID)  
- ParentCount As Integer  (кол-во родителей  - складывать по ключу из столбца PARENT)  
- Childs As String (список не более чем из первых 2-х элементов, если больше – добавить троеточие)  
- Parents As String (аналогично списку Childs)  
 
4. Массив отчета (0 To 5) заполнить из полученного словаря только для элементов с ChildCount>1 или ParentCount>1:  
 
- ID As String (ключ Key словаря)  
- ChildCount  As Integer  (только если > 1)  
- ParentCount As Integer  (только если > 1)  
- Childs As String (что получилось в словаре)  
- Parents As String (что получилось в словаре)  
- Description As String (Например: "ERRORS: ChildCount>1, ParentCount>1")  
 
5. Босса отслеживать в цикле п.3 по пустому значению в столбце PARENT (комментарий = "root"), а если не случилось, то (считать параллельно)по максимуму ChildCount  (комментарий = "calculated").    
 
6. В первой строке отчета прописать ID босса, а в комментарии - "root" или "calculated". Если root-боссов насчиталось несколько, то указать это в комментарии как ошибку. Перед тем как формировать остальные строки отчета по п.4 удобно удалить из словаря элемент Босса, чтобы исключить проверку в цикле.  
 
Может что-то не учел, но похоже, что исходные правила можно трактовать подобным образом. По-крайней мере, можно рассмотреть это как стартовый вариант идеи.
 
Владимир,  
 
Спасибо большое за практически готовый код :-) В выходные попробую воплотить и отпишусь на следующей неделе.
KL
 
Владимир,  
 
Сейчас вчитался, и похоже не хватает главных проверок. Или я чего-то не разглядел в описании? :-)  
 
Мои критерии - лишь описание дополнительных ситуаций.    
 
Самое главное - это проверка на то, имеем ли мы связанный ациклический граф (т.е. дерево). Тут нужно проверять не только на наличие сирот и наличие более одного родителя, но и на ситуации когда...    
 
отец - сын своему сыну  
1|2  
2|1  
 
дед - сын своему внуку  
1|2  
2|3  
3|1  
 
сын - отец сам себе  
1|1  
 
наверняка есть еще другие возможные ошибки.  
 
Убежден, что существует готовый алгоритм подобной проверки, но не могу найти.  
Подозреваю, что:  
- без рекурсии не обойтись  
- придется просто тупо строить дерево (тогда вопрос будет уже как)  
 
Спасибо за участие.
KL
 
Попробовал такую UDF:  
 
Function God(ByVal Child, ByVal Parent, ByRef ra As Range)  
   Dim res As Range: NewParent = Parent  
   Do  
       Set res = ra.Columns(1).Find(NewParent, , , xlWhole)  
       If Not res Is Nothing Then  
           NewParent = res.Next  
           If NewParent = Child Then God = "Loop present": Exit Function  
           If NewParent = "" Then God = res: Exit Function  
           If NewParent = res Then God = res & " child=parent": Exit Function  
       Else  
           God = NewParent: Exit Function  
       End If  
   Loop Until res Is Nothing  
End Function  
 
 
Вот что получилось: http://excelvba.ru/XL_Files/Sample__02-07-2010__4-27-23.zip  
 
В столбце результатов имеем что-то типа этого:  
 
28861  
1250 child=parent  
28861  
28861  
28861  
 
 
Может, анализ этого доп.столбца поможет в решении...
 
Протянув формулу до конца, наблюдаем, что в графе одна ошибка (в строке 261)  
1250 1250 Loop present  
 
Исправив эту ошибку (меняем 1250 - 1250 на 1250 - 28861), обнаруживаем, что дерево построено корректно - все пути ведут наверх, к 28861  
 
Или в моём алгоритме есть ошибка?
 
Кое-что забыл - самую малость)  
Помимо того, что для каждого ID надо найти родителя,  
надо ещё убедиться, что он у каждого один.  
(доп. столбец с формулой СЧЁТЕСЛИ)  
 
Вот итоговый файл: http://excelvba.ru/XL_Files/Sample__02-07-2010__4-48-31.zip  
 
Теперь уже можно утверждать с уверенностью - "дерево" в полном порядке  
 
Итого:  
Необходимое и достаточное условие для того, чтобы "дерево" было "правильным":  
1) в столбце GOD все значения одинаковы, кроме одного - нуля,  
2) в столбце COUNT все единицы.
 
EducatedFool,  
 
Спасибо большое. Похоже, это оно. Единственно, наверное придется цикл все же сделать конечным, а то если например поставить в ячейке [B295] вставить 420, то все повисает надолго :-) Но это уже мелочи.
KL
 
Похоже, дело не в цикле, а в цепной реакции пересчетов. Т.ч. думаю, если отказаться от формул и сделать все в коде, то скорее всего проблема исчезнет
KL
 
Пока как-то так, но еще не закончил:  
 
Function Top(ByVal Child, ByVal Parent, ByRef ra As Range)  
   Dim res As Range  
   Dim NewParent  
   Dim dups As Collection  
   Set dups = New Collection  
     
   NewParent = Parent  
   On Error GoTo errHandler  
   Do  
       Set res = ra.Find(NewParent, , , xlWhole)  
       If Not res Is Nothing Then  
           dups.Add NewParent, NewParent  
           NewParent = res.Next  
           If NewParent = "" Then Top = res:  Exit Function  
           If NewParent = res Then Top = "Child=Parent": Exit Function  
       Else  
           Top = IIf(NewParent = "", Child, NewParent): Exit Function  
       End If  
         
   Loop Until res Is Nothing  
   Exit Function  
errHandler:  
   Top = "Loop"  
End Function  
 
Sub CheckTree()  
   Dim rng As Range  
   Dim arr As Variant  
   Dim i As Long  
   Dim wsReport As Worksheet  
     
   Application.ScreenUpdating = False  
     
   On Error Resume Next  
   Application.DisplayAlerts = False  
   Worksheets("Report").Delete  
   Application.DisplayAlerts = True  
   On Error GoTo 0  
     
   Worksheets(1).Copy after:=Worksheets(1)  
     
   With ActiveSheet  
       .Name = "Report"  
       Set rng = .Range("A2", .Cells(.Rows.Count, 1).End(xlUp))  
     
       ReDim arr(1 To rng.Count, 1 To 1) As String  
         
       For i = 1 To UBound(arr)  
           If rng(i, 1) <> "" Then arr(i, 1) = Top(rng(i, 1), rng(i, 2), rng)  
       Next i  
         
       With rng.Offset(, 2)  
           .Value = arr  
       End With  
         
       'check for duplicated values in col1  
       'check for empty values in col1  
       'check for empty values in col2 >1  
       'color code values in col3  
   End With  
   Application.ScreenUpdating = True  
End Sub  
   
 
1) Пришлось добавить проверку на повтор NewParent, чтобы избежать бесконечного цикла в ситуации описанной в моем сообщении от 02.07.2010, 03:17  
   
2) Мне показалось, что строка  
 
If NewParent = Child Then God = "Loop present": Exit Function  
 
дублирует строку  
 
If NewParent = res Then God = res & " child=parent": Exit Function  
 
т.ч. я удалил первую, но может, я что-то не разглядел.  
 
Заранее спасибо за комментарии.
KL
 
а если просто   вот так?
Живи и дай жить..
 
"таб", соответственно, это первые два столбца
Живи и дай жить..
 
{quote}{login=EducatedFool}{date=02.07.2010 02:52}{thema=}{post}Кое-что забыл - самую малость)  
Помимо того, что для каждого ID надо найти родителя,  
надо ещё убедиться, что он у каждого один.  
(доп. столбец с формулой СЧЁТЕСЛИ)  
 
{/post}{/quote}  
 
 
если в первом столбце элементы уникальны, то родитель один по определению..  
 
количество уникальных можно проверить известной формулой и сравнить с числом строк
Живи и дай жить..
 
слэн,  
 
Большое спасибо. Очень визуально.    
Я слегка модифицировал формулу:  
 
=ЕСЛИ(И(B2<>"";B2<>A2);Т(ВПР(B2;$A$2:$B$391;2;0));"")  
 
Это в принципе вариант, но в моем случае, должностей может быть до 10.000, т.ч. визуальный анализ затруднен, да и формулы могут начать тормозить. Также усложняется проверка на то, все ли оканчиваются, на одной и той же должности.
KL
 
Так пока выглядит решение. Теперь дело за оптимизацией кода :-)  
 
Всем спасибо за участие!
KL
 
{quote}{login=KL}{date=03.07.2010 02:01}{thema=}{post}слэн,  
 
Большое спасибо. Очень визуально.    
Я слегка модифицировал формулу:  
 
=ЕСЛИ(И(B2<>"";B2<>A2);Т(ВПР(B2;$A$2:$B$391;2;0));"")  
 
Это в принципе вариант, но в моем случае, должностей может быть до 10.000, т.ч. визуальный анализ затруднен, да и формулы могут начать тормозить. Также усложняется проверка на то, все ли оканчиваются, на одной и той же должности.{/post}{/quote}  
 
так на этом же алгоритм и построить.  чуть-чуть модернизировав.. щас
 
вот  
 
Sub check()  
   Dim c As Collection  
   Dim pr, n&, i&, ar() As Long, m&, rez()  
   n = Cells(Rows.Count, 2).End(xlUp).Row - 1  
   m = WorksheetFunction.Max(Range("a2").Resize(n))  
   pr = Range("a2").Resize(n, 2)  
   ReDim Preserve pr(1 To n, 1 To 2)  
   ReDim rez(0)  
   ReDim ar(0 To m)  
   For i = 1 To n  
       ar(pr(i, 1)) = CLng(pr(i, 2))  
   Next  
   On Error GoTo er  
   For i = 1 To n  
       Set c = New Collection  
       c.Add pr(i, 2), CStr(pr(i, 2))  
       Do  
           c.Add ar(c(c.Count)), CStr(ar(c(c.Count)))  
       Loop  
cnt: Next  
   For i = 1 To UBound(rez)  
       Cells(rez(i) + 1, 1).Interior.ColorIndex = 3  
   Next  
   Exit Sub  
er: If c(c.Count) <> 0 Then  
       ReDim Preserve rez(0 To UBound(rez) + 1)  
       rez(UBound(rez)) = i  
   End If  
   Resume cnt  
End Sub
Живи и дай жить..
 
Слэн,  
 
Спасибо, попробую как выдастся свободная минутка.    
 
Правда вижу, что использовали функцию MAX, а с ней две проблемы:  
 
1) ID руководителя не обязательно больше или меньше, чем у подчиненного.Единственное требование к ID - уникальность  
 
2) значения все текстовые  
 
Спасибо еще раз.
KL
 
max используется только один раз для определения размерности вспомогательного массива, который используется для ускорения поиска(замена впр) - это предварительная индексация, затем не надо каждый раз использовать какой либо алгоритм поиска -любой элемент находится просто по индексу.  
 
для этого я и перевожу все в тип long. отдельно замечу, что размерность вспомогательного массива ar отсчитывается от нуля. Таким образом происходит намеренное "зацикливание" алгоритма, по которому, в свою очередь, определяется необходимость прехода к след значению.  
 
т.е. в индексе массива "зашифрованы"  ID, а в самих значениях, их parent.  
 
у какого-то id parent пустой - он преобразовывается в ноль. а нулевой элемент просто не инициируется, поэтому у него какбы и id, и parent равны нулю, поэтому на следующем шаге цикла очередное наращивание коллекции вызывает ошибку, так как ноль уже был.  
 
 
с 390 элементами работает мгновенно(даже на моем рабочем убогом компе) :)  
 
использование коллекции отлавливает не только появление двух подряд нулей в конце, но и вообще повторение элементов на уровне хоть  внуков, хоть прадедов..  
 
если потребуется ускориться, всегда можно перейти на dictionary, кот предлагал ZVI
 
Слэн,  
 
Спасибо еще раз. Я как раз переводил в Dictionary :-)  
 
Не очень понимаю смысл строки:  
 
ar(pr(i, 1)) = CLng(pr(i, 2))  
 
ведь pr(i, 1) вернет стринг, да еще и с кодом сотрудника, а это вернейшая ошибка "Subscript out of range", если учитывать, что:  
 
m = WorksheetFunction.Max(Range("a2").Resize(n)) -> всегда будет равно 0  
 
а значит:  
 
ReDim ar(0 To m) -> всегда будет равно ar(0 To 0)  
 
Или я не вижу очевидного?
KL
 
ОК. Понял. Если переделать код вот так, то будет работать в большинстве случаев, т.е. когда коды будут числовыми:  
 
Sub check()  
   Dim c As Collection  
   Dim pr, n&, i&, ar() As Long, m&, rez()  
   n = Cells(Rows.Count, 2).End(xlUp).Row - 1  
   m = Evaluate("MAX(--" & Range("a2").Resize(n).Address & ")")  
   pr = Range("a2").Resize(n, 2)  
   ReDim Preserve pr(1 To n, 1 To 2)  
   ReDim rez(0)  
   ReDim ar(0 To m)  
   For i = 1 To n  
       ar(CLng(pr(i, 1))) = CLng(pr(i, 2))  
   Next  
   On Error GoTo er  
   For i = 1 To n  
       Set c = New Collection  
       c.Add pr(i, 2), CStr(pr(i, 2))  
       Do  
       c.Add ar(c(c.Count)), CStr(ar(c(c.Count)))  
       Loop  
cnt: Next  
   For i = 1 To UBound(rez)  
       Cells(rez(i) + 1, 1).Interior.ColorIndex = 3  
   Next  
   Exit Sub  
er:  
   If c(c.Count) <> 0 Then  
       ReDim Preserve rez(0 To UBound(rez) + 1)  
       rez(UBound(rez)) = i  
   End If  
   Resume cnt  
End Sub  
 
Проблема в том, что коды мне не подвластны и могут иметь любой формат.  
 
Но мысль мне нравится.
KL
 
да, я забыл, что вручную перевел коды в числа.  
 
текстовый формат - это ничего. можно воспользоваться формулой макс(диапазон+0)  
 
тогда в принципе не обязательно преводить в long?  но, наверное все таки стоит.  
 
щас поэксперементирую.
Живи и дай жить..
 
вот
 
если коды нецифровые, тоже можно обойти  
 
допустим есть два массива id  и pr  с нечисловыми кодами..  
 
тогда ar можно получить так:  
 
for i=1 to n  
ar(i)=match(pr(i),id)  
next  
 
немножко дольше инициализация, но потом также быстро..
Живи и дай жить..
 
{quote}{login=слэн}{date=06.07.2010 02:44}{thema=}{post}если коды нецифровые, тоже можно обойти  
 
допустим есть два массива id  и pr  с нечисловыми кодами..  
 
тогда ar можно получить так:  
 
for i=1 to n  
ar(i)=match(pr(i),id)  
next  
 
немножко дольше инициализация, но потом также быстро..{/post}{/quote}  
 
Мне нравится! Пожалуй буду разрабатывать этот вариант. Спасибо, слэн.
KL
 
ну да. В общем то это просто сведение задачи к уже решенной :)  
 
интересно, можно ли обойтись без match..
Живи и дай жить..
 
можно попробовать вот так:  
 
Sub ini()  
Dim ar, pr, n&, i&, c As New Collection, rez()  
   ReDim rez(0)  
   n = Cells(Rows.Count, 2).End(xlUp).Row - 1  
   pr = Range("a2").Resize(n, 2)  
   On Error GoTo er  
   For i = 1 To n  
       c.Add c.Count + 1, CStr(pr(i, 1))  
   Next  
   For i = 1 To n  
       ar(i) = pr(c(CStr(pr(i, 2))), 1)  
   Next  
   Exit Sub  
er: ReDim Preserve rez(0 To UBound(rez) + 1)  
   rez(UBound(rez)) = i  
   Resume Next  
End Sub  
 
 
первый цикл все равно полезен для выявления повторов
Живи и дай жить..
Страницы: 1
Наверх